diff --git a/tailbone/forms2/core.py b/tailbone/forms2/core.py
index 2f53572c..d429fda0 100644
--- a/tailbone/forms2/core.py
+++ b/tailbone/forms2/core.py
@@ -502,6 +502,8 @@ class Form(object):
if key in schema:
schema[key].widget = widget
+ # TODO: we are now doing this when making deform.Form, in which
+ # case, do we still need to do it here?
# apply any default values
for key, default in self.defaults.items():
if key in schema:
@@ -656,6 +658,13 @@ class Form(object):
schema = self.make_schema()
+ # TODO: we are still also doing this when making the schema, but
+ # seems like this should be the right place instead?
+ # apply any default values
+ for key, default in self.defaults.items():
+ if key in schema:
+ schema[key].default = default
+
# get initial form values from model instance
kwargs = {}
if self.model_instance:
diff --git a/tailbone/forms2/types.py b/tailbone/forms2/types.py
index e9fcdb59..d45b957e 100644
--- a/tailbone/forms2/types.py
+++ b/tailbone/forms2/types.py
@@ -26,6 +26,8 @@ Form Schema Types
from __future__ import unicode_literals, absolute_import
+import six
+
from rattail.db import model
import colander
@@ -58,20 +60,28 @@ class JQueryTime(colander.Time):
return colander.timeparse(cstruct, formats[0])
-class ObjectType(colander.SchemaType):
+class ModelType(colander.SchemaType):
"""
Custom schema type for scalar ORM relationship fields.
"""
model_class = None
+ session = None
+
+ def __init__(self, model_class=None, session=None):
+ if model_class:
+ self.model_class = model_class
+ if session:
+ self.session = session
+ else:
+ self.session = self.make_session()
+
+ def make_session(self):
+ return Session()
@property
def model_title(self):
self.model_class.get_model_title()
- @property
- def session(self):
- return Session()
-
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
@@ -86,28 +96,46 @@ class ObjectType(colander.SchemaType):
return obj
-class StoreType(ObjectType):
+# TODO: deprecate / remove this
+ObjectType = ModelType
+
+
+class StoreType(ModelType):
"""
Custom schema type for store field.
"""
model_class = model.Store
-class CustomerType(ObjectType):
+class CustomerType(ModelType):
"""
Custom schema type for customer field.
"""
model_class = model.Customer
-class ProductType(ObjectType):
+class DepartmentType(ModelType):
+ """
+ Custom schema type for department field.
+ """
+ model_class = model.Department
+
+
+class EmployeeType(ModelType):
+ """
+ Custom schema type for employee field.
+ """
+ model_class = model.Employee
+
+
+class ProductType(ModelType):
"""
Custom schema type for product relationship field.
"""
model_class = model.Product
-class UserType(ObjectType):
+class UserType(ModelType):
"""
Custom schema type for user field.
"""
diff --git a/tailbone/forms2/widgets.py b/tailbone/forms2/widgets.py
index d99853b7..666d9f41 100644
--- a/tailbone/forms2/widgets.py
+++ b/tailbone/forms2/widgets.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2018 Lance Edgar
#
# This file is part of Rattail.
#
@@ -44,7 +44,9 @@ class ReadonlyWidget(dfwidget.HiddenWidget):
if cstruct in (colander.null, None):
cstruct = ''
# TODO: is this hacky?
- text = field.parent.tailbone_form.render_field_value(field.name)
+ text = kw.get('text')
+ if not text:
+ text = field.parent.tailbone_form.render_field_value(field.name)
return HTML.tag('span', text) + tags.hidden(field.name, value=cstruct, id=field.oid)
@@ -66,7 +68,9 @@ class JQueryDateWidget(dfwidget.DateInputWidget):
requirements = None
default_options = (
+ ('changeMonth', True),
('changeYear', True),
+ ('dateFormat', 'yy-mm-dd'),
)
def serialize(self, field, cstruct, **kw):
@@ -76,8 +80,8 @@ class JQueryDateWidget(dfwidget.DateInputWidget):
template = readonly and self.readonly_template or self.template
options = dict(
kw.get('options') or self.options or self.default_options
- )
- options['dateFormat'] = 'yy-mm-dd'
+ )
+ options.update(kw.get('extra_options', {}))
kw.setdefault('options_json', json.dumps(options))
values = self.get_template_values(field, cstruct, kw)
return field.renderer(template, **values)
@@ -132,7 +136,7 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
kw['options'] = json.dumps(options)
kw['field_display'] = self.field_display
kw['cleared_callback'] = self.cleared_callback
- kw['selected_callback'] = self.selected_callback
+ kw.setdefault('selected_callback', self.selected_callback)
tmpl_values = self.get_template_values(field, cstruct, kw)
template = readonly and self.readonly_template or self.template
return field.renderer(template, **tmpl_values)
diff --git a/tailbone/templates/deform/date_jquery.pt b/tailbone/templates/deform/date_jquery.pt
index c33e8332..d8fe1668 100644
--- a/tailbone/templates/deform/date_jquery.pt
+++ b/tailbone/templates/deform/date_jquery.pt
@@ -20,4 +20,14 @@
}
);
+
+
+
diff --git a/tailbone/templates/shifts/base.mako b/tailbone/templates/shifts/base.mako
index fd608d86..a76f8c36 100644
--- a/tailbone/templates/shifts/base.mako
+++ b/tailbone/templates/shifts/base.mako
@@ -41,6 +41,15 @@
return true;
}
+ function date_selected(dateText, inst) {
+ if (confirm_leave()) {
+ $('#filter-form').submit();
+ } else {
+ // revert date value
+ $('.week-picker input[name="date"]').val($('.week-picker').data('week'));
+ }
+ }
+
$(function() {
$('#filter-form').submit(function() {
@@ -69,17 +78,7 @@
$('.week-picker button.nav').click(function() {
if (confirm_leave()) {
- $('.week-picker #date').val($(this).data('date'));
- $('#filter-form').submit();
- }
- });
-
- $('.week-picker #date').datepicker({
- dateFormat: 'mm/dd/yy',
- changeYear: true,
- changeMonth: true,
- showButtonPanel: true,
- onSelect: function(dateText, inst) {
+ $('.week-picker input[name="date"]').val($(this).data('date'));
$('#filter-form').submit();
}
});
@@ -134,8 +133,8 @@
<%def name="timesheet_wrapper(with_edit_form=False, change_employee=None)">
- ${form.begin(id='filter-form')}
- ${form.csrf_token()}
+ ${h.form(request.current_route_url(_query=False), id='filter-form')}
+ ${h.csrf_token(request)}
- ${form.end()}
+ ${h.end_form()}
% if with_edit_form:
${self.edit_form()}
diff --git a/tailbone/templates/shifts/schedule.mako b/tailbone/templates/shifts/schedule.mako
index 61a69c32..ec2a136c 100644
--- a/tailbone/templates/shifts/schedule.mako
+++ b/tailbone/templates/shifts/schedule.mako
@@ -6,7 +6,11 @@
${h.link_to("Edit Schedule", url('schedule.edit'))}
% endif
% if request.has_perm('schedule.print'):
- ${h.link_to("Print Schedule", url('schedule.print'), target='_blank')}
+ % if employee is Undefined:
+ ${h.link_to("Print Schedule", url('schedule.print'), target='_blank')}
+ % else:
+ ${h.link_to("Print this Schedule", url('schedule.employee.print'), target='_blank')}
+ % endif
% endif
% if request.has_perm('timesheet.view'):
${h.link_to("View this Time Sheet", url('schedule.goto.timesheet'), class_='goto')}
diff --git a/tailbone/templates/shifts/schedule_print_employee.mako b/tailbone/templates/shifts/schedule_print_employee.mako
new file mode 100644
index 00000000..0ceddd88
--- /dev/null
+++ b/tailbone/templates/shifts/schedule_print_employee.mako
@@ -0,0 +1,20 @@
+## -*- coding: utf-8; -*-
+<%namespace file="/shifts/base.mako" import="timesheet" />
+<%namespace file="/shifts/schedule.mako" import="render_day" />
+
+
+ ## TODO: this seems a little hacky..?
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/normalize.css'), media='all')}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/base.css'), media='all')}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css'), media='all')}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/timesheet.css'), media='all')}
+ ${h.stylesheet_link(request.static_url('tailbone:static/css/schedule_print.css'), media='print')}
+
+
+
+ ${employee} -
+ ${week_of}
+
+ ${timesheet(render_day=render_day)}
+
+
diff --git a/tailbone/views/shifts/lib.py b/tailbone/views/shifts/lib.py
index 253abd05..a8c9dfae 100644
--- a/tailbone/views/shifts/lib.py
+++ b/tailbone/views/shifts/lib.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2018 Lance Edgar
#
# This file is part of Rattail.
#
@@ -36,28 +36,29 @@ from rattail.db import model, api
from rattail.time import localtime, make_utc, get_sunday
from rattail.util import pretty_hours, hours_as_decimal
-import formencode as fe
-from pyramid_simpleform import Form
+import colander
+from deform import widget as dfwidget
from webhelpers2.html import tags, HTML
-from tailbone import forms
+from tailbone import forms2 as forms
from tailbone.db import Session
from tailbone.views import View
-class ShiftFilter(fe.Schema):
- allow_extra_fields = True
- filter_extra_fields = True
- store = forms.validators.ValidStore()
- department = forms.validators.ValidDepartment()
- date = fe.validators.DateConverter()
+class ShiftFilter(colander.Schema):
+
+ store = colander.SchemaNode(forms.types.StoreType())
+
+ department = colander.SchemaNode(forms.types.DepartmentType())
+
+ date = colander.SchemaNode(colander.Date())
-class EmployeeShiftFilter(fe.Schema):
- allow_extra_fields = True
- filter_extra_fields = True
- employee = forms.validators.ValidEmployee()
- date = fe.validators.DateConverter()
+class EmployeeShiftFilter(colander.Schema):
+
+ employee = colander.SchemaNode(forms.types.EmployeeType())
+
+ date = colander.SchemaNode(colander.Date())
class TimeSheetView(View):
@@ -166,47 +167,88 @@ class TimeSheetView(View):
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']
- self.request.session['timesheet.{}.store'.format(self.key)] = store.uuid if store else None
- department = form.data['department']
- 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
- raise self.redirect(self.request.current_route_url())
+ if form.validate(newstyle=True):
+ store = form.validated['store']
+ self.request.session['timesheet.{}.store'.format(self.key)] = store.uuid if store else None
+ department = form.validated['department']
+ self.request.session['timesheet.{}.department'.format(self.key)] = department.uuid if department else None
+ date = form.validated['date']
+ self.request.session['timesheet.{}.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None
+ raise self.redirect(self.request.current_route_url())
def process_employee_filter_form(self, form):
"""
Process an "employee 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():
- employee = form.data['employee']
- self.request.session['timesheet.{}.employee'.format(self.key)] = employee.uuid if employee else None
- date = form.data['date']
- self.request.session['timesheet.{}.employee.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None
- raise self.redirect(self.request.current_route_url())
+ if form.validate(newstyle=True):
+ employee = form.validated['employee']
+ self.request.session['timesheet.{}.employee'.format(self.key)] = employee.uuid if employee else None
+ date = form.validated['date']
+ self.request.session['timesheet.{}.employee.date'.format(self.key)] = date.strftime('%m/%d/%Y') if date else None
+ raise self.redirect(self.request.current_route_url())
+
+ def make_full_filter_form(self, context):
+ form = forms.Form(schema=ShiftFilter(), request=self.request)
+
+ stores = self.get_stores()
+ store_values = [(s.uuid, "{} - {}".format(s.id, s.name)) for s in stores]
+ store_values.insert(0, ('', "(all)"))
+ form.set_widget('store', forms.widgets.PlainSelectWidget(values=store_values))
+ if context['store']:
+ form.set_default('store', context['store'].uuid)
+
+ departments = self.get_departments()
+ department_values = [(d.uuid, d.name) for d in departments]
+ department_values.insert(0, ('', "(all)"))
+ form.set_widget('department', forms.widgets.PlainSelectWidget(values=department_values))
+ if context['department']:
+ form.set_default('department', context['department'].uuid)
+
+ form.set_type('date', 'date_jquery')
+ form.set_default('date', get_sunday(context['date']))
+ return 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()
+ form = self.make_full_filter_form(context)
+ self.process_filter_form(form)
context['form'] = form
return self.render_full(**context)
+ def make_employee_filter_form(self, context):
+ """
+ View time sheet for single employee.
+ """
+ permission_prefix = self.key
+ form = forms.Form(schema=EmployeeShiftFilter(), request=self.request)
+
+ if self.request.has_perm('{}.viewall'.format(permission_prefix)):
+ employee_display = six.text_type(context['employee'] or '')
+ employees_url = self.request.route_url('employees.autocomplete')
+ form.set_widget('employee', forms.widgets.JQueryAutocompleteWidget(
+ field_display=employee_display, service_url=employees_url))
+ if context['employee']:
+ form.set_default('employee', context['employee'].uuid)
+ else:
+ form.set_widget('employee', forms.widgets.ReadonlyWidget())
+ form.set_default('employee', context['employee'].uuid)
+
+ form.set_type('date', 'date_jquery')
+ form.set_default('date', get_sunday(context['date']))
+ return form
+
def employee(self):
"""
View time sheet for single employee.
"""
- form = Form(self.request, schema=EmployeeShiftFilter)
- self.process_employee_filter_form(form)
context = self.get_employee_context()
+ form = self.make_employee_filter_form(context)
+ self.process_employee_filter_form(form)
context['form'] = form
return self.render_single(**context)
@@ -280,7 +322,8 @@ class TimeSheetView(View):
context = {
'page_title': self.get_title_full(),
- 'form': forms.FormRenderer(form) if form else None,
+ 'form': form,
+ 'dform': form.make_deform_form() if form else None,
'employees': employees,
'stores': stores,
'store_options': store_options,
@@ -326,7 +369,8 @@ class TimeSheetView(View):
context = {
'single': True,
'page_title': "Employee {}".format(self.get_title()),
- 'form': forms.FormRenderer(form) if form else None,
+ 'form': form,
+ 'dform': form.make_deform_form() if form else None,
'employee': employee,
'employees': [employee],
'week_of': week_of,
diff --git a/tailbone/views/shifts/schedule.py b/tailbone/views/shifts/schedule.py
index a8113f9f..393acf8d 100644
--- a/tailbone/views/shifts/schedule.py
+++ b/tailbone/views/shifts/schedule.py
@@ -1,8 +1,8 @@
-# -*- coding: utf-8 -*-
+# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2018 Lance Edgar
#
# This file is part of Rattail.
#
@@ -31,10 +31,8 @@ import datetime
from rattail.db import model
from rattail.time import localtime, make_utc, get_sunday
-from pyramid_simpleform import Form
-
from tailbone.db import Session
-from tailbone.views.shifts.lib import TimeSheetView, ShiftFilter
+from tailbone.views.shifts.lib import TimeSheetView
class ScheduleView(TimeSheetView):
@@ -61,10 +59,9 @@ class ScheduleView(TimeSheetView):
return self.redirect(self.request.route_url('schedule.edit'))
# okay then, process filters; redirect if any were received
- form = Form(self.request, schema=ShiftFilter)
- self.process_filter_form(form)
-
context = self.get_timesheet_context()
+ form = self.make_full_filter_form(context)
+ self.process_filter_form(form)
# okay then, maybe process saved shift data
if self.request.method == 'POST':
@@ -199,12 +196,20 @@ class ScheduleView(TimeSheetView):
permission='schedule.edit')
config.add_tailbone_permission('schedule', 'schedule.edit', "Edit full schedule")
- # print schedule
+ # printing "any" schedule requires this permission
+ config.add_tailbone_permission('schedule', 'schedule.print', "Print schedule")
+
+ # print full schedule
config.add_route('schedule.print', '/schedule/print')
config.add_view(cls, attr='full', route_name='schedule.print',
renderer='/shifts/schedule_print.mako',
permission='schedule.print')
- config.add_tailbone_permission('schedule', 'schedule.print', "Print schedule")
+
+ # print employee schedule
+ config.add_route('schedule.employee.print', '/schedule/employee/print')
+ config.add_view(cls, attr='employee', route_name='schedule.employee.print',
+ renderer='/shifts/schedule_print_employee.mako',
+ permission='schedule.print')
def includeme(config):
diff --git a/tailbone/views/shifts/timesheet.py b/tailbone/views/shifts/timesheet.py
index b62b9bd4..5e8f9f51 100644
--- a/tailbone/views/shifts/timesheet.py
+++ b/tailbone/views/shifts/timesheet.py
@@ -1,8 +1,8 @@
-# -*- coding: utf-8 -*-
+# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2018 Lance Edgar
#
# This file is part of Rattail.
#
@@ -31,10 +31,8 @@ import datetime
from rattail.db import model
from rattail.time import make_utc, localtime
-from pyramid_simpleform import Form
-
from tailbone.db import Session
-from tailbone.views.shifts.lib import TimeSheetView as BaseTimeSheetView, EmployeeShiftFilter
+from tailbone.views.shifts.lib import TimeSheetView as BaseTimeSheetView
class TimeSheetView(BaseTimeSheetView):
@@ -50,10 +48,9 @@ class TimeSheetView(BaseTimeSheetView):
View for editing single employee's timesheet
"""
# process filters; redirect if any were received
- form = Form(self.request, schema=EmployeeShiftFilter)
- self.process_employee_filter_form(form)
-
context = self.get_employee_context()
+ form = self.make_employee_filter_form(context)
+ self.process_employee_filter_form(form)
# okay then, maybe process saved shift data
if self.request.method == 'POST':