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 %} - -

Forbidden

-

You don't have access to view this page.

- +

You don't have permission to view this information.

+

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 %} +

Utilization by Unit

+

Notes:

+

+ 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. +

+

Jump to:

+ + -

Utilization by Unit

-{% if request.user.is_staff %} -

Notes:

-

- 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. -

-

Jump to:

- + {% for unit in object_list %} + + + + + + + + + + + + + + + + + + -{% for i in unit_choices %} - -
+

{{unit.name}}

+
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 }}) +
+
- - - - - - - - - - - - - - - - - - - {% for userdata in object_list %} - - {% if userdata.unit is i.0 %} - - - - - - - {% endif %} + + {% for userdata in unit.billable_staff %} + + + + + + + {% endfor %} + +
-

{{i.1}}

-
NameLast 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 %} +
{% endfor %} - - - -{% endfor %} - -{% else %} - -

This page is viewable by Tock users with an `is_staff` status of True.

-

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