diff --git a/tailbone/templates/mobile/master/create_row.mako b/tailbone/templates/mobile/master/create_row.mako index 8a6157d2..7b5dae0c 100644 --- a/tailbone/templates/mobile/master/create_row.mako +++ b/tailbone/templates/mobile/master/create_row.mako @@ -1,4 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/mobile/master/create.mako" /> +<%def name="title()">New ${model_title} Row + ${parent.body()} diff --git a/tailbone/templates/mobile/master/view_row.mako b/tailbone/templates/mobile/master/view_row.mako index 36817eb1..513563d9 100644 --- a/tailbone/templates/mobile/master/view_row.mako +++ b/tailbone/templates/mobile/master/view_row.mako @@ -8,5 +8,5 @@ ${parent.body()} % if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)): - ${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn')} + ${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')} % endif diff --git a/tailbone/views/__init__.py b/tailbone/views/__init__.py index d8090a19..f2b4fcdc 100644 --- a/tailbone/views/__init__.py +++ b/tailbone/views/__init__.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. # @@ -30,6 +30,7 @@ from .core import View from .master import MasterView from .master2 import MasterView2 from .master3 import MasterView3 +from .master4 import MasterView4 # TODO: deprecate / remove some of this from .autocomplete import AutocompleteView diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index 012a151f..a5305033 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.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. # @@ -39,7 +39,7 @@ from webhelpers2.html import HTML, tags from tailbone import grids from tailbone.db import Session -from tailbone.views import MasterView3 as MasterView, AutocompleteView +from tailbone.views import MasterView4 as MasterView, AutocompleteView from rattail.db import model @@ -83,6 +83,19 @@ class CustomersView(MasterView): 'groups', ] + mobile_form_fields = [ + 'id', + 'name', + 'default_phone', + 'default_email', + 'default_address', + 'email_preference', + 'active_in_pos', + 'active_in_pos_sticky', + 'people', + 'groups', + ] + def configure_grid(self, g): super(CustomersView, self).configure_grid(g) @@ -286,13 +299,6 @@ class CustomersView(MasterView): items.append(HTML.tag('li', tags.link_to(text, url))) return HTML.tag('ul', HTML.literal('').join(items)) - # def configure_mobile_fieldset(self, fs): - # fs.configure( - # include=[ - # fs.email, - # fs.phone, - # ]) - def get_version_child_classes(self): return [ (model.CustomerPhoneNumber, 'parent_uuid'), diff --git a/tailbone/views/master.py b/tailbone/views/master.py index d0873a92..4a9bc50e 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -824,6 +824,9 @@ class MasterView(View): def validate_mobile_form(self, form): return form.validate() + def validate_row_form(self, form): + return form.validate() + def save_edit_form(self, form): self.save_form(form) self.after_edit(form.fieldset.model) @@ -1741,10 +1744,9 @@ class MasterView(View): index_url = self.get_action_url('view', parent) form = self.make_row_form(self.model_row_class, cancel_url=index_url) if self.request.method == 'POST': - if form.validate(): + if self.validate_row_form(form): self.before_create_row(form) - self.save_create_row_form(form) - obj = form.fieldset.model + obj = self.save_create_row_form(form) or form.fieldset.model self.after_create_row(obj) return self.redirect_after_create_row(obj) return self.render_to_response('create_row', { @@ -1775,7 +1777,7 @@ class MasterView(View): instance_url = self.get_action_url('view', parent, mobile=True) form = self.make_mobile_row_form(self.model_row_class, cancel_url=instance_url) if self.request.method == 'POST': - if form.validate(): + if self.validate_mobile_row_form(form): self.before_create_row(form) # let save() return alternate object if necessary obj = self.save_create_row_form(form) or form.fieldset.model @@ -1835,7 +1837,7 @@ class MasterView(View): form = self.make_row_form(row) if self.request.method == 'POST': - if form.validate(): + if self.validate_row_form(form): self.save_edit_row_form(form) return self.redirect_after_edit_row(row) @@ -1860,7 +1862,7 @@ class MasterView(View): form = self.make_mobile_row_form(row) if self.request.method == 'POST': - if form.validate(): + if self.validate_mobile_row_form(form): self.save_edit_row_form(form) return self.redirect_after_edit_row(row, mobile=True) diff --git a/tailbone/views/master4.py b/tailbone/views/master4.py new file mode 100644 index 00000000..07ae1247 --- /dev/null +++ b/tailbone/views/master4.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2018 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Master View (v4) +""" + +from __future__ import unicode_literals, absolute_import + +import deform + +from tailbone import forms2 as forms +from tailbone.views import MasterView3 + + +class MasterView4(MasterView3): + """ + Base "master" view class. All model master views should derive from this. + """ + row_labels = {} + + def make_mobile_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): + """ + Creates a new mobile form for the given model class/instance. + """ + if factory is None: + factory = self.get_mobile_form_factory() + if fields is None: + fields = self.get_mobile_form_fields() + if schema is None: + schema = self.make_mobile_form_schema() + + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_mobile_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_mobile_form(form) + return form + + def make_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): + """ + Creates a new row form for the given model class/instance. + """ + if factory is None: + factory = self.get_row_form_factory() + if fields is None: + fields = self.get_row_form_fields() + if schema is None: + schema = self.make_row_form_schema() + + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_row_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_row_form(form) + return form + + def make_mobile_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): + """ + Creates a new mobile form for the given model class/instance. + """ + if factory is None: + factory = self.get_mobile_row_form_factory() + if fields is None: + fields = self.get_mobile_row_form_fields() + if schema is None: + schema = self.make_mobile_row_form_schema() + + if not self.creating: + kwargs['model_instance'] = instance + kwargs = self.make_mobile_row_form_kwargs(**kwargs) + form = factory(fields, schema, **kwargs) + self.configure_mobile_row_form(form) + return form + + @classmethod + def get_mobile_form_factory(cls): + """ + Returns the factory or class which is to be used when creating new + mobile forms. + """ + return getattr(cls, 'mobile_form_factory', forms.Form) + + @classmethod + def get_row_form_factory(cls): + """ + Returns the factory or class which is to be used when creating new row + forms. + """ + return getattr(cls, 'row_form_factory', forms.Form) + + @classmethod + def get_mobile_row_form_factory(cls): + """ + Returns the factory or class which is to be used when creating new + mobile row forms. + """ + return getattr(cls, 'mobile_row_form_factory', forms.Form) + + def make_mobile_form_schema(self): + if not self.model_class: + # TODO + raise NotImplementedError + + def make_row_form_schema(self): + if not self.model_row_class: + # TODO + raise NotImplementedError + + def make_mobile_row_form_schema(self): + if not self.model_row_class: + # TODO + raise NotImplementedError + + def get_mobile_form_fields(self): + if hasattr(self, 'mobile_form_fields'): + return self.mobile_form_fields + # TODO + # raise NotImplementedError + + def get_row_form_fields(self): + if hasattr(self, 'row_form_fields'): + return self.row_form_fields + # TODO + # raise NotImplementedError + + def get_mobile_row_form_fields(self): + if hasattr(self, 'mobile_row_form_fields'): + return self.mobile_row_form_fields + # TODO + # raise NotImplementedError + + def make_mobile_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new mobile forms. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_class', None), + 'action_url': self.request.current_route_url(_query=None), + } + if self.creating: + defaults['cancel_url'] = self.get_index_url(mobile=True) + else: + instance = kwargs['model_instance'] + defaults['cancel_url'] = self.get_action_url('view', instance, mobile=True) + defaults.update(kwargs) + return defaults + + def make_row_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new row forms. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_row_class', None), + 'action_url': self.request.current_route_url(_query=None), + } + if self.creating: + kwargs.setdefault('cancel_url', self.request.get_referrer()) + else: + instance = kwargs['model_instance'] + kwargs.setdefault('cancel_url', self.get_row_action_url('view', instance)) + defaults.update(kwargs) + return defaults + + def make_mobile_row_form_kwargs(self, **kwargs): + """ + Return a dictionary of kwargs to be passed to the factory when creating + new mobile row forms. + """ + defaults = { + 'request': self.request, + 'readonly': self.viewing, + 'model_class': getattr(self, 'model_row_class', None), + 'action_url': self.request.current_route_url(_query=None), + } + if self.creating: + defaults['cancel_url'] = self.request.get_referrer() + else: + instance = kwargs['model_instance'] + defaults['cancel_url'] = self.get_row_action_url('view', instance, mobile=True) + defaults.update(kwargs) + return defaults + + def configure_mobile_form(self, form): + """ + Configure the primary mobile form. + """ + # TODO: is any of this stuff from configure_form() needed? + # if self.editing: + # model_class = self.get_model_class(error=False) + # if model_class: + # mapper = orm.class_mapper(model_class) + # for key in mapper.primary_key: + # for field in form.fields: + # if field == key.name: + # form.set_readonly(field) + # break + # form.remove_field('uuid') + + self.set_labels(form) + + def configure_row_grid(self, grid): + super(MasterView4, self).configure_row_grid(grid) + self.set_row_labels(grid) + + def configure_row_form(self, form): + """ + Configure a row form. + """ + # TODO: is any of this stuff from configure_form() needed? + # if self.editing: + # model_class = self.get_model_class(error=False) + # if model_class: + # mapper = orm.class_mapper(model_class) + # for key in mapper.primary_key: + # for field in form.fields: + # if field == key.name: + # form.set_readonly(field) + # break + # form.remove_field('uuid') + + self.set_row_labels(form) + + def configure_mobile_row_form(self, form): + """ + Configure the mobile row form. + """ + # TODO: is any of this stuff from configure_form() needed? + # if self.editing: + # model_class = self.get_model_class(error=False) + # if model_class: + # mapper = orm.class_mapper(model_class) + # for key in mapper.primary_key: + # for field in form.fields: + # if field == key.name: + # form.set_readonly(field) + # break + # form.remove_field('uuid') + + self.set_row_labels(form) + + def set_row_labels(self, obj): + for key, label in self.row_labels.items(): + obj.set_label(key, label) + + def validate_mobile_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + + def validate_row_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + + def validate_mobile_row_form(self, form): + controls = self.request.POST.items() + try: + self.form_deserialized = form.validate(controls) + except deform.ValidationFailure: + return False + return True + + def save_mobile_create_form(self, form): + self.before_create(form) + with self.Session.no_autoflush: + obj = self.objectify(form, self.form_deserialized) + self.before_create_flush(obj, form) + self.Session.add(obj) + self.Session.flush() + return obj + + # TODO: still need to verify this logic + def save_create_row_form(self, form): + # self.before_create(form) + # with self.Session().no_autoflush: + # obj = self.objectify(form, self.form_deserialized) + # self.before_create_flush(obj, form) + obj = self.objectify(form, self.form_deserialized) + self.Session.add(obj) + self.Session.flush() + return obj + + def save_edit_row_form(self, form): + obj = self.objectify(form, self.form_deserialized) + self.after_edit_row(obj) + self.Session.flush() + return obj