From 048951153d64377e2c004cdbcc9d642891c92aea Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 12 Oct 2016 14:16:06 -0500 Subject: [PATCH] Add basic ability to edit employee schedule --- tailbone/templates/shifts/base.mako | 16 +- tailbone/templates/shifts/schedule.mako | 11 +- tailbone/templates/shifts/schedule_edit.mako | 254 +++++++++++++++++++ tailbone/views/shifts/core.py | 5 + tailbone/views/shifts/lib.py | 110 ++++---- tailbone/views/shifts/schedule.py | 80 +++++- 6 files changed, 419 insertions(+), 57 deletions(-) create mode 100644 tailbone/templates/shifts/schedule_edit.mako diff --git a/tailbone/templates/shifts/base.mako b/tailbone/templates/shifts/base.mako index c5427547..c00460d2 100644 --- a/tailbone/templates/shifts/base.mako +++ b/tailbone/templates/shifts/base.mako @@ -138,16 +138,14 @@ % for emp in sorted(employees, key=unicode): - + ${emp} % for day in emp.weekdays: - - % for shift in day['shifts']: -

${render_shift(shift)}

- % endfor + + ${self.render_day(day)} % endfor - ${emp.hours_display} + ${emp.hours_display} % endfor % if employee is UNDEFINED: @@ -171,3 +169,9 @@ + +<%def name="render_day(day)"> + % for shift in day['shifts']: +

${render_shift(shift)}

+ % endfor + diff --git a/tailbone/templates/shifts/schedule.mako b/tailbone/templates/shifts/schedule.mako index 420c2ceb..72c710ed 100644 --- a/tailbone/templates/shifts/schedule.mako +++ b/tailbone/templates/shifts/schedule.mako @@ -2,11 +2,12 @@ <%inherit file="/shifts/base.mako" /> <%def name="context_menu()"> - % if request.has_perm('timesheet.view'): -
  • ${h.link_to("View this Time Sheet", url('schedule.goto.timesheet'), class_='goto')}
  • - % endif -##
  • ${h.link_to("Print this Schedule", '#')}
  • -##
  • ${h.link_to("Edit this Schedule", '#')}
  • + % if request.has_perm('schedule.edit'): +
  • ${h.link_to("Edit Schedule", url('schedule.edit'))}
  • + % endif + % if request.has_perm('timesheet.view'): +
  • ${h.link_to("View this Time Sheet", url('schedule.goto.timesheet'), class_='goto')}
  • + % endif ${self.timesheet()} diff --git a/tailbone/templates/shifts/schedule_edit.mako b/tailbone/templates/shifts/schedule_edit.mako new file mode 100644 index 00000000..97e86f92 --- /dev/null +++ b/tailbone/templates/shifts/schedule_edit.mako @@ -0,0 +1,254 @@ +## -*- coding: utf-8 -*- +<%inherit file="/shifts/base.mako" /> + +<%def name="head_tags()"> + ${parent.head_tags()} + + + + +<%def name="context_menu()"> + % if request.has_perm('schedule.viewall'): +
  • ${h.link_to("View Schedule", url('schedule'))}
  • + % endif + + +<%def name="render_day(day)"> + % for shift in day['shifts']: +

    + ${render_shift(shift)} +

    + % endfor + + +${h.form(url('schedule.edit'), id="schedule-form")} +${self.timesheet()} +${h.end_form()} + +
    + + +
    + + + +
    +
    + ${h.text('edit_start_time')} thru ${h.text('edit_end_time')} + +
    +
    diff --git a/tailbone/views/shifts/core.py b/tailbone/views/shifts/core.py index 2023e871..4335c42e 100644 --- a/tailbone/views/shifts/core.py +++ b/tailbone/views/shifts/core.py @@ -59,6 +59,11 @@ class ScheduledShiftsView(MasterView): url_prefix = '/shifts/scheduled' def configure_grid(self, g): + g.joiners['employee'] = lambda q: q.join(model.Employee).join(model.Person) + g.filters['employee'] = g.make_filter('employee', model.Person.display_name, + default_active=True, default_verb='contains', + label="Employee Name") + g.default_sortkey = 'start_time' g.default_sortdir = 'desc' g.append(ShiftLengthField('length')) diff --git a/tailbone/views/shifts/lib.py b/tailbone/views/shifts/lib.py index 3cb8b0ef..c6e07362 100644 --- a/tailbone/views/shifts/lib.py +++ b/tailbone/views/shifts/lib.py @@ -34,6 +34,7 @@ from rattail.time import localtime, make_utc, get_sunday import formencode as fe from pyramid_simpleform import Form +from webhelpers.html import HTML from tailbone import forms from tailbone.db import Session @@ -71,14 +72,60 @@ class TimeSheetView(View): def get_title(cls): return cls.title or cls.key.capitalize() - def full(self): + def get_timesheet_context(self): + """ + Determine date/store/dept context from user's session and/or defaults. + """ date = None + date_key = 'timesheet.{}.date'.format(self.key) + if date_key in self.request.session: + date_value = self.request.session.get(date_key) + if date_value: + try: + date = datetime.datetime.strptime(date_value, '%m/%d/%Y').date() + except ValueError: + pass + if not date: + date = localtime(self.rattail_config).date() + store = None department = None + store_key = 'timesheet.{}.store'.format(self.key) + department_key = 'timesheet.{}.department'.format(self.key) + if store_key in self.request.session or department_key in self.request.session: + store_uuid = self.request.session.get(store_key) + if store_uuid: + store = Session.query(model.Store).get(store_uuid) if store_uuid else None + department_uuid = self.request.session.get(department_key) + if department_uuid: + department = Session.query(model.Department).get(department_uuid) + else: # no store/department in session + if self.default_filter_store: + store = self.rattail_config.get('rattail', 'store') + if store: + store = api.get_store(Session(), store) + employees = Session.query(model.Employee)\ .filter(model.Employee.status == enum.EMPLOYEE_STATUS_CURRENT) + if store: + employees = employees.join(model.EmployeeStore)\ + .filter(model.EmployeeStore.store == store) + if department: + employees = employees.join(model.EmployeeDepartment)\ + .filter(model.EmployeeDepartment.department == department) - form = Form(self.request, schema=ShiftFilter) + return { + 'date': date, + 'store': store, + 'department': department, + 'employees': employees.all(), + } + + def process_filter_form(self, form): + """ + Process a "shift filter" form if one was in fact POST'ed. If it was + then we store new context in session and redirect to display as normal. + """ if self.request.method == 'POST': if form.validate(): store = form.data['store'] @@ -87,45 +134,18 @@ class TimeSheetView(View): self.request.session['timesheet.{}.department'.format(self.key)] = department.uuid if department else None date = form.data['date'] self.request.session['timesheet.{}.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None - return self.redirect(self.request.current_route_url()) + raise self.redirect(self.request.current_route_url()) - else: - store_key = 'timesheet.{}.store'.format(self.key) - department_key = 'timesheet.{}.department'.format(self.key) - if store_key in self.request.session or department_key in self.request.session: - store_uuid = self.request.session.get(store_key) - if store_uuid: - store = Session.query(model.Store).get(store_uuid) if store_uuid else None - department_uuid = self.request.session.get(department_key) - if department_uuid: - department = Session.query(model.Department).get(department_uuid) - else: # no store/department in session - if self.default_filter_store: - store = self.rattail_config.get('rattail', 'store') - if store: - store = api.get_store(Session(), store) - - date_key = 'timesheet.{}.date'.format(self.key) - if date_key in self.request.session: - date_value = self.request.session.get(date_key) - if date_value: - try: - date = datetime.datetime.strptime(date_value, '%m/%d/%Y').date() - except ValueError: - pass - - if store: - employees = employees.join(model.EmployeeStore)\ - .filter(model.EmployeeStore.store == store) - - if department: - employees = employees.join(model.EmployeeDepartment)\ - .filter(model.EmployeeDepartment.department == department) - - if not date: - date = localtime(self.rattail_config).date() - - return self.render_full(date, employees.all(), store=store, department=department, form=form) + def full(self): + """ + View a "full" timesheet/schedule, i.e. all employees but filterable by + store and/or department. + """ + form = Form(self.request, schema=ShiftFilter) + self.process_filter_form(form) + context = self.get_timesheet_context() + context['form'] = form + return self.render_full(**context) def employee(self): """ @@ -233,7 +253,7 @@ class TimeSheetView(View): options.insert(0, ('', "(all)")) return options - def render_full(self, date, employees, store=None, department=None, form=None): + def render_full(self, date=None, employees=None, store=None, department=None, form=None, **kwargs): """ Render a time sheet for one or more employees, for the week which includes the specified date. @@ -257,7 +277,7 @@ class TimeSheetView(View): departments = self.get_departments() department_options = self.get_department_options(departments) - return { + context = { 'page_title': "Full {}".format(self.get_title()), 'form': forms.FormRenderer(form) if form else None, 'employees': employees, @@ -275,9 +295,11 @@ class TimeSheetView(View): 'permission_prefix': self.key, 'render_shift': self.render_shift, } + context.update(kwargs) + return context def render_shift(self, shift): - return shift.get_display(self.rattail_config) + return HTML.tag('span', c=shift.get_display(self.rattail_config)) def render_single(self, date, employee, form=None): """ @@ -371,7 +393,7 @@ class TimeSheetView(View): """ title = cls.get_title() config.add_tailbone_permission_group(cls.key, title) - config.add_tailbone_permission(cls.key, '{}.view'.format(cls.key), "View employee {}".format(title)) + config.add_tailbone_permission(cls.key, '{}.view'.format(cls.key), "View single employee {}".format(title)) config.add_tailbone_permission(cls.key, '{}.viewall'.format(cls.key), "View full {}".format(title)) # full time sheet diff --git a/tailbone/views/shifts/schedule.py b/tailbone/views/shifts/schedule.py index 6229fbd2..21952fbc 100644 --- a/tailbone/views/shifts/schedule.py +++ b/tailbone/views/shifts/schedule.py @@ -26,9 +26,15 @@ Views for employee schedules from __future__ import unicode_literals, absolute_import -from rattail.db import model +import datetime -from tailbone.views.shifts.lib import TimeSheetView +from rattail.db import model +from rattail.time import localtime, make_utc + +from pyramid_simpleform import Form + +from tailbone.db import Session +from tailbone.views.shifts.lib import TimeSheetView, ShiftFilter class ScheduleView(TimeSheetView): @@ -38,6 +44,76 @@ class ScheduleView(TimeSheetView): key = 'schedule' model_class = model.ScheduledShift + def edit(self): + """ + View for editing (full) schedule. + """ + if self.request.method == 'POST': + + # organize form data by uuid / field + fields = ['employee_uuid', 'store_uuid', 'start_time', 'end_time', 'delete'] + data = dict([(f, {}) for f in fields]) + for key in self.request.POST: + for field in fields: + if key.startswith('{}-'.format(field)): + uuid = key[len('{}-'.format(field)):] + if uuid: + data[field][uuid] = self.request.POST[key] + + # apply delete operations + deleted = [] + for uuid, value in data['delete'].iteritems(): + assert value == 'delete' + shift = Session.query(model.ScheduledShift).get(uuid) + assert shift + Session.delete(shift) + deleted.append(uuid) + + # apply create / update operations + created = {} + updated = {} + time_format = '%a %d %b %Y %I:%M %p' + for uuid, employee_uuid in data['start_time'].iteritems(): + if uuid in deleted: + continue + if uuid.startswith('new-'): + shift = model.ScheduledShift() + shift.employee_uuid = data['employee_uuid'][uuid] + shift.store_uuid = data['store_uuid'][uuid] + Session.add(shift) + created[uuid] = shift + else: + shift = Session.query(model.ScheduledShift).get(uuid) + assert shift + updated[uuid] = shift + start_time = datetime.datetime.strptime(data['start_time'][uuid], time_format) + shift.start_time = make_utc(localtime(self.rattail_config, start_time)) + end_time = datetime.datetime.strptime(data['end_time'][uuid], time_format) + shift.end_time = make_utc(localtime(self.rattail_config, end_time)) + + self.request.session.flash("Changes were applied: created {}, updated {}, " + "deleted {} Scheduled Shifts".format( + len(created), len(updated), len(deleted))) + return self.redirect(self.request.route_url('schedule.edit')) + + form = Form(self.request, schema=ShiftFilter) + self.process_filter_form(form) + context = self.get_timesheet_context() + context['form'] = form + context['page_title'] = "Edit Schedule" + return self.render_full(**context) + + @classmethod + def defaults(cls, config): + cls._defaults(config) + + # edit schedule + config.add_route('schedule.edit', '/schedule/edit') + config.add_view(cls, attr='edit', route_name='schedule.edit', + renderer='/shifts/schedule_edit.mako', + permission='schedule.edit') + config.add_tailbone_permission('schedule', 'schedule.edit', "Edit full schedule") + def includeme(config): ScheduleView.defaults(config)