diff --git a/tock/tock/templates/403.html b/tock/tock/templates/403.html index cf62a4de9..6a5a35cb6 100644 --- a/tock/tock/templates/403.html +++ b/tock/tock/templates/403.html @@ -1,8 +1,6 @@ {% extends "base.html" %} {% block content %} - -
You don't have access to view this page.
- +If you need access, please contact your supervisor.
{% endblock %} diff --git a/tock/tock/templates/utilization/group_utilization.html b/tock/tock/templates/utilization/group_utilization.html index 16f00dc8f..86587399b 100644 --- a/tock/tock/templates/utilization/group_utilization.html +++ b/tock/tock/templates/utilization/group_utilization.html @@ -4,132 +4,118 @@ {% endblock %} {% block content %} ++ The following report contains users who are marked as billable in Tock, + organized by their unit as listed in Tock. Both attributes may be easily + updated via the Employees page of the Tock admin + interface. Within each unit, employees are ordered alphabetically by last name. +
++ The contents of this page may only be viewed by Tock users who are marked as + "staff" users. This attribute may be updated via the Tock + admin interface, as well, via the Users page. +
++ Utilization is calculated by dividing the total number of hours submitted on + projects that are marked "billable" in Tock, divided by the total number of + hours submitted on all projects for the given period. +
+- The following report contains users who are marked as billable in Tock, - organized by their unit as listed in Tock. Both attributes may be easily - updated via the Employees page of the Tock admin - interface. Within each unit, employees are ordered alphabetically by last name. -
-- The contents of this page may only be viewed by Tock users who are marked as - "staff" users. This attribute may be updated via the Tock - admin interface, as well, via the Users page. -
-- Utilization is calculated by dividing the total number of hours submitted on - projects that are marked "billable" in Tock, divided by the total number of - hours submitted on all projects for the given period. -
-Name | +
+ Last Week + (Ending {{ through_date }}) + % billable (billable hrs / total hrs) + |
+
+ Last Four Weeks + ({{ recent_start_date }} - {{ through_date }}) + % billable (billable hrs / total hrs) + |
+ Fiscal Year to Date + (Ending {{ through_date }}) + % billable (billable hrs / total hrs) + |
+
---|---|---|---|
Totals: | +
+
+ {{ unit.last.utilization }} + ({{ unit.last.billable_hours }} / {{ unit.last.total_hours }}) + + |
+
+
+ {{ unit.recent.utilization }} + ({{ unit.recent.billable_hours }} / {{ unit.recent.total_hours }}) + + |
+
+
+ {{ unit.fytd.utilization }} + ({{ unit.fytd.billable_hours }} / {{ unit.fytd.total_hours }}) + + |
+
Name | -Last Week (Ending {{ through_date }}) % billable (billable hrs / total hrs) |
- Last Four Weeks ({{ recent_start_date }} - {{ through_date }}) % billable (billable hrs / total hrs) |
- Fiscal Year to Date (Ending {{ through_date }}) % billable (billable hrs / total hrs) |
-
---|---|---|---|
Totals: | -
-
- {% for ut in unit_totals %}
- {% if i.1 is ut.last.unit_name %}
- {{ ut.last.utilization }} - ({{ ut.last.billable_hours }} / {{ ut.last.total_hours }}) - {% endif %} - {% endfor %} - - |
-
-
- {% for ut in unit_totals %}
- {% if i.1 is ut.last.unit_name %}
- {{ ut.recent.utilization }} - ({{ ut.recent.billable_hours }} / {{ ut.recent.total_hours }}) - {% endif %} - {% endfor %} - - |
-
-
- {% for ut in unit_totals %}
- {% if i.1 is ut.last.unit_name %}
- {{ ut.fytd.utilization }} - ({{ ut.fytd.billable_hours }} / {{ ut.fytd.total_hours }}) - {% endif %} - {% endfor %} - - |
-
- {{ userdata.user_data }} - | -- {% if userdata.last_all_hours_total %} - - {{ userdata.last }} - ({{ userdata.last_billable_hours_total }} / - {{ userdata.last_all_hours_total }}) - - {% else %} - -- - {% endif %} - | -- {% if userdata.recent_all_hours_total %} - {{ userdata.recent }} - ({{ userdata.recent_billable_hours_total }} / - {{ userdata.recent_all_hours_total }}) - {% else %} - -- - {% endif %} - | -- {% if userdata.fytd_all_hours_total %} - {{ userdata.fytd }} - ({{ userdata.fytd_billable_hours_total }} / - {{ userdata.fytd_all_hours_total }}) - {% else %} - -- - {% endif %} - | -
+ {{ userdata }} + | ++ {% if userdata.last_all_hours_total %} + + {{ userdata.last }} + ({{ userdata.last_billable_hours_total }} / + {{ userdata.last_all_hours_total }}) + + {% else %} + -- + {% endif %} + | ++ {% if userdata.recent_all_hours_total %} + {{ userdata.recent }} + ({{ userdata.recent_billable_hours_total }} / + {{ userdata.recent_all_hours_total }}) + {% else %} + -- + {% endif %} + | ++ {% if userdata.fytd_all_hours_total %} + {{ userdata.fytd }} + ({{ userdata.fytd_billable_hours_total }} / + {{ userdata.fytd_all_hours_total }}) + {% else %} + -- + {% endif %} + | +
If you need access, please contact your supervisor.
- -{% endif %} - {% endblock %} diff --git a/tock/utilization/tests/test_views.py b/tock/utilization/tests/test_views.py index e4cdeeb10..7d420fab7 100644 --- a/tock/utilization/tests/test_views.py +++ b/tock/utilization/tests/test_views.py @@ -108,22 +108,22 @@ def test_utilization(self): ) self.assertEqual(len( - response.context['unit_choices']), len(UserData.UNIT_CHOICES) + response.context['object_list']), len(UserData.UNIT_CHOICES) ) self.assertContains(response, 'regular.user') self.assertContains(response, 'aaron.snow') self.assertTrue(response.context['through_date']) self.assertTrue(response.context['recent_start_date']) - self.assertEqual(len(response.context['user_list']), 2) - self.assertTrue(response.context['user_list'][0].__dict__['last']) - self.assertTrue(response.context['user_list'][0].__dict__['fytd']) - self.assertTrue(response.context['user_list'][0].__dict__['recent']) + self.assertEqual(len(response.context['object_list'][0]['billable_staff']), 2) + self.assertTrue(response.context['object_list'][0]['last']) + self.assertTrue(response.context['object_list'][0]['fytd']) + self.assertTrue(response.context['object_list'][0]['recent']) self.assertTrue( - response.context['user_list'][0].__dict__['_user_data_cache'] + response.context['object_list'][0]['billable_staff'][0]._user_cache ) self.assertEqual( - response.context['user_list'][0].__dict__['_user_data_cache'].\ - __dict__['unit'], 0) + response.context['object_list'][0]['billable_staff'][0].unit, 0 + ) def test_summary_rows(self): response = self.app.get( @@ -131,7 +131,7 @@ def test_summary_rows(self): user=self.req_user ) self.assertEqual( - response.context['unit_totals'][0]['recent']['total_hours'], + response.context['object_list'][0]['recent']['total_hours'], (self.b_timecard_object.hours_spent + \ self.nb_timecard_object.hours_spent) ) diff --git a/tock/utilization/views.py b/tock/utilization/views.py index 4117e3acd..0a9ec8cc0 100644 --- a/tock/utilization/views.py +++ b/tock/utilization/views.py @@ -1,7 +1,7 @@ -from django.db.models import Sum -from django.views.generic import ListView -from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse +from django.template.defaultfilters import slugify +from django.views.generic import ListView from hours.models import TimecardObject, ReportingPeriod from employees.models import UserData @@ -11,261 +11,241 @@ class GroupUtilizationView(PermissionMixin, ListView): template_name = 'utilization/group_utilization.html' - - def get_queryset(self): - """Gets submitted timecards limited to the reporting periods in - question.""" - - """Although recent_rps is set to last four reporting periods, could - accept a form response that allows the user or app to dynamically - customize number of periods to include in the queryset.""" - - available_periods = ReportingPeriod.objects.count() - requested_periods = 4 - - if available_periods >= requested_periods: - recent_rps = get_dates(requested_periods) + requested_periods = 4 + + def dispatch(self, *args, **kwargs): + """ + Resolve recent reporting periods. + + Although recent_rps is set to the last four reporting periods, + we could accept a form response that allows the user or app to + dynamically customize number of periods to include in the queryset. + + Also, if they're not staff, we're going to go ahead and bounce + them to 403 so we don't make all these queries. + """ + if not self.request.user.is_authenticated: + return self.handle_no_permission() + if not self.request.user.is_staff: + raise PermissionDenied + self.available_periods = ReportingPeriod.objects.count() + + if self.available_periods >= self.requested_periods: + self.recent_rps = get_dates(self.requested_periods) else: - recent_rps = get_dates(available_periods) - - billable_staff = User.objects.filter( - user_data__is_billable=True, - user_data__current_employee=True - ).prefetch_related('user_data') - - for staffer in billable_staff: - staffer.unit = staffer.user_data.unit + self.recent_rps = get_dates(self.available_periods) + return super().dispatch(*args, **kwargs) - """Create smallest possible TimecardObject queryset based on the - earliest_date value returned by get_dates(). Also prefetches the - related user and accounting code for later use.""" - max_tos = TimecardObject.objects.filter( - timecard__user=staffer, - submitted=True, - timecard__reporting_period__start_date__gte=recent_rps[3] - ).prefetch_related( - 'timecard__user', - 'project__accounting_code' - ) - - """Filters the max_tos queryset to only look for TimecardObjects - that are related to reporting periods within the current fiscal - year. This operation is unnecessary except at the beginning of the - fiscal year.""" - fytd_tos = max_tos.filter( - timecard__reporting_period__start_date__gte=recent_rps[2]) - - """Calcuates the billable hours decimal value in the queryset.""" - fytd_billable_hours = fytd_tos.filter( - project__accounting_code__billable=True - ).aggregate( - Sum('hours_spent' - ) - ) - - """Calcuates the all hours decimal value in the queryset.""" - fytd_all_hours = fytd_tos.aggregate( - Sum('hours_spent' + def get_queryset(self): + """ + Gets submitted timecards for billable staff + limited to the reporting periods in question. + """ + # Start stubbing a dict for our units, using a quick list comprehension + units = [{ + 'id': choice[0], + 'name': choice[1], + 'slug': slugify(choice[1]) + } for choice in UserData.UNIT_CHOICES] + # now we'll start building out that dict further, + # starting with the staff for each unit + for unit in units: + billable_staff = UserData.objects.filter( + is_billable=True, + current_employee=True, + unit = unit['id'] + ).prefetch_related('user') + + for staffer in billable_staff: + """ + Create smallest possible TimecardObject queryset based on the + earliest_date value returned by get_dates(). Also prefetches the + related user and accounting code for later use. + + We're casting this to values() because we need very little data, + it's faster, and we can work with it in pure python so we avoid + additional queries hitting the database. + """ + user_timecards = TimecardObject.objects.filter( + submitted=True, + timecard__user=staffer.user, + timecard__reporting_period__start_date__gte=self.recent_rps[3] + ).select_related( + 'timecard__reporting_period', + 'project__accounting_code__billable' + ).values( + 'id', + 'hours_spent', + 'timecard__reporting_period', + 'timecard__reporting_period__start_date', + 'project__accounting_code__billable' ) - ) - - """Filters the fytd_tos queryset to only look for TimecardObjects - that are related to reporting periods within the last n reporting - periods, where n is the argument passed to the get_dates() - function.""" - - recent_tos = max_tos.filter( - timecard__reporting_period__start_date__gte=recent_rps[1] - ) - - """Calcuates the billable hours decimal value in the queryset.""" - recent_billable_hours = recent_tos.filter( - project__accounting_code__billable=True - ).aggregate( - Sum('hours_spent' + """ + We also need to know the billable cards, but + we only need the IDs to boil down each reporting period QS + and find the intersection below. + """ + user_billable_timecard_ids = user_timecards.filter( + project__accounting_code__billable=True + ).values_list('id', flat=True) + + """ + Filter the timecard queryset to only look for cards that are + related to reporting periods within the current fiscal year. + + This operation is unnecessary except at the beginning of the + fiscal year. + """ + fytd_hours = [] + for card in user_timecards: + if card['timecard__reporting_period__start_date'] >= self.recent_rps[2]: + fytd_hours.append(card['hours_spent']) + fytd_hours = sum(fytd_hours) + + fytd_billable = [] + for card in user_timecards: + if card['timecard__reporting_period__start_date'] >= self.recent_rps[2] \ + and card['id'] in user_billable_timecard_ids: + fytd_billable.append(card['hours_spent']) + fytd_billable = sum(fytd_billable) + + staffer.fytd = calculate_utilization(fytd_billable, fytd_hours) + staffer.fytd_all_hours_total: fytd_hours + staffer.fytd_billable_hours = fytd_billable if fytd_billable else 0.0 + + """ + Get hours for reporting periods within the last n reporting + periods, where n is the argument passed to the get_dates() + function. + """ + recent_hours = [] + for card in user_timecards: + if card['timecard__reporting_period__start_date'] >= self.recent_rps[1]: + recent_hours.append(card['hours_spent']) + recent_hours = sum(recent_hours) + + recent_billable = [] + for card in user_timecards: + if card['timecard__reporting_period__start_date'] >= self.recent_rps[1] \ + and card['id'] in user_billable_timecard_ids: + recent_billable.append(card['hours_spent']) + recent_billable = sum(recent_billable) + + staffer.recent = calculate_utilization(recent_billable, recent_hours) + staffer.recent_all_hours_total = recent_hours + staffer.recent_billable_hours_total = recent_billable if recent_billable else 0.0 + """ + Get hours from the latest reporting period + """ + last_hours = [] + for card in user_timecards: + if card['timecard__reporting_period__start_date'] >= self.recent_rps[0]: + last_hours.append(card['hours_spent']) + last_hours = sum(last_hours) + + last_billable = [] + for card in user_timecards: + if card['timecard__reporting_period__start_date'] >= self.recent_rps[0] \ + and card['id'] in user_billable_timecard_ids: + last_billable.append(card['hours_spent']) + last_billable = sum(last_billable) + + staffer.last = calculate_utilization(last_billable, last_hours) + staffer.last_all_hours_total = last_hours + staffer.last_billable_hours_total = last_billable if last_billable else 0.0 + + staffer.last_url = reverse( + 'reports:ReportingPeriodUserDetailView', + kwargs={ + 'username':staffer.user, + 'reporting_period': self.recent_rps[4] + } ) - ) - """Calcuates the all hours decimal value in the queryset.""" - recent_all_hours = recent_tos.aggregate( - Sum('hours_spent' - ) - ) + unit['billable_staff'] = billable_staff - """Filters the recent_tos queryset to only look for TimecardObjects - that are related to reporting periods within the last 1 reporting - period. - """ - last_tos = recent_tos.filter( - timecard__reporting_period__end_date=recent_rps[0] + last_total_hours = sum(TimecardObject.objects.filter( + timecard__reporting_period__start_date=self.recent_rps[4], + submitted=True, + timecard__user__user_data__unit=unit['id'], + ).values_list('hours_spent', flat=True) ) - - """Calcuates the billable hours decimal value in the queryset.""" - last_billable_hours = last_tos.filter( + last_billable_hours = sum(TimecardObject.objects.filter( + submitted=True, + timecard__reporting_period__start_date=self.recent_rps[4], + timecard__user__user_data__unit=unit['id'], project__accounting_code__billable=True - ).aggregate( - Sum('hours_spent' - ) + ).values_list('hours_spent', flat=True) ) - - """Calcuates the all hours decimal value in the queryset.""" - last_all_hours = last_tos.aggregate( - Sum('hours_spent' - ) + # Query and calculate last in RP hours. + recent_total_hours = sum(TimecardObject.objects.filter( + submitted=True, + timecard__reporting_period__start_date__gte=self.recent_rps[1], + timecard__user__user_data__unit=unit['id'] + ).values_list('hours_spent', flat=True) ) - - - staffer.fytd = calculate_utilization( - fytd_billable_hours['hours_spent__sum'], - fytd_all_hours['hours_spent__sum'] + recent_billable_hours = sum( + TimecardObject.objects.filter( + submitted=True, + timecard__reporting_period__start_date__gte=self.recent_rps[1], + timecard__user__user_data__unit=unit['id'], + project__accounting_code__billable=True + ).values_list('hours_spent', flat=True) ) - staffer.fytd_all_hours_total = fytd_all_hours['hours_spent__sum'] - if fytd_billable_hours['hours_spent__sum']: - staffer.fytd_billable_hours_total = fytd_billable_hours['hours_spent__sum'] - else: - staffer.fytd_billable_hours_total = 0.0 - - staffer.recent = calculate_utilization( - recent_billable_hours['hours_spent__sum'], - recent_all_hours['hours_spent__sum'] + # Query and calculate all RP hours for FY to date. + fytd_total_hours = sum( + TimecardObject.objects.filter( + submitted=True, + timecard__reporting_period__start_date__gte=self.recent_rps[2], + timecard__user__user_data__unit=unit['id'], + ).values_list('hours_spent', flat=True) ) - staffer.recent_all_hours_total = recent_all_hours['hours_spent__sum'] - if recent_billable_hours['hours_spent__sum']: - staffer.recent_billable_hours_total = recent_billable_hours['hours_spent__sum'] - else: - staffer.recent_billable_hours_total = 0.0 - - - staffer.last = calculate_utilization( - last_billable_hours['hours_spent__sum'], - last_all_hours['hours_spent__sum'] + fytd_billable_hours = sum(TimecardObject.objects.filter( + submitted=True, + timecard__reporting_period__start_date__gte=self.recent_rps[2], + timecard__user__user_data__unit=unit['id'], + project__accounting_code__billable=True + ).values_list('hours_spent', flat=True) ) - staffer.last_all_hours_total = last_all_hours['hours_spent__sum'] - if last_billable_hours['hours_spent__sum']: - staffer.last_billable_hours_total = last_billable_hours['hours_spent__sum'] - else: - staffer.last_billable_hours_total = 0.0 - staffer.last_url = reverse( - 'reports:ReportingPeriodUserDetailView', - kwargs={ - 'username':staffer, - 'reporting_period': recent_rps[4] + unit.update({ + 'last': { + 'unit_name': unit['name'], + 'billable_hours': last_billable_hours, + 'total_hours': last_total_hours, + 'utilization': calculate_utilization( + last_billable_hours, + last_total_hours + ) + }, + 'recent': { + 'unit_name': unit['name'], + 'billable_hours': recent_billable_hours, + 'total_hours': recent_total_hours, + 'utilization': calculate_utilization( + recent_billable_hours, + recent_total_hours + ) + }, + 'fytd': { + 'unit_name': unit['name'], + 'billable_hours': fytd_billable_hours, + 'total_hours': fytd_total_hours, + 'utilization': calculate_utilization( + fytd_billable_hours, + fytd_total_hours + ) } - ) + }) - return billable_staff + return units def get_context_data(self, **kwargs): context = super(GroupUtilizationView, self).get_context_data(**kwargs) - - available_periods = ReportingPeriod.objects.count() - requested_periods = 4 - - if available_periods >= requested_periods: - recent_rps = get_dates(requested_periods) - else: - recent_rps = get_dates(available_periods) - - units = UserData.UNIT_CHOICES - unit_totals = [] - for unit in units: - # Query and calculate most recent RP hours. - last_total_hours = TimecardObject.objects.filter( - timecard__reporting_period__start_date=recent_rps[4], - submitted=True, - timecard__user__user_data__unit=unit[0], - ).aggregate( - Sum('hours_spent' - ) - )['hours_spent__sum'] - last_billable_hours = TimecardObject.objects.filter( - timecard__reporting_period__start_date=recent_rps[4], - submitted=True, - timecard__user__user_data__unit=unit[0], - project__accounting_code__billable=True - ).aggregate( - Sum('hours_spent' - ) - )['hours_spent__sum'] - # Query and calculate last n RP hours. - recent_total_hours = TimecardObject.objects.filter( - timecard__reporting_period__start_date__gte=recent_rps[1], - submitted=True, - timecard__user__user_data__unit=unit[0], - ).aggregate( - Sum('hours_spent' - ) - )['hours_spent__sum'] - recent_billable_hours = TimecardObject.objects.filter( - timecard__reporting_period__start_date__gte=recent_rps[1], - submitted=True, - timecard__user__user_data__unit=unit[0], - project__accounting_code__billable=True - ).aggregate( - Sum('hours_spent' - ) - )['hours_spent__sum'] - # Query and calculate all RP hours for FY to date. - fytd_total_hours = TimecardObject.objects.filter( - timecard__reporting_period__start_date__gte=recent_rps[2], - submitted=True, - timecard__user__user_data__unit=unit[0], - ).aggregate( - Sum('hours_spent' - ) - )['hours_spent__sum'] - fytd_billable_hours = TimecardObject.objects.filter( - timecard__reporting_period__start_date__gte=recent_rps[2], - submitted=True, - timecard__user__user_data__unit=unit[0], - project__accounting_code__billable=True - ).aggregate( - Sum('hours_spent' - ) - )['hours_spent__sum'] - - unit_totals.append( - {'last': - { - 'unit_name': unit[1], - 'billable_hours': last_billable_hours, - 'total_hours': last_total_hours, - 'utilization': calculate_utilization( - last_billable_hours, - last_total_hours - ) - } - , - 'recent': - { - 'unit_name': unit[1], - 'billable_hours': recent_billable_hours, - 'total_hours': recent_total_hours, - 'utilization': calculate_utilization( - recent_billable_hours, - recent_total_hours - ) - }, - 'fytd': - { - 'unit_name': unit[1], - 'billable_hours': fytd_billable_hours, - 'total_hours': fytd_total_hours, - 'utilization': calculate_utilization( - fytd_billable_hours, - fytd_total_hours - ) - } - } - ) context.update( { - 'unit_choices': UserData.UNIT_CHOICES, - 'through_date': recent_rps[0], - 'recent_start_date': recent_rps[1], - 'unit_totals':unit_totals + 'through_date': self.recent_rps[0], + 'recent_start_date': self.recent_rps[1], } ) - return context