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>
+
+<%def name="render_day(day)">
+ % for shift in day['shifts']:
+ ${render_shift(shift)}
+ % endfor
+%def>
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
%def>
${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>
+
+<%def name="context_menu()">
+ % if request.has_perm('schedule.viewall'):
+ ${h.link_to("View Schedule", url('schedule'))}
+ % endif
+%def>
+
+<%def name="render_day(day)">
+ % for shift in day['shifts']:
+
+ ${render_shift(shift)}
+
+ % endfor
+%def>
+
+${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)