Commits

Caleb Smith committed 0e9d3f8

Added a payroll test that uses the date filter template tag and fails due to
the off by one error

  • Participants
  • Parent commits 45abb02

Comments (0)

Files changed (7)

File timepiece/management/commands/check_entries.py

         all_entries = self.find_entries(people, start, *args, **kwargs)
         all_overlaps = self.check_all(all_entries, *args, **kwargs)
         if self.verbosity >= 1:
-            self.stdout.write('Total overlapping entries: %d\n' % all_overlaps)
+            print 'Total overlapping entries: %d' % all_overlaps
 
     def check_all(self, all_entries, *args, **kwargs):
         """
                 'last': user.last_name,
                 'total': user_total_overlaps,
             }
-            self.stdout.write('Total overlapping entries for user ' + \
-                '%(first)s %(last)s: %(total)d\n' % overlap_data)
+            print 'Total overlapping entries for user ' + \
+                '%(first)s %(last)s: %(total)d' % overlap_data
         return user_total_overlaps
 
     def find_start(self, **kwargs):
         forever = kwargs.get('all', False)
         if forever:
             if self.verbosity >= 1:
-                self.stdout.write('Checking overlaps from the beginning ' + \
-                    'of time\n')
+                print 'Checking overlaps from the beginning ' + \
+                    'of time'
         else:
             if self.verbosity >= 1:
-                self.stdout.write('Checking overlap starting on: ' + \
-                    start.strftime('%m/%d/%Y') + '\n')
+                print 'Checking overlap starting on: ' + \
+                    start.strftime('%m/%d/%Y')
 
     def show_name(self, person):
-        self.stdout.write('Checking %s %s...\n' % \
-        (person.first_name, person.last_name))
+        print 'Checking %s %s...' % \
+        (person.first_name, person.last_name)
 
     def show_overlap(self, entry_a, entry_b=None):
         def make_output_data(entry):
             output = 'Entry %(entry)d for %(first_name)s %(last_name)s from ' \
             % data_a + '%(start_time)s to %(end_time)s on %(project)s overlaps ' \
             % data_a + 'with another entry.'
-        self.stdout.write(output + '\n')
+        print output

File timepiece/models.py.orig

+import datetime
+import logging
+from decimal import Decimal
+
+from django.conf import settings
+from django.db import models
+from django.db.models import Q, Avg, Sum, Max, Min
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
+
+from timepiece import utils
+
+from dateutil.relativedelta import relativedelta
+from dateutil import rrule
+
+from datetime import timedelta
+
+
+class Attribute(models.Model):
+    ATTRIBUTE_TYPES = (
+        ('project-type', 'Project Type'),
+        ('project-status', 'Project Status'),
+    )
+    SORT_ORDER_CHOICES = [(x, x) for x in xrange(-20, 21)]
+    type = models.CharField(max_length=32, choices=ATTRIBUTE_TYPES)
+    label = models.CharField(max_length=255)
+    sort_order = models.SmallIntegerField(
+        null=True,
+        blank=True,
+        choices=SORT_ORDER_CHOICES,
+    )
+    enable_timetracking = models.BooleanField(default=False,
+        help_text='Enable time tracking functionality for projects with this '
+                  'type or status.',
+    )
+    billable = models.BooleanField(default=False)
+
+    class Meta:
+        unique_together = ('type', 'label')
+        ordering = ('sort_order',)
+
+    def __unicode__(self):
+        return self.label
+
+
+class Business(models.Model):
+    name = models.CharField(max_length=255, blank=True)
+    slug = models.SlugField(max_length=255, unique=True, blank=True)
+    email = models.EmailField(blank=True)
+    description = models.TextField(blank=True)
+    notes = models.TextField(blank=True)
+    external_id = models.CharField(max_length=32, blank=True)
+
+    def save(self, *args, **kwargs):
+        queryset = Business.objects.all()
+        if not self.slug:
+            if self.id:
+                queryset = queryset.exclude(id__exact=self.id)
+            self.slug = utils.slugify_uniquely(self.name, queryset, 'slug')
+        super(Business, self).save(*args, **kwargs)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        ordering = ('name',)
+
+
+class Project(models.Model):
+    name = models.CharField(max_length=255)
+    trac_environment = models.CharField(max_length=255, blank=True, null=True)
+    business = models.ForeignKey(
+        Business,
+        related_name='new_business_projects',
+    )
+    point_person = models.ForeignKey(User, limit_choices_to={'is_staff': True})
+    users = models.ManyToManyField(
+        User,
+        related_name='user_projects',
+        through='ProjectRelationship',
+    )
+    type = models.ForeignKey(
+        Attribute,
+        limit_choices_to={'type': 'project-type'},
+        related_name='projects_with_type',
+    )
+    status = models.ForeignKey(
+        Attribute,
+        limit_choices_to={'type': 'project-status'},
+        related_name='projects_with_status',
+    )
+    description = models.TextField()
+    billing_period = models.ForeignKey(
+        'RepeatPeriod',
+        null=True,
+        blank=True,
+        related_name='projects',
+    )
+
+    class Meta:
+        ordering = ('name', 'status', 'type',)
+        permissions = (
+            ('view_project', 'Can view project'),
+            ('email_project_report', 'Can email project report'),
+            ('view_project_time_sheet', 'Can view project time sheet'),
+            ('export_project_time_sheet', 'Can export project time sheet'),
+        )
+
+    def __unicode__(self):
+        return self.name
+
+    def trac_url(self):
+        return settings.TRAC_URL % self.trac_environment
+
+
+class RelationshipType(models.Model):
+    name = models.CharField(max_length=255, unique=True)
+    slug = models.CharField(max_length=255, unique=True, editable=False)
+
+    def save(self):
+        queryset = RelationshipType.objects.all()
+        if self.id:
+            queryset = queryset.exclude(id__exact=self.id)
+        self.slug = utils.slugify_uniquely(self.name, queryset, 'slug')
+        super(RelationshipType, self).save()
+
+    def __unicode__(self):
+        return self.name
+
+
+class ProjectRelationship(models.Model):
+    types = models.ManyToManyField(
+        RelationshipType,
+        related_name='project_relationships',
+        blank=True,
+    )
+    user = models.ForeignKey(
+        User,
+        related_name='project_relationships',
+    )
+    project = models.ForeignKey(
+        Project,
+        related_name='project_relationships',
+    )
+
+    class Meta:
+        unique_together = ('user', 'project')
+
+    def __unicode__(self):
+        return "%s's relationship to %s" % (
+            self.project.name,
+            self.user.get_full_name(),
+        )
+
+
+class Activity(models.Model):
+    """
+    Represents different types of activity: debugging, developing,
+    brainstorming, QA, etc...
+    """
+    code = models.CharField(
+        max_length=5,
+        unique=True,
+        help_text='Enter a short code to describe the type of ' + \
+            'activity that took place.'
+    )
+    name = models.CharField(
+        max_length=50,
+        help_text="""Now enter a more meaningful name for the activity.""",
+    )
+    billable = models.BooleanField(default=True)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        ordering = ('name',)
+        verbose_name_plural = 'activities'
+
+
+class Location(models.Model):
+    name = models.CharField(max_length=255, unique=True)
+    slug = models.CharField(max_length=255, unique=True)
+
+    def __unicode__(self):
+        return self.name
+
+
+class EntryManager(models.Manager):
+    def get_query_set(self):
+        qs = super(EntryManager, self).get_query_set()
+        qs = qs.select_related('activity', 'project__type')
+        # ensure our select_related are added.  Without this line later calls
+        # to select_related will void ours (not sure why - probably a bug
+        # in Django)
+        foo = str(qs.query)
+        qs = qs.extra({'billable': 'timepiece_activity.billable AND '
+                                   'timepiece_attribute.billable'})
+        return qs
+
+
+class EntryWorkedManager(models.Manager):
+    def get_query_set(self):
+        qs = super(EntryWorkedManager, self).get_query_set()
+        projects = getattr(settings, 'TIMEPIECE_PROJECTS', {})
+        return qs.exclude(project__in=projects.values())
+
+
+ENTRY_STATUS = (
+    ('unverified', 'Unverified',),
+    ('verified', 'Verified',),
+    ('approved', 'Approved',),
+    ('invoiced', 'Invoiced',),
+)
+
+
+class Entry(models.Model):
+    """
+    This class is where all of the time logs are taken care of
+    """
+
+    user = models.ForeignKey(User, related_name='timepiece_entries')
+    project = models.ForeignKey(Project, related_name='entries')
+    activity = models.ForeignKey(
+        Activity,
+        related_name='entries',
+    )
+    location = models.ForeignKey(
+        Location,
+        related_name='entries',
+    )
+    status = models.CharField(
+        max_length=24,
+        choices=ENTRY_STATUS,
+        default='unverified',
+    )
+    start_time = models.DateTimeField()
+    end_time = models.DateTimeField(blank=True, null=True)
+    seconds_paused = models.PositiveIntegerField(default=0)
+    pause_time = models.DateTimeField(blank=True, null=True)
+    comments = models.TextField(blank=True)
+    date_updated = models.DateTimeField(auto_now=True)
+
+    hours = models.DecimalField(max_digits=8, decimal_places=2, default=0)
+
+    objects = EntryManager()
+    worked = EntryWorkedManager()
+
+    def check_overlap(self, entry_b):
+        """
+        Given two entries, return True if they overlap, otherwise return False
+        
+        Does not factor in the pause time. (current implementation)
+        """
+        entry_a = self
+        #if entries are open, consider them to be closed right now
+        if not entry_a.end_time:
+            entry_a.end_time = datetime.datetime.now()
+        if not entry_b.end_time:
+            entry_b.end_time = datetime.datetime.now()
+        #Check the two entries against each other        
+        start_is_inside = entry_a.start_time > entry_b.start_time \
+            and entry_a.start_time < entry_b.end_time 
+        end_is_inside = entry_a.end_time > entry_b.start_time \
+            and entry_a.end_time < entry_b.end_time        
+        b_is_inside = entry_a.start_time < entry_b.start_time \
+            and entry_a.end_time > entry_b.end_time
+        overlap = start_is_inside or end_is_inside or b_is_inside
+        return overlap
+
+    def is_overlapping(self):
+        if self.start_time and self.end_time:
+            entries = self.user.timepiece_entries.filter(
+            Q(end_time__range=(self.start_time, self.end_time)) | \
+            Q(start_time__range=(self.start_time, self.end_time)) | \
+            Q(start_time__lte=self.start_time, end_time__gte=self.end_time))
+
+            totals = entries.aggregate(
+            max=Max('end_time'), min=Min('start_time'))
+
+            totals['total'] = 0
+            for entry in entries:
+                totals['total'] = totals['total'] + entry.get_seconds()
+
+            totals['diff'] = totals['max'] - totals['min']
+            totals['diff'] = totals['diff'].seconds + \
+                totals['diff'].days * 86400
+
+            if totals['total'] > totals['diff']:
+                return True
+            else:
+                return False
+        else:
+            return None
+
+    def clean(self):
+        if not self.user_id:
+            raise ValidationError('An unexpected error has occured')
+        if not self.start_time:
+            raise ValidationError('Please enter a valid start time')
+        start = self.start_time
+        if self.end_time:
+            end = self.end_time
+        #Current entries have no end_time
+        else:
+            end = start + datetime.timedelta(seconds=1)
+        entries = self.user.timepiece_entries.filter(
+            Q(end_time__range=(start, end)) | \
+            Q(start_time__range=(start, end)) | \
+            Q(start_time__lte=start, end_time__gte=end))
+        #An entry can not conflict with itself so remove it from the list
+        if self.id:
+            entries = entries.exclude(pk=self.id)
+        for entry in entries:
+            entry_data = {
+                'project': entry.project,
+                'activity': entry.activity,
+                'start_time': entry.start_time,
+                'end_time': entry.end_time
+            }
+            #Conflicting saved entries
+            if entry.end_time:
+                if entry.start_time.date() == start.date() \
+                and entry.end_time.date() == end.date():
+                    entry_data['start_time'] = entry.start_time.strftime(
+                        '%H:%M:%S')
+                    entry_data['end_time'] = entry.end_time.strftime(
+                        '%H:%M:%S')
+                    output = 'Start time overlaps with: ' + \
+                    '%(project)s - %(activity)s - ' % entry_data + \
+                    'from %(start_time)s to %(end_time)s' % entry_data
+                    raise ValidationError(output)
+                else:
+                    entry_data['start_time'] = entry.start_time.strftime(
+                        '%H:%M:%S on %m\%d\%Y')
+                    entry_data['end_time'] = entry.end_time.strftime(
+                        '%H:%M:%S on %m\%d\%Y')
+                    output = 'Start time overlaps with: ' + \
+                    '%(project)s - %(activity)s - ' % entry_data + \
+                    'from %(start_time)s to %(end_time)s' % entry_data
+                    raise ValidationError(output)
+
+        if end <= start:
+            raise ValidationError('Ending time must exceed the starting time')
+        return True
+
+    def save(self, **kwargs):
+        self.hours = Decimal('%.2f' % round(self.total_hours, 2))
+        super(Entry, self).save(**kwargs)
+
+    def get_seconds(self):
+        """
+        Determines the difference between the starting and ending time.  The
+        result is returned as an integer of seconds.
+        """
+        if self.start_time and self.end_time:
+            # only calculate when the start and end are defined
+            delta = self.end_time - self.start_time
+            seconds = delta.seconds - self.seconds_paused
+        else:
+            seconds = 0
+            delta = datetime.timedelta(days=0)
+
+        return seconds + (delta.days * 86400)
+
+    def __total_hours(self):
+        """
+        Determined the total number of hours worked in this entry
+        """
+        total = self.get_seconds() / 3600.0
+        #in case seconds paused are greater than the elapsed time
+        if total < 0:
+            total = 0
+        return total
+    total_hours = property(__total_hours)
+
+    def __is_paused(self):
+        """
+        Determine whether or not this entry is paused
+        """
+        return bool(self.pause_time)
+    is_paused = property(__is_paused)
+
+    def pause(self):
+        """
+        If this entry is not paused, pause it.
+        """
+        if not self.is_paused:
+            self.pause_time = datetime.datetime.now()
+
+    def pause_all(self):
+        """
+        Pause all open entries
+        """
+        entries = self.user.timepiece_entries.filter(
+        end_time__isnull=True).all()
+        for entry in entries:
+            entry.pause()
+            entry.save()
+
+    def unpause(self, date=None):
+        if self.is_paused:
+            if not date:
+                date = datetime.datetime.now()
+            delta = date - self.pause_time
+            self.seconds_paused += delta.seconds
+            self.pause_time = None
+
+    def toggle_paused(self):
+        """
+        Toggle the paused state of this entry.  If the entry is already paused,
+        it will be unpaused; if it is not paused, it will be paused.
+        """
+        if self.is_paused:
+            self.unpause()
+        else:
+            self.pause()
+
+    def __is_closed(self):
+        """
+        Determine whether this entry has been closed or not
+        """
+        return bool(self.end_time)
+    is_closed = property(__is_closed)
+
+    def clock_in(self, user, project):
+        """
+        Set this entry up for saving the first time, as an open entry.
+        """
+        if not self.is_closed:
+            self.user = user
+            self.project = project
+            if not self.start_time:
+                self.start_time = datetime.datetime.now()
+
+    def __billing_window(self):
+        return BillingWindow.objects.get(
+            period__users=self.user,
+            date__lte=self.end_time,
+            end_date__gt=self.end_time)
+    billing_window = property(__billing_window)
+
+    def __is_editable(self):
+        return self.status == 'unverified'
+    is_editable = property(__is_editable)
+
+    def __delete_key(self):
+        """
+        Make it a little more interesting for deleting logs
+        """
+        salt = '%i-%i-apple-%s-sauce' \
+            % (self.id, self.is_paused, self.is_closed)
+        try:
+            import hashlib
+        except ImportError:
+            import sha
+            key = sha.new(salt).hexdigest()
+        else:
+            key = hashlib.sha1(salt).hexdigest()
+        return key
+    delete_key = property(__delete_key)
+
+    def __unicode__(self):
+        """
+        The string representation of an instance of this class
+        """
+        return '%s on %s' % (self.user, self.project)
+
+    class Meta:
+        verbose_name_plural = 'entries'
+        permissions = (
+            ('can_clock_in', 'Can use Pendulum to clock in'),
+            ('can_pause', 'Can pause and unpause log entries'),
+            ('can_clock_out', 'Can use Pendulum to clock out'),
+            ('view_entry_summary', 'Can view entry summary page'),
+        )
+
+# Add a utility method to the User class that will tell whether or not a
+# particular user has any unclosed entries
+User.clocked_in = property(lambda user: user.timepiece_entries.filter(
+    end_time__isnull=True).count() > 0)
+
+
+class RepeatPeriodManager(models.Manager):
+    def update_billing_windows(self, date_boundary=None):
+        active_billing_periods = self.filter(
+            active=True,
+        ).select_related(
+            'project'
+        )
+        windows = []
+        for period in active_billing_periods:
+            windows += ((period,
+                period.update_billing_windows(date_boundary)),)
+        return windows
+
+
+class RepeatPeriod(models.Model):
+    INTERVAL_CHOICES = (
+        ('day', 'Day(s)'),
+        ('week', 'Week(s)'),
+        ('month', 'Month(s)'),
+        ('year', 'Year(s)'),
+    )
+    count = models.PositiveSmallIntegerField(
+        choices=[(x, x) for x in range(1, 32)],
+    )
+    interval = models.CharField(
+        max_length=10,
+        choices=INTERVAL_CHOICES,
+    )
+    active = models.BooleanField(default=False)
+
+    users = models.ManyToManyField(
+        User,
+        blank=True,
+        through='PersonRepeatPeriod',
+        related_name='repeat_periods',
+    )
+
+    objects = RepeatPeriodManager()
+
+    def __unicode__(self):
+        return "%d %s" % (self.count, self.get_interval_display())
+
+    def delta(self):
+        return relativedelta(**{str(self.interval + 's'): self.count})
+
+    def update_billing_windows(self, date_boundary=None):
+        if not date_boundary:
+            date_boundary = datetime.date.today()
+        windows = []
+        try:
+            window = self.billing_windows.order_by('-date').select_related()[0]
+        except IndexError:
+            window = None
+        if window:
+            start_date = window.date
+            while window.date + self.delta() <= date_boundary:
+                window.id = None
+                if window.date + self.delta() == window.end_date:
+                    # same delta as last time
+                    window.date += self.delta()
+                else:
+                    # delta changed, make sure to include extra time
+                    window.date = window.end_date
+                window.end_date += self.delta()
+                window.save(force_insert=True)
+            return self.billing_windows.filter(
+                date__gt=start_date
+            ).order_by('date')
+        else:
+            return []
+
+
+class BillingWindow(models.Model):
+    period = models.ForeignKey(RepeatPeriod, related_name='billing_windows')
+    date = models.DateField()
+    end_date = models.DateField()
+
+    class Meta:
+        get_latest_by = 'date'
+
+    def __unicode__(self):
+        return "%s through %s" % (self.date, self.end_date)
+
+    def next(self):
+        if not hasattr(self, '_next'):
+            try:
+                window = BillingWindow.objects.filter(
+                    period=self.period,
+                    date__gt=self.date,
+                ).order_by('date')[0]
+            except IndexError:
+                window = None
+            self._next = window
+        return self._next
+
+    def previous(self):
+        if not hasattr(self, '_previous'):
+            try:
+                window = BillingWindow.objects.filter(
+                    period=self.period,
+                    date__lt=self.date,
+                ).order_by('-date')[0]
+            except IndexError:
+                window = None
+            self._previous = window
+        return self._previous
+
+    def __entries(self):
+            return Entry.objects.filter(
+                end_time__lte=self.end_date,
+                end_time__gt=self.date)
+    entries = property(__entries)
+
+
+class PersonRepeatPeriod(models.Model):
+    user = models.ForeignKey(
+        User,
+        unique=True,
+        null=True,
+    )
+    repeat_period = models.ForeignKey(
+        RepeatPeriod,
+        unique=True,
+    )
+
+    def hours_in_week(self, date):
+        left, right = utils.get_week_window(date)
+        entries = Entry.worked.filter(user=self.user)
+        entries = entries.filter(end_time__gt=left,
+            end_time__lt=right, status='approved')
+        return entries.aggregate(s=Sum('hours'))['s']
+
+    def overtime_hours_in_week(self, date):
+        hours = self.hours_in_week(date)
+        if hours > 40:
+            return hours - 40
+        return 0
+
+    def total_monthly_overtime(self, day):
+        start = day.replace(day=1)
+        end = start + relativedelta(months=1)
+        weeks = utils.generate_weeks(start=start, end=end)
+        overtime = Decimal('0.0')
+        for week in weeks:
+            overtime += self.overtime_hours_in_week(week)
+        return overtime
+
+    def summary(self, date, end_date):
+        """
+        Returns a summary of hours worked in the given time frame, for this
+        user.  The setting TIMEPIECE_PROJECTS can be used to separate out hours
+        for paid leave that should not be included in the total worked (e.g.,
+        sick time, vacation time, etc.).  Those hours will be added to the
+        summary separately using the dictionary key set in TIMEPIECE_PROJECTS.
+        """
+        projects = getattr(settings, 'TIMEPIECE_PROJECTS', {})
+        user = self.user
+        entries = user.timepiece_entries.filter(end_time__gt=date,
+                                                end_time__lte=end_date,
+                                                status='approved')
+        data = {'billable': Decimal('0'), 'non_billable': Decimal('0')}
+        data['total'] = entries.aggregate(s=Sum('hours'))['s']
+        billable = entries.exclude(project__in=projects.values())
+        billable = billable.values(
+            'billable',
+        ).annotate(s=Sum('hours'))
+        for row in billable:
+            if row['billable']:
+                data['billable'] += row['s']
+            else:
+                data['non_billable'] += row['s']
+        data['total_worked'] = data['billable'] + data['non_billable']
+        data['paid_leave'] = {}
+        for name, pk in projects.iteritems():
+            qs = entries.filter(project=projects[name])
+            data['paid_leave'][name] = qs.aggregate(s=Sum('hours'))['s']
+        return data
+
+    def list_total_hours(self, N=2):
+        bw = BillingWindow.objects.filter(period=self.repeat_period).order_by(
+            '-date')[:N]
+        result = []
+        for b in bw:
+            result.append(self.user.timepiece_entries.filter(
+                end_time__lte=b.end_date,
+                end_time__gt=b.date
+            ).aggregate(total=Sum('hours')))
+        return result
+
+    class Meta:
+        permissions = (
+            ('view_person_time_sheet', 'Can view person\'s timesheet.'),
+            ('edit_person_time_sheet', 'Can edit person\'s timesheet.'),
+        )
+
+
+class ProjectContract(models.Model):
+    CONTRACT_STATUS = (
+        ('upcoming', 'Upcoming'),
+        ('current', 'Current'),
+        ('complete', 'Complete'),
+    )
+
+    project = models.ForeignKey(Project, related_name='contracts')
+    start_date = models.DateField()
+    end_date = models.DateField()
+    num_hours = models.DecimalField(max_digits=8, decimal_places=2,
+                                    default=0)
+    status = models.CharField(choices=CONTRACT_STATUS, default='upcomming',
+                              max_length=32)
+
+    def hours_worked(self):
+        # TODO put this in a .extra w/a subselect
+        if not hasattr(self, '_hours_worked'):
+            self._hours_worked = Entry.objects.filter(
+                project=self.project,
+                start_time__gte=self.start_date,
+                end_time__lt=self.end_date + datetime.timedelta(days=1),
+            ).aggregate(sum=Sum('hours'))['sum']
+        return self._hours_worked or 0
+
+    @property
+    def hours_assigned(self):
+        # TODO put this in a .extra w/a subselect
+        if not hasattr(self, '_hours_assigned'):
+            self._hours_assigned =\
+              self.assignments.aggregate(sum=Sum('num_hours'))['sum']
+        return self._hours_assigned or 0
+
+    @property
+    def hours_allocated(self):
+        allocations = AssignmentAllocation.objects.filter(
+            assignment__contract=self)
+        return allocations.aggregate(sum=Sum('hours'))['sum']
+
+    @property
+    def hours_remaining(self):
+        return self.num_hours - self.hours_worked()
+
+    @property
+    def weeks_remaining(self):
+        return utils.generate_weeks(end=self.end_date)
+
+    def __unicode__(self):
+        return unicode(self.project)
+
+
+class AssignmentManager(models.Manager):
+    def active_during_week(self, week, next_week):
+        q = Q(contract__end_date__gte=week, contract__end_date__lt=next_week)
+        q |= Q(contract__start_date__gte=week,
+            contract__start_date__lt=next_week)
+        q |= Q(contract__start_date__lt=week, contract__end_date__gt=next_week)
+        return self.get_query_set().filter(q)
+
+    def sort_by_priority(self):
+        return sorted(self.get_query_set().all(),
+            key=lambda contract: contract.this_weeks_priority_number)
+
+
+# contract assignment logger
+logger = logging.getLogger('timepiece.ca')
+
+
+class ContractAssignment(models.Model):
+    contract = models.ForeignKey(ProjectContract, related_name='assignments')
+    user = models.ForeignKey(
+        User,
+        related_name='assignments',
+    )
+    start_date = models.DateField()
+    end_date = models.DateField()
+    num_hours = models.DecimalField(max_digits=8, decimal_places=2,
+                                    default=0)
+    min_hours_per_week = models.IntegerField(default=0)
+
+    objects = AssignmentManager()
+
+    def _log(self, msg):
+        logger.debug('{0} - {1}'.format(self, msg))
+
+    def _filtered_hours_worked(self, end_date):
+        return Entry.objects.filter(
+            user=self.user,
+            project=self.contract.project,
+            start_time__gte=self.start_date,
+            end_time__lt=end_date,
+        ).aggregate(sum=Sum('hours'))['sum'] or 0
+
+    def filtered_hours_worked_with_in_window(self, start_date, end_date):
+        return Entry.objects.filter(
+            user=self.user,
+            project=self.contract.project,
+            start_time__gte=start_date,
+            end_time__lt=end_date,
+        ).aggregate(sum=Sum('hours'))['sum'] or 0
+
+    @property
+    def hours_worked(self):
+        if not hasattr(self, '_hours_worked'):
+            date = self.end_date + datetime.timedelta(days=1)
+            self._hours_worked = self._filtered_hours_worked(date)
+        return self._hours_worked or 0
+
+    @property
+    def hours_remaining(self):
+        return self.num_hours - self.hours_worked
+
+    @property
+    def this_weeks_priority_number(self):
+        """
+        Only works if already filtered to the current week. Otherwise groups
+        outside the range will be listed as ongoing instead of befor or after.
+        """
+        if not hasattr(self, '_priority_type'):
+            weeks = utils.get_week_window(datetime.datetime.now())
+            if self.end_date < weeks[1].date() \
+            and self.end_date >= weeks[0].date():
+                self._priority_type = 0
+            elif self.start_date < weeks[1].date() \
+            and self.start_date >= weeks[0].date():
+                self._priority_type = 1
+            else:
+                self._priority_type = 2
+        return self._priority_type
+
+    @property
+    def this_weeks_priority_type(self):
+        type_list = ['ending', 'starting', 'ongoing', ]
+        return type_list[self.this_weeks_priority_number]
+
+    def get_average_weekly_committment(self):
+        week_start = utils.get_week_start()
+        # calculate hours left on contract (subtract worked hours this week)
+        remaining = self.num_hours - self._filtered_hours_worked(week_start)
+        commitment = remaining / self.contract.weeks_remaining.count()
+        return commitment
+
+    def weekly_commitment(self, day=None):
+        self._log("Commitment for {0}".format(day))
+        # earlier assignments may have already allocated time for this week
+        unallocated = self.unallocated_hours_for_week(day)
+        self._log('Unallocated hours {0}'.format(unallocated))
+        reserved = self.remaining_min_hours()
+        self._log('Reserved hours {0}'.format(reserved))
+        # start with unallocated hours
+        commitment = unallocated
+        # reserve required hours on later assignments (min_hours_per_week)
+        commitment -= self.remaining_min_hours()
+        self._log('Commitment after reservation {0}'.format(commitment))
+        # if we're under the needed minimum hours and we have available
+        # time, then raise our commitment to the desired level
+        if commitment < self.min_hours_per_week \
+        and unallocated >= self.min_hours_per_week:
+            commitment = self.min_hours_per_week
+        self._log('Commitment after minimum weekly hours {0}'\
+            .format(commitment))
+        # calculate hours left on contract (subtract worked hours this week)
+        week_start = utils.get_week_start(day)
+        remaining = self.num_hours - self._filtered_hours_worked(week_start)
+        total_allocated = self.blocks.aggregate(s=Sum('hours'))['s'] or 0
+        remaining -= total_allocated
+        if remaining < 0:
+            remaining = 0
+        self._log('Remaining {0}'.format(remaining))
+        # reduce commitment to remaining hours
+        if commitment > remaining:
+            commitment = remaining
+        self._log('Final commitment {0}'.format(commitment))
+        return commitment
+
+    def allocated_hours_for_week(self, day):
+        week, next_week = utils.get_week_window(day)
+        allocs = AssignmentAllocation.objects
+        allocs = allocs.filter(assignment__user=self.user)
+        allocs = allocs.filter(date__gte=week, date__lt=next_week)
+        hours = allocs.aggregate(s=Sum('hours'))['s']
+        return hours or 0
+
+    def unallocated_hours_for_week(self, day):
+        """ Calculate number of hours left to work for a week """
+        allocated = self.allocated_hours_for_week(day)
+        self._log('Allocated hours {0}'.format(allocated))
+        try:
+            schedule = PersonSchedule.objects.filter(user=self.user)[0]
+        except IndexError:
+            schedule = None
+        if schedule:
+            unallocated = schedule.hours_per_week - allocated
+        else:
+            unallocated = 40 - allocated
+        return unallocated
+
+    def remaining_contracts(self):
+        assignments = ContractAssignment.objects.exclude(pk=self.pk)
+        assignments = assignments.filter(end_date__gte=self.end_date,
+                                         user=self.user)
+        return assignments.order_by('-end_date')
+
+    def remaining_min_hours(self):
+        return self.remaining_contracts().aggregate(
+            s=Sum('min_hours_per_week'))['s'] or 0
+
+    class Meta:
+        unique_together = (('contract', 'user'),)
+
+    def __unicode__(self):
+        return u'%s / %s' % (self.user, self.contract.project)
+
+
+class AllocationManager(models.Manager):
+    def during_this_week(self, user, day=None):
+        week = utils.get_week_start(day=day)
+        return self.get_query_set().filter(
+            date=week, assignment__user=user,
+            assignment__contract__status='current'
+            ).exclude(hours=0)
+
+
+class AssignmentAllocation(models.Model):
+    assignment = models.ForeignKey(ContractAssignment, related_name='blocks')
+    date = models.DateField()
+    hours = models.DecimalField(max_digits=8, decimal_places=2, default=0)
+
+    @property
+    def hours_worked(self):
+        if not hasattr(self, '_hours_worked'):
+            end_date = self.date + datetime.timedelta(weeks=1)
+            self._hours_worked = self.assignment.\
+                    filtered_hours_worked_with_in_window(self.date, end_date)
+        return self._hours_worked or 0
+
+    @property
+    def hours_left(self):
+        if not hasattr(self, '_hours_left'):
+            self._hours_left = self.hours - self.hours_worked
+        return self._hours_left or 0
+
+    objects = AllocationManager()
+
+
+class PersonSchedule(models.Model):
+    user = models.ForeignKey(
+        User,
+        unique=True,
+        null=True,
+    )
+    hours_per_week = models.DecimalField(max_digits=8, decimal_places=2,
+                                         default=0)
+    end_date = models.DateField()
+
+    @property
+    def furthest_end_date(self):
+        assignments = self.user.assignments.order_by('-end_date')
+        assignments = assignments.exclude(contract__status='complete')
+        try:
+            end_date = assignments.values('end_date')[0]['end_date']
+        except IndexError:
+            end_date = self.end_date
+        return end_date
+
+    @property
+    def hours_available(self):
+        today = datetime.date.today()
+        weeks_remaining = (self.end_date - today).days / 7.0
+        return float(self.hours_per_week) * weeks_remaining
+
+    @property
+    def hours_scheduled(self):
+        if not hasattr(self, '_hours_scheduled'):
+            self._hours_scheduled = 0
+            now = datetime.datetime.now()
+            for assignment in self.user.assignments.filter(end_date__gte=now):
+                self._hours_scheduled += assignment.hours_remaining
+        return self._hours_scheduled
+
+    def __unicode__(self):
+        return unicode(self.user)
+
+
+class UserProfile(models.Model):
+    user = models.OneToOneField(User, unique=True, related_name='profile')
+    default_activity = models.ForeignKey(Activity, blank=True, null=True)
+
+    def __unicode__(self):
+        return unicode(self.user)

File timepiece/tests/__init__.py.orig

 from timepiece.tests.projection import *
 from timepiece.tests.projects import *
 from timepiece.tests.user_settings import *
+from timepiece.tests.management import *
 #Test does not reflect current state of the code. Import here when fixed
-from timepiece.tests.management import *
-

File timepiece/tests/base.py

     def setUp(self):
         self.user = User.objects.create_user('user', 'u@abc.com', 'abc')
         self.user2 = User.objects.create_user('user2', 'u2@abc.com', 'abc')
+        self.superuser = User.objects.create_user('superuser', 'super@abc.com', 'abc')
         permissions = Permission.objects.filter(
             content_type=ContentType.objects.get_for_model(timepiece.Entry),
             codename__in=('can_clock_in', 'can_clock_out',
         )
         self.user.user_permissions = permissions
         self.user2.user_permissions = permissions
-
+        self.superuser.is_superuser = True
+        self.superuser.save()
         self.user = self.user
         self.activity = timepiece.Activity.objects.create(
             code="WRK",

File timepiece/tests/management.py

             'seconds_paused': 0,
             'status': 'verified',
         }
+        self.good_start = datetime.datetime.now() - datetime.timedelta(days=0, hours=8)
+        self.good_end = datetime.datetime.now() - datetime.timedelta(days=0)
+        self.bad_start = datetime.datetime.now() - datetime.timedelta(days=1, hours=8)
+        self.bad_end = datetime.datetime.now() - datetime.timedelta(days=1)
         #Create users for the test
         self.user.first_name = 'first1'
         self.user.last_name = 'last1'
             self.default_data.update({
                 'user': user,
             })
-            for day in range(0, 80):
+            #Range uses 1 so that good_start/good_end use today as valid times.
+            for day in range(1, 80):
                 self.default_data.update({
                     'start_time': datetime.datetime.now() - datetime.timedelta(days=day, hours=8),
                     'end_time': datetime.datetime.now() - datetime.timedelta(days=day,),
 
     #helper functions
     buffer_dict = {}
-    overlap_cp = re.compile(
-        '(?P<entry>\d+) for ' + \
-        '(?P<first>\w+) (?P<last>\w+) from ' + \
-        '(?P<start_time_str>\d+-\d+-\d+ \d+:\d+:\d+.\d+) to ' + \
-        '(?P<end_time_str>\d+-\d+-\d+ \d+:\d+:\d+.\d+) on ' + \
-        '(?P<project>\w+)',
-    )
-
-    def overlaps_to_dict(self, tupe_in):
-        dict_out = {
-            'entry': tupe_in[0],
-            'first_name': tupe_in[1],
-            'last_name': tupe_in[2],
-            'start_time_str': tupe_in[3],
-            'end_time_str': tupe_in[4],
-            'project': tupe_in[5],
-        }
-        return dict_out
-
     def check(self, *args, **kwargs):
         output = err = StringIO()
         call_command('check_entries', *args, stdout=output, stderr=err, **kwargs)
         output.seek(0)
         err.seek(0)
         out = output.read()
-        overlap_tupes = self.overlap_cp.findall(out)
-        overlaps = []
-        for tupe in overlap_tupes:
-            overlaps.append(self.overlaps_to_dict(tupe))
+        overlap = re.findall('Entry.*', out)
         out_list = out.split('\n')
         err_list = err.read().split('\n')
         self.buffer_dict = {
             'out': out_list,
             'err': err_list,
-            'overlap': overlaps
+            'overlap': overlap,
         }
         return self.buffer_dict
 
     def get_err(self, dict_in=buffer_dict):
         return dict_in.get('err', [])
 
-    def get_overlap(self, dict_in=buffer_dict):
-        return dict_in.get('overlap', [])
-
     def show_output(self, dict_in=buffer_dict):
         for string in self.get_output(dict_in):
             print string
         for string in self.get_err(dict_in):
             print string
 
-    def show_overlap(self, dict_in=buffer_dict):
-        for dict_item in self.get_overlap(dict_in):
-            print dict_item.items()
+    def make_entry(self, **kwargs):
+        valid = kwargs.get('valid', True)
+        if valid:
+            default_start = self.good_start
+            default_end = self.good_end
+        else:
+            default_start = self.bad_start
+            default_end = self.bad_end
+        user = kwargs.get('user', self.user)
+        start = kwargs.get('start_time', default_start)
+        end = kwargs.get('end_time', default_end)
+        data = self.default_data
+        data.update({
+            'user': user,
+            'start_time': start,
+            'end_time': end,
+        })
+        self.create_entry(data)
 
-    def testAllEntries(self):
-        self.default_data.update({
-            'start_time': datetime.datetime.now() - datetime.timedelta(days=0, hours=8),
-            'end_time': datetime.datetime.now() - datetime.timedelta(days=0,),
-        })
-        self.create_entry(self.default_data)
-#        check_1 = self.check('first1', verbosity=2)
-#        check_2 = self.check('first2', verbosity=2)
-        bars = check_1_2 = self.check('first1', 'first2', verbosity=2)
-        self.show_overlap(bars)
-        foos = self.get_overlap(check_1_2)
+    #tests
+    def testFindPeople(self):
+        #Find one person by icontains first or last name, return all if no args
+        people1 = check_entries.Command().find_people('firsT1')
+        people2 = check_entries.Command().find_people('LasT2')
+        all_people = check_entries.Command().find_people()
+        #obtain instances from the querysets
+        person1 = people1.get(pk=self.user.pk)
+        person2 = people2.get(pk=self.user2.pk)
+        all_1 = all_people.get(pk=self.user.pk)
+        all_2 = all_people.get(pk=self.user2.pk)
+        all_3 = all_people.get(pk=self.superuser.pk)
+        self.assertEqual(people1.count(), 1)
+        self.assertEqual(people2.count(), 1)
+        self.assertEqual(all_people.count(), 3)
+        self.assertEqual(person1, self.user)
+        self.assertEqual(person2, self.user2)
+        self.assertEqual(all_1, person1)
+        self.assertEqual(all_2, person2)
+        self.assertEqual(all_3, self.superuser)
+        
+    def testCheckOverlap(self):
+        #define start and end times relative to a valid entry
+        a_start_before = self.good_start - datetime.timedelta(minutes=5)
+        a_start_inside = self.good_start + datetime.timedelta(minutes=5)
+        a_end_inside = self.good_start + datetime.timedelta(minutes=5)
+        a_end_after = self.good_end + datetime.timedelta(minutes=5)
+        #Create a valid entry for today
+        self.make_entry(valid=True)
 
-#        print foos[0].get('entry', '')
-#        print check_1_2['out']
+        #Create a bad entry starting inside the valid one
+        self.make_entry(start_time=a_start_inside, end_time=a_end_after)
+        #Create a bad entry ending inside the valid one
+        self.make_entry(start_time=a_start_before, end_time=a_end_after)
+        #Create a bad entry that starts and ends outside a valid one
+        self.make_entry(start_time=a_start_inside, end_time=a_end_inside)
+        #Create a bad entry that starts and ends inside a valid one
+        self.make_entry(start_time=a_start_before, end_time=a_end_after)
+
+        entries = timepiece.Entry.objects.filter(user=self.user)
+        user_total_overlaps = 0
+        for index_a, entry_a in enumerate(entries):
+            for index_b in range(index_a, len(entries)):
+                entry_b = entries[index_b]
+                if entry_a.check_overlap(entry_b):
+                    print 'Conflict with %s and %s' % (entry_a, entry_b)
+                    user_total_overlaps += 1
+#                    check_entries.Command().show_overlap(entry_a, entry_b)
+#                    self.show_overlap(entry_a, entry_b)
+
+
+
+#Use:
+#check_entries.Command().method()
+        
+        
+#    def testAllEntries(self):
+#        self.default_data.update({
+#            'start_time': datetime.datetime.now() - datetime.timedelta(days=0, hours=8),
+#            'end_time': datetime.datetime.now() - datetime.timedelta(days=0,),
+#        })
+#        self.create_entry(self.default_data)
+#        bars = check_1_2 = self.check('first1', 'first2', verbosity=2)
+#        self.show_output(bars)
+#        print bars['overlap']

File timepiece/tests/payroll.py

 from timepiece import forms as timepiece_forms
 from timepiece.tests.base import TimepieceDataTestCase
 
-from dateutil import relativedelta
+from dateutil.relativedelta import relativedelta
 
 
 class PayrollTest(TimepieceDataTestCase):
             minutes = 0
         if not start:
             start = datetime.datetime.now()
+            #In case the default would fall off the end of the billing period
+            if start.day >= 28:
+                start -= relativedelta(days=1)
         end = start + datetime.timedelta(hours=hours, minutes=minutes)
         data = {'user': self.user,
                 'start_time': start,
         if status:
             data['status'] = status
         return self.create_entry(data)
-
-    def testPersonSummary(self):
+    def make_logs(self):
         sick = self.create_project()
         vacation = self.create_project()
         settings.TIMEPIECE_PROJECTS = {
             'sick': sick.pk, 'vacation': vacation.pk
         }
-        rp = self.create_person_repeat_period({'user': self.user})
-        start = datetime.date.today().replace(day=1)
-        end = start + relativedelta.relativedelta(months=1)
         billable = self.log_time(delta=(3, 30), status='approved')
         non_billable = self.log_time(delta=(2, 0),
             billable=False, status='approved')
         sick = self.log_time(delta=(8, 0), project=sick, status='approved')
         vacation = self.log_time(delta=(4, 0), project=vacation,
             status='approved')
-        #if the billing period can't contain these hours, expand it
-        if end < datetime.date.today() + relativedelta.relativedelta(days=2):
-            end += relativedelta.relativedelta(days=1)
+        #make an entry on the very last day no matter the current time.
+        end_day = datetime.datetime.now() + relativedelta(months=1, day=1) - \
+            relativedelta(days=1)
+        last_day = self.log_time(start=end_day, status='approved', delta=(8,0))
+
+    def testPersonSummary(self):
+        self.make_logs()
+        rp = self.create_person_repeat_period({'user': self.user})
+        start = datetime.date.today().replace(day=1)
+        end = start + relativedelta(months=1)
         summary = rp.summary(start, end)
-        self.assertEqual(summary['billable'], Decimal('3.50'))
+        self.check_summary(summary)
+    
+    def testPersonSummaryView(self):
+        from timepiece.templatetags import timepiece_tags as tags
+        self.client.login(username='superuser', password='abc')
+        self.make_logs()
+        rp = self.create_person_repeat_period({'user': self.user})
+        response = self.client.get(reverse('payroll_summary'), follow=True)
+        context = response.context
+        date_filters = tags.date_filters(context, 'months')
+        this_month = date_filters['filters'].values()[0][-1]
+        this_month_url = this_month[1]
+        response = self.client.get(this_month_url, follow=True)
+        start = response.context['from_date']
+        end = response.context['to_date']
+        this_user = response.context['periods'].get(user=self.user.pk)
+        summary = this_user.summary(start, end)
+        self.check_summary(summary)
+
+    def check_summary(self, summary):
+        self.assertEqual(summary['billable'], Decimal('11.50'))
         self.assertEqual(summary['non_billable'], Decimal('2.00'))
         self.assertEqual(summary['paid_leave']['sick'], Decimal('8.00'))
         self.assertEqual(summary['paid_leave']['vacation'], Decimal('4.00'))
-        self.assertEqual(summary['total'], Decimal('17.50'))
+        self.assertEqual(summary['total'], Decimal('25.50'))
 
     def testWeeklyHours(self):
         """ Test basic functionality of hours worked per week """

File timepiece/utils.py.orig

+from dateutil import rrule
+from dateutil.relativedelta import relativedelta
+
+from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.template.defaultfilters import slugify
+
+from django.contrib.sites.models import Site
+from datetime import date, datetime, timedelta, time as time_obj
+import time
+import calendar
+
+
+def slugify_uniquely(s, queryset=None, field='slug'):
+    """
+    Returns a slug based on 's' that is unique for all instances of the given
+    field in the given queryset.
+
+    If no string is given or the given string contains no slugify-able
+    characters, default to the given field name + N where N is the number of
+    default slugs already in the database.
+    """
+    new_slug = new_slug_base = slugify(s)
+    if queryset:
+        queryset = queryset.filter(**{'%s__startswith' % field: new_slug_base})
+        similar_slugs = [value[0] for value in queryset.values_list(field)]
+        i = 1
+        while new_slug in similar_slugs:
+            new_slug = "%s%d" % (new_slug_base, i)
+            i += 1
+    return new_slug
+
+
+def render_with(template_name):
+    """
+    Renders the view wrapped by this decorator with the given template.  The
+    view should return the context to be used in the template, or an
+    HttpResponse.
+
+    If the view returns an HttpResponseRedirect, the decorator will redirect
+    to the given URL, or to request.REQUEST['next'] (if it exists).
+    """
+    def render_with_decorator(view_func):
+        def wrapper(*args, **kwargs):
+            request = args[0]
+            response = view_func(*args, **kwargs)
+
+            if isinstance(response, HttpResponse):
+                if isinstance(response, HttpResponseRedirect) and \
+                  'next' in request.REQUEST:
+                    return HttpResponseRedirect(request.REQUEST['next'])
+                else:
+                    return response
+            else:
+                # assume response is a context dictionary
+                context = response
+                return render_to_response(
+                    template_name,
+                    context,
+                    context_instance=RequestContext(request),
+                )
+        return wrapper
+    return render_with_decorator
+
+
+def determine_period(the_date=date.today(), delta=0):
+    """
+    Determine the start and end date for an accounting period.  If a date
+    is passed in, that date will be used to determine the accounting period.
+    If no date is passed in, the current date will be used.
+    """
+    delta = int(delta)
+
+    try:
+        # attempt to get the configuration for the current site
+        site = Site.objects.get_current()
+        config = site.timepiececonfiguration
+    except:
+        raise Exception('Please configure Pendulum for %s!' % site)
+
+    if config.is_monthly and (config.month_start >= 1 \
+    and config.month_start <= 31):
+        if config.month_start == 1:
+            #if the periods start on the first of the month, just use the first
+            #and last days of each month for the period
+            if delta > 0:
+                diff = the_date.month - delta
+                if diff < 1:
+                    # determine how many years to go back
+                    years = abs(diff / 12)
+
+                    # determine how many months to go back
+                    months = delta - (years * 12)
+                    if months == the_date.month:
+                        # don't give an invalid month
+                        months = -1
+                        years += 1
+
+                    # now set the year and month
+                    year = the_date.year - years
+                    month = the_date.month - months
+                else:
+                    year, month = the_date.year, diff
+            else:
+                year, month = the_date.year, the_date.month
+
+            num_days = calendar.monthrange(year, month)[1]
+            sy, sm, sd = year, month, 1
+            ey, em, ed = year, month, num_days
+        else:
+            # if the periods don't start on the first of the month, try to
+            # figure out which days are required
+
+            sy, sm, sd = the_date.year, the_date.month, config.month_start
+
+            # now take the delta into account
+            if delta > 0:
+                diff = sm - delta
+                if diff < 1:
+                    # determine how many years to go back
+                    years = abs(diff / 12)
+
+                    # determine how many months to go back
+                    months = delta - (years * 12)
+                    if months == sm:
+                        # don't give an invalid month
+                        months = -1
+                        years += 1
+
+                    # now set the year and month
+                    sy, sm = sy - years, sm - months
+                else:
+                    sm = diff
+
+            if the_date.day >= config.month_start:
+                # if we are already into the period that began this month
+                if sm == 12:
+                    # see if the period spans into the next year
+                    ey, em = sy + 1, 1
+                else:
+                    # if not, just add a month and subtract a day
+                    ey, em = sy, em + 1
+            else:
+                # if we are in the period that ends this month
+                if sm == 1:
+                    # and we're in January, set the start to last december
+                    sy, sm = sy - 1, 12
+                    ey, em = sy + 1, 1
+                else:
+                    # otherwise, just keep it simple
+                    sm = sm - 1
+                    ey, em = sy, sm + 1
+
+            ed = sd - 1
+
+            # this should handle funky situations where a period begins on the
+            # 31st of a month or whatever...
+            num_days = calendar.monthrange(ey, em)[1]
+            if ed > num_days:
+                ed = num_days
+
+    elif config.install_date and config.period_length:
+        #if we have periods with a set number of days...
+        #find out how many days have passed since the installation date
+        diff = the_date - config.install_date
+
+        #find out how many days are left over after dividing the number of days
+        #since installation by the length of the period
+        days_into_period = diff.days % config.period_length
+
+        #determine the start date of the period
+        start = the_date - timedelta(days=days_into_period)
+
+        #now take into account the delta
+        if delta > 0:
+            start = start - timedelta(days=(delta * config.period_length))
+            end = start + timedelta(days=config.period_length - 1)
+        else:
+            #determine the end date of the period
+            end = the_date + \
+            timedelta(days=(config.period_length - days_into_period - 1))
+
+        sy, sm, sd = start.year, start.month, start.day
+        ey, em, ed = end.year, end.month, end.day
+    else:
+        raise Exception('Invalid Pendulum configuration for %s' % site)
+
+    start_date = datetime(sy, sm, sd, 0, 0, 0)
+    end_date = datetime(ey, em, ed, 23, 59, 59)
+
+    return (start_date, end_date)
+
+DEFAULT_TIME_FORMATS = [
+    '%H:%M',        # 23:15         => 23:15:00
+    '%H:%M:%S',     # 05:50:21      => 05:50:21
+    '%I:%M:%S %p',  # 11:40:53 PM   => 23:40:53
+    '%I:%M %p',     # 6:21 AM       => 06:21:00
+    '%I %p',        # 1 pm          => 13:00:00
+    '%I:%M:%S%p',   # 8:45:52pm     => 23:45:52
+    '%I:%M%p',      # 12:03am       => 00:03:00
+    '%I%p',         # 12pm          => 12:00:00
+    '%H',           # 22            => 22:00:00
+]
+
+
+def parse_time(time_str, input_formats=None):
+    """
+    This function will take a string with some sort of representation of time
+    in it.  The string will be parsed using a variety of formats until a match
+    is found for the format given.  The return value is a datetime.time object.
+    """
+    formats = input_formats or DEFAULT_TIME_FORMATS
+
+    # iterate over all formats until we find a match
+    for format in formats:
+        try:
+            # attempt to parse the time with the current format
+            value = time.strptime(time_str, format)
+        except ValueError:
+            continue
+        else:
+            # turn the time_struct into a datetime.time object
+            return time_obj(*value[3:6])
+
+    # return None if there's no matching format
+    return None
+
+
+def get_total_time(seconds):
+    """
+    Returns the specified number of seconds in an easy-to-read HH:MM:SS format
+    """
+    hours = int(seconds / 3600)
+    seconds %= 3600
+    minutes = int(seconds / 60)
+    seconds %= 60
+
+    return u'%02i:%02i:%02i' % (hours, minutes, seconds)
+
+
+def get_week_start(day=None):
+    if not day:
+        day = date.today()
+    if day.isoweekday() != 7:
+        week_start = day - timedelta(days=day.isoweekday())
+    else:
+        week_start = day
+    return week_start
+
+
+def generate_weeks(end, start=None):
+    start = get_week_start(start)
+    return rrule.rrule(rrule.WEEKLY, dtstart=start, until=end, byweekday=6)
+
+
+def get_week_window(day):
+    start = get_week_start(day)
+    end = start + timedelta(weeks=1)
+    weeks = generate_weeks(start=start, end=end)
+    return list(weeks)
+
+
+def date_filter(func):
+    def inner_decorator(request, *args, **kwargs):
+        from timepiece import forms as timepiece_forms
+        if request.GET:
+            form = timepiece_forms.DateForm(request.GET)
+            if form.is_valid():
+                from_date = form.cleaned_data.get('from_date')
+                to_date = form.cleaned_data.get('to_date')
+                form.save()
+                status = form.cleaned_data.get('status')
+                activity = form.cleaned_data.get('activity')
+            else:
+                raise Http404
+        else:
+            form = timepiece_forms.DateForm()
+            today = date.today()
+            from_date = today.replace(day=1)
+            to_date = from_date + relativedelta(months=1)
+            status = activity = None            
+        return func(request, form, from_date, to_date, status, activity,
+            *args, **kwargs)
+    return inner_decorator