text: from copy import deepcopy
  import datetime
  from dateutil.relativedelta import relativedelta
  from decimal import Decimal
  from itertools import groupby
  import json
  import urllib
  
  from django.contrib import messages
  from django.contrib.auth.decorators import login_required, permission_required
  from django.contrib.auth.models import User, Permission
  from django.contrib.contenttypes.models import ContentType
  from django.core import exceptions
  from django.core.urlresolvers import reverse
  from django.db import transaction
  from django.db.models import Q
  from django.http import HttpResponse, HttpResponseRedirect, Http404
  from django.shortcuts import redirect, render
  from django.utils.decorators import method_decorator
  from django.views.generic import TemplateView, View
  
  from timepiece import utils
  from timepiece.forms import DATE_FORM_FORMAT
  from timepiece.utils.csv import DecimalEncoder
  
  from timepiece.crm.models import Project, UserProfile
  from timepiece.entries.forms import ClockInForm, ClockOutForm, \
          AddUpdateEntryForm, ProjectHoursForm, ProjectHoursSearchForm
  from timepiece.entries.models import Entry, ProjectHours
  
  
  class Dashboard(TemplateView):
      template_name = 'timepiece/dashboard.html'
  
      @method_decorator(login_required)
      def dispatch(self, request, active_tab, *args, **kwargs):
          self.active_tab = active_tab or 'progress'
          self.user = request.user
          return super(Dashboard, self).dispatch(request, *args, **kwargs)
  
      def get_dates(self):
          today = datetime.date.today()
          day = today
          if 'week_start' in self.request.GET:
              param = self.request.GET.get('week_start')
              try:
                  day = datetime.datetime.strptime(param, '%Y-%m-%d').date()
              except:
                  pass
          week_start = utils.get_week_start(day)
          week_end = week_start + relativedelta(days=6)
          return today, week_start, week_end
  
      def get_hours_per_week(self, user=None):
          """Retrieves the number of hours the user should work per week."""
          try:
              profile = UserProfile.objects.get(user=user or self.user)
          except UserProfile.DoesNotExist:
              profile = None
          return profile.hours_per_week if profile else Decimal('40.00')
  
      def get_context_data(self, *args, **kwargs):
          today, week_start, week_end = self.get_dates()
  
          # Query for the user's active entry if it exists.
          active_entry = utils.get_active_entry(self.user)
  
          # Process this week's entries to determine assignment progress.
          week_entries = Entry.objects.filter(user=self.user) \
                  .timespan(week_start, span='week', current=True) \
                  .select_related('project')
          assignments = ProjectHours.objects.filter(user=self.user,
                  week_start=week_start.date())
          project_progress = self.process_progress(week_entries, assignments)
  
          # Total hours that the user is expected to clock this week.
          total_assigned = self.get_hours_per_week(self.user)
          total_worked = sum([p['worked'] for p in project_progress])
  
          # Others' active entries.
          others_active_entries = Entry.objects.filter(end_time__isnull=True) \
                  .exclude(user=self.user).select_related('user', 'project',
                  'activity')
  
          return {
              'active_tab': self.active_tab,
              'today': today,
              'week_start': week_start.date(),
              'week_end': week_end.date(),
              'active_entry': active_entry,
              'total_assigned': total_assigned,
              'total_worked': total_worked,
              'project_progress': project_progress,
              'week_entries': week_entries,
              'others_active_entries': others_active_entries,
          }
  
      def process_progress(self, entries, assignments):
          """
          Returns a list of progress summary data (pk, name, hours worked, and
          hours assigned) for each project either worked or assigned.
          The list is ordered by project name.
          """
          # Determine all projects either worked or assigned.
          project_q = Q(id__in=assignments.values_list('project__id', flat=True))
          project_q |= Q(id__in=entries.values_list('project__id', flat=True))
          projects = Project.objects.filter(project_q).select_related('business')
  
          # Hours per project.
          project_data = {}
          for project in projects:
              try:
                  assigned = assignments.get(project__id=project.pk).hours
              except ProjectHours.DoesNotExist:
                  assigned = Decimal('0.00')
              project_data[project.pk] = {
                  'project': project,
                  'assigned': assigned,
                  'worked': Decimal('0.00'),
              }
  
          for entry in entries:
              pk = entry.project_id
              hours = Decimal('%.2f' % (entry.get_total_seconds() / 3600.0))
              project_data[pk]['worked'] += hours
  
          # Sort by maximum of worked or assigned hours (highest first).
          key = lambda x: x['project'].name.lower()
          project_progress = sorted(project_data.values(), key=key)
  
          return project_progress
  
  
  @permission_required('entries.can_clock_in')
  @transaction.commit_on_success
  def clock_in(request):
      """For clocking the user into a project."""
      user = request.user
      # Lock the active entry for the duration of this transaction, to prevent
      # creating multiple active entries.
      active_entry = utils.get_active_entry(user, select_for_update=True)
  
      initial = dict([(k, v) for k, v in request.GET.items()])
      data = request.POST or None
      form = ClockInForm(data, initial=initial, user=user, active=active_entry)
      if form.is_valid():
          entry = form.save()
          message = 'You have clocked into {0} on {1}.'.format(
                  entry.activity.name, entry.project)
          messages.info(request, message)
          return HttpResponseRedirect(reverse('dashboard'))
  
      return render(request, 'timepiece/entry/clock_in.html', {
          'form': form,
          'active': active_entry,
      })
  
  
  @permission_required('entries.can_clock_out')
  def clock_out(request):
      entry = utils.get_active_entry(request.user)
      if not entry:
          message = "Not clocked in"
          messages.info(request, message)
          return HttpResponseRedirect(reverse('dashboard'))
      if request.POST:
          form = ClockOutForm(request.POST, instance=entry)
          if form.is_valid():
              entry = form.save()
              message = 'You have clocked out of {0} on {1}.'.format(
                      entry.activity.name, entry.project)
              messages.info(request, message)
              return HttpResponseRedirect(reverse('dashboard'))
          else:
              message = 'Please correct the errors below.'
              messages.error(request, message)
      else:
          form = ClockOutForm(instance=entry)
      return render(request, 'timepiece/entry/clock_out.html', {
          'form': form,
          'entry': entry,
      })
  
  
  @permission_required('entries.can_pause')
  def toggle_pause(request):
      """Allow the user to pause and unpause the active entry."""
      entry = utils.get_active_entry(request.user)
      if not entry:
          raise Http404
  
      # toggle the paused state
      entry.toggle_paused()
      entry.save()
  
      # create a message that can be displayed to the user
      action = 'paused' if entry.is_paused else 'resumed'
      message = 'Your entry, {0} on {1}, has been {2}.'.format(
              entry.activity.name, entry.project, action)
      messages.info(request, message)
  
      # redirect to the log entry list
      return HttpResponseRedirect(reverse('dashboard'))
  
  
  @permission_required('entries.change_entry')
  def create_edit_entry(request, entry_id=None):
      if entry_id:
          try:
              entry = Entry.no_join.get(pk=entry_id)
          except Entry.DoesNotExist:
              entry = None
          if not entry or not (entry.is_editable or
                  request.user.has_perm('entries.view_payroll_summary')):
              raise Http404
      else:
          entry = None
  
      entry_user = entry.user if entry else request.user
      if request.method == 'POST':
          form = AddUpdateEntryForm(data=request.POST, instance=entry,
                  user=entry_user)
          if form.is_valid():
              entry = form.save()
              if entry_id:
                  message = 'The entry has been updated successfully.'
              else:
                  message = 'The entry has been created successfully.'
              messages.info(request, message)
              url = request.REQUEST.get('next', reverse('dashboard'))
              return HttpResponseRedirect(url)
          else:
              message = 'Please fix the errors below.'
              messages.error(request, message)
      else:
          initial = dict([(k, request.GET[k]) for k in request.GET.keys()])
          form = AddUpdateEntryForm(instance=entry, user=entry_user,
                  initial=initial)
  
      return render(request, 'timepiece/entry/create_edit.html', {
          'form': form,
          'entry': entry,
      })
  
  
  @permission_required('entries.view_payroll_summary')
  def reject_entry(request, entry_id):
      """
      Admins can reject an entry that has been verified or approved but not
      invoiced to set its status to 'unverified' for the user to fix.
      """
      return_url = request.REQUEST.get('next', reverse('dashboard'))
      try:
          entry = Entry.no_join.get(pk=entry_id)
      except:
          message = 'No such log entry.'
          messages.error(request, message)
          return redirect(return_url)
  
      if entry.status == Entry.UNVERIFIED or entry.status == Entry.INVOICED:
          msg_text = 'This entry is unverified or is already invoiced.'
          messages.error(request, msg_text)
          return redirect(return_url)
  
      if request.POST.get('Yes'):
          entry.status = Entry.UNVERIFIED
          entry.save()
          msg_text = 'The entry\'s status was set to unverified.'
          messages.info(request, msg_text)
          return redirect(return_url)
      return render(request, 'timepiece/entry/reject.html', {
          'entry': entry,
          'next': request.REQUEST.get('next'),
      })
  
  
  @permission_required('entries.delete_entry')
  def delete_entry(request, entry_id):
      """
      Give the user the ability to delete a log entry, with a confirmation
      beforehand.  If this method is invoked via a GET request, a form asking
      for a confirmation of intent will be presented to the user. If this method
      is invoked via a POST request, the entry will be deleted.
      """
      try:
          entry = Entry.no_join.get(pk=entry_id, user=request.user)
      except Entry.DoesNotExist:
          message = 'No such entry found.'
          messages.info(request, message)
          url = request.REQUEST.get('next', reverse('dashboard'))
          return HttpResponseRedirect(url)
  
      if request.method == 'POST':
          key = request.POST.get('key', None)
          if key and key == entry.delete_key:
              entry.delete()
              message = 'Deleted {0} for {1}.'.format(entry.activity.name,
                      entry.project)
              messages.info(request, message)
              url = request.REQUEST.get('next', reverse('dashboard'))
              return HttpResponseRedirect(url)
          else:
              message = 'You are not authorized to delete this entry!'
              messages.error(request, message)
  
      return render(request, 'timepiece/entry/delete.html', {
          'entry': entry,
      })
  
  
  class ScheduleMixin(object):
  
      def dispatch(self, request, *args, **kwargs):
          # Since we use get param in multiple places, attach it to the class
          default_week = utils.get_week_start(datetime.date.today()).date()
  
          if request.method == 'GET':
              week_start_str = request.GET.get('week_start', '')
          else:
              week_start_str = request.POST.get('week_start', '')
  
          # Account for an empty string
          self.week_start = default_week if week_start_str == '' \
              else utils.get_week_start(datetime.datetime.strptime(
                  week_start_str, '%Y-%m-%d').date())
  
          return super(ScheduleMixin, self).dispatch(request, *args,
                  **kwargs)
  
      def get_hours_for_week(self, week_start=None):
          """
          Gets all ProjectHours entries in the 7-day period beginning on
          week_start.
          """
          week_start = week_start if week_start else self.week_start
          week_end = week_start + relativedelta(days=7)
  
          return ProjectHours.objects.filter(
              week_start__gte=week_start, week_start__lt=week_end)
  
  
  class ScheduleView(ScheduleMixin, TemplateView):
      template_name = 'timepiece/schedule/view.html'
  
      def dispatch(self, request, *args, **kwargs):
          if not request.user.has_perm('entries.can_clock_in'):
              return HttpResponseRedirect(reverse('auth_login'))
  
          return super(ScheduleView, self).dispatch(request, *args, **kwargs)
  
      def get_users_from_project_hours(self, project_hours):
          """
          Gets a list of the distinct users included in the project hours
          entries, ordered by name.
          """
          name = ('user__first_name', 'user__last_name')
          users = project_hours.values_list('user__id', *name).distinct()\
                               .order_by(*name)
          return users
  
      def get_context_data(self, **kwargs):
          context = super(ScheduleView, self).get_context_data(**kwargs)
  
          initial = {'week_start': self.week_start}
          form = ProjectHoursSearchForm(initial=initial)
  
          project_hours = self.get_hours_for_week()
          project_hours = project_hours.values('project__id', 'project__name',
                  'user__id', 'user__first_name', 'user__last_name', 'hours',
                  'published')
          project_hours = project_hours.order_by('-project__type__billable',
                  'project__name')
          if not self.request.user.has_perm('entries.add_projecthours'):
              project_hours = project_hours.filter(published=True)
          users = self.get_users_from_project_hours(project_hours)
          id_list = [user[0] for user in users]
          projects = []
  
          func = lambda o: o['project__id']
          for project, entries in groupby(project_hours, func):
              entries = list(entries)
              proj_id = entries[0]['project__id']
              name = entries[0]['project__name']
              row = [{} for i in range(len(id_list))]
              for entry in entries:
                  index = id_list.index(entry['user__id'])
                  hours = entry['hours']
                  row[index]['hours'] = row[index].get('hours', 0) + hours
                  row[index]['published'] = entry['published']
              projects.append((proj_id, name, row))
  
          context.update({
              'form': form,
              'week': self.week_start,
              'prev_week': self.week_start - relativedelta(days=7),
              'next_week': self.week_start + relativedelta(days=7),
              'users': users,
              'project_hours': project_hours,
              'projects': projects
          })
          return context
  
  
  class EditScheduleView(ScheduleMixin, TemplateView):
      template_name = 'timepiece/schedule/edit.html'
  
      def dispatch(self, request, *args, **kwargs):
          if not request.user.has_perm('entries.add_projecthours'):
              return HttpResponseRedirect(reverse('view_schedule'))
  
          return super(EditScheduleView, self).dispatch(request, *args,
                  **kwargs)
  
      def get_context_data(self, **kwargs):
          context = super(EditScheduleView, self).get_context_data(**kwargs)
  
          form = ProjectHoursSearchForm(initial={
              'week_start': self.week_start
          })
  
          context.update({
              'form': form,
              'week': self.week_start,
              'ajax_url': reverse('ajax_schedule')
          })
          return context
  
      def post(self, request, *args, **kwargs):
          ph = self.get_hours_for_week(self.week_start).filter(published=False)
  
          if ph.exists():
              ph.update(published=True)
              msg = 'Unpublished project hours are now published'
          else:
              msg = 'There were no hours to publish'
  
          messages.info(request, msg)
  
          param = {
              'week_start': self.week_start.strftime(DATE_FORM_FORMAT)
          }
          url = '?'.join((reverse('edit_schedule'), urllib.urlencode(param),))
  
          return HttpResponseRedirect(url)
  
  
  class ScheduleAjaxView(ScheduleMixin, View):
      permissions = ('entries.add_projecthours',)
  
      def dispatch(self, request, *args, **kwargs):
          if not request.user.has_perm('entries.add_projecthours'):
              return HttpResponseRedirect(reverse('auth_login'))
  
          return super(ScheduleAjaxView, self).dispatch(request, *args,
                  **kwargs)
  
      def get_instance(self, data, week_start):
          try:
              user = User.objects.get(pk=data.get('user', None))
              project = Project.objects.get(pk=data.get('project', None))
              hours = data.get('hours', None)
              week = datetime.datetime.strptime(week_start,
                      DATE_FORM_FORMAT).date()
  
              ph = ProjectHours.objects.get(user=user, project=project,
                      week_start=week)
              ph.hours = Decimal(hours)
          except (exceptions.ObjectDoesNotExist):
              ph = None
  
          return ph
  
      def get(self, request, *args, **kwargs):
          """
          Returns the data as a JSON object made up of the following key/value
          pairs:
              project_hours: the current project hours for the week
              projects: the projects that have hours for the week
              all_projects: all of the projects; used for autocomplete
              all_users: all users that can clock in; used for completion
          """
          perm = Permission.objects.filter(
              content_type=ContentType.objects.get_for_model(Entry),
              codename='can_clock_in'
          )
          project_hours = self.get_hours_for_week().values(
              'id', 'user', 'user__first_name', 'user__last_name',
              'project', 'hours', 'published'
          ).order_by('-project__type__billable', 'project__name',
              'user__first_name', 'user__last_name')
          inner_qs = project_hours.values_list('project', flat=True)
          projects = Project.objects.filter(pk__in=inner_qs).values() \
              .order_by('name')
          all_projects = Project.objects.values('id', 'name')
          user_q = Q(groups__permissions=perm) | Q(user_permissions=perm)
          user_q |= Q(is_superuser=True)
          all_users = User.objects.filter(user_q) \
              .values('id', 'first_name', 'last_name')
  
          data = {
              'project_hours': list(project_hours),
              'projects': list(projects),
              'all_projects': list(all_projects),
              'all_users': list(all_users),
              'ajax_url': reverse('ajax_schedule'),
          }
          return HttpResponse(json.dumps(data, cls=DecimalEncoder),
              mimetype='application/json')
  
      def duplicate_entries(self, duplicate, week_update):
          def duplicate_builder(queryset, new_date):
              for instance in queryset:
                  duplicate = deepcopy(instance)
                  duplicate.id = None
                  duplicate.published = False
                  duplicate.week_start = new_date
                  yield duplicate
  
          def duplicate_helper(queryset, new_date):
              try:
                  ProjectHours.objects.bulk_create(
                      duplicate_builder(queryset, new_date)
                  )
              except AttributeError:
                  for entry in duplicate_builder(queryset, new_date):
                      entry.save()
              msg = 'Project hours were copied'
              messages.info(self.request, msg)
  
          this_week = datetime.datetime.strptime(week_update,
                  DATE_FORM_FORMAT).date()
          prev_week = this_week - relativedelta(days=7)
          prev_week_qs = self.get_hours_for_week(prev_week)
          this_week_qs = self.get_hours_for_week(this_week)
  
          param = {
              'week_start': week_update
          }
          url = '?'.join((reverse('edit_schedule'),
              urllib.urlencode(param),))
  
          if not prev_week_qs.exists():
              msg = 'There are no hours to copy'
              messages.warning(self.request, msg)
          else:
              this_week_qs.delete()
              duplicate_helper(prev_week_qs, this_week)
          return HttpResponseRedirect(url)
  
      def update_week(self, week_start):
          try:
              instance = self.get_instance(self.request.POST, week_start)
          except TypeError:
              msg = 'Parameter week_start must be a date in the format ' \
                  'yyyy-mm-dd'
              return HttpResponse(msg, status=500)
  
          form = ProjectHoursForm(self.request.POST, instance=instance)
  
          if form.is_valid():
              ph = form.save()
              return HttpResponse(str(ph.pk), mimetype='text/plain')
  
          msg = 'The request must contain values for user, project, and hours'
          return HttpResponse(msg, status=500)
  
      def post(self, request, *args, **kwargs):
          """
          Create or update an hour entry for a particular use and project. This
          function expects the following values:
              user: the user pk for the hours
              project: the project pk for the hours
              hours: the actual hours to store
              week_start: the start of the week for the hours
  
          If the duplicate key is present along with week_update, then items
          will be duplicated from week_update to the current week
          """
          duplicate = request.POST.get('duplicate', None)
          week_update = request.POST.get('week_update', None)
          week_start = request.POST.get('week_start', None)
  
          if duplicate and week_update:
              return self.duplicate_entries(duplicate, week_update)
  
          return self.update_week(week_start)
  
  
  class ScheduleDetailView(ScheduleMixin, View):
      permissions = ('entries.add_projecthours',)
  
      def delete(self, request, *args, **kwargs):
          """Remove a project from the database."""
          assignment_id = kwargs.get('assignment_id', None)
  
          if assignment_id:
              ProjectHours.objects.filter(pk=assignment_id).delete()
              return HttpResponse('ok', mimetype='text/plain')
  
          return HttpResponse('', status=500)
  