Add new v3 master with v2 forms, with colander/deform

goal here is to replace FormAlchemy dependency, slowly but surely..
so far only the Settings and Stores views use v3 master
This commit is contained in:
Lance Edgar 2017-07-18 12:29:05 -05:00
parent 2b5aaa0753
commit 4dcd89fba7
18 changed files with 718 additions and 59 deletions

View file

@ -29,6 +29,7 @@ from __future__ import unicode_literals, absolute_import
from .core import View
from .master import MasterView
from .master2 import MasterView2
from .master3 import MasterView3
# TODO: deprecate / remove some of this
from .autocomplete import AutocompleteView

View file

@ -225,7 +225,7 @@ class MasterView(View):
self.creating = True
form = self.make_form(self.get_model_class())
if self.request.method == 'POST':
if form.validate():
if self.validate_form(form):
# let save_create_form() return alternate object if necessary
obj = self.save_create_form(form) or form.fieldset.model
self.after_create(obj)
@ -615,6 +615,7 @@ class MasterView(View):
"""
self.editing = True
instance = self.get_instance()
instance_title = self.get_instance_title(instance)
if not self.editable_instance(instance):
self.request.session.flash("Edit is not permitted for {}: {}".format(
@ -624,18 +625,22 @@ class MasterView(View):
form = self.make_form(instance)
if self.request.method == 'POST':
if form.validate():
if self.validate_form(form):
self.save_edit_form(form)
# note we must fetch new instance title, in case it changed
self.request.session.flash("{} has been updated: {}".format(
self.get_model_title(), self.get_instance_title(instance)))
return self.redirect_after_edit(instance)
return self.render_to_response('edit', {
'instance': instance,
'instance_title': self.get_instance_title(instance),
'instance_title': instance_title,
'instance_deletable': self.deletable_instance(instance),
'form': form})
def validate_form(self, form):
return form.validate()
def save_edit_form(self, form):
self.save_form(form)
self.after_edit(form.fieldset.model)

View file

@ -39,6 +39,7 @@ class MasterView2(MasterView):
sortable = True
rows_pageable = True
mobile_pageable = True
labels = {'uuid': "UUID"}
@classmethod
def get_grid_factory(cls):
@ -391,8 +392,12 @@ class MasterView2(MasterView):
the given row object, or ``None``.
"""
def set_labels(self, obj):
for key, label in self.labels.items():
obj.set_label(key, label)
def configure_grid(self, grid):
pass
self.set_labels(grid)
def configure_row_grid(self, grid):
pass

134
tailbone/views/master3.py Normal file
View file

@ -0,0 +1,134 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Master View
"""
from __future__ import unicode_literals, absolute_import
from sqlalchemy import orm
import deform
from tailbone import forms2 as forms
from tailbone.views import MasterView2
class MasterView3(MasterView2):
"""
Base "master" view class. All model master views should derive from this.
"""
@classmethod
def get_form_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
grid instances.
"""
return getattr(cls, 'form_factory', forms.Form)
def make_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs):
"""
Creates a new form for the given model class/instance
"""
if factory is None:
factory = self.get_form_factory()
if fields is None:
fields = self.get_form_fields()
if schema is None:
schema = self.make_form_schema()
if not self.creating:
kwargs['model_instance'] = instance
kwargs = self.make_form_kwargs(**kwargs)
form = factory(fields, schema, **kwargs)
self.configure_form(form)
return form
def make_form_schema(self):
if not self.model_class:
# TODO
raise NotImplementedError
def get_form_fields(self):
if hasattr(self, 'form_fields'):
return self.form_fields
# TODO
# raise NotImplementedError
def make_form_kwargs(self, **kwargs):
"""
Return a dictionary of kwargs to be passed to the factory when creating
new form instances.
"""
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:
kwargs.setdefault('cancel_url', self.get_index_url())
else:
instance = kwargs['model_instance']
kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
defaults.update(kwargs)
return defaults
def configure_form(self, form):
"""
Configure the primary form. By default this just sets any primary key
fields to be readonly (if we have a :attr:`model_class`).
"""
if self.editing and self.model_class:
mapper = orm.class_mapper(self.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 validate_form(self, form):
controls = self.request.POST.items()
try:
self.form_deserialized = form.validate(controls)
except deform.ValidationFailure:
return False
return True
def save_create_form(self, form):
self.before_create(form)
obj = form.schema.objectify(self.form_deserialized)
self.Session.add(obj)
self.Session.flush()
return obj
def save_edit_form(self, form):
obj = form.schema.objectify(self.form_deserialized, context=form.model_instance)
self.after_edit(obj)
self.Session.flush()

View file

@ -30,16 +30,9 @@ import re
from rattail.db import model
import formalchemy as fa
import colander
from tailbone.db import Session
from tailbone.views import MasterView2 as MasterView
def unique_name(value, field):
setting = Session.query(model.Setting).get(value)
if setting:
raise fa.ValidationError("Setting name must be unique")
from tailbone.views import MasterView3 as MasterView
class SettingsView(MasterView):
@ -48,27 +41,28 @@ class SettingsView(MasterView):
"""
model_class = model.Setting
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
grid_columns = [
'name',
'value',
]
def configure_grid(self, g):
super(SettingsView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
g.set_link('name')
def _preconfigure_fieldset(self, fs):
fs.name.set(validate=unique_name)
if self.editing:
fs.name.set(readonly=True)
def configure_form(self, f):
super(SettingsView, self).configure_form(f)
if self.creating:
f.set_validator('name', self.unique_name)
def configure_fieldset(self, fs):
fs.configure(include=[
fs.name,
fs.value,
])
def unique_name(self, node, value):
setting = self.Session.query(model.Setting).get(value)
if setting:
raise colander.Invalid(node, "Setting name must be unique")
def editable_instance(self, setting):
if self.rattail_config.demo():

View file

@ -30,7 +30,9 @@ import sqlalchemy as sa
from rattail.db import model
from tailbone.views import MasterView2 as MasterView
import colander
from tailbone.views import MasterView3 as MasterView
class StoresView(MasterView):
@ -39,6 +41,7 @@ class StoresView(MasterView):
"""
model_class = model.Store
has_versions = True
grid_columns = [
'id',
'name',
@ -46,40 +49,47 @@ class StoresView(MasterView):
'email',
]
def configure_grid(self, g):
labels = {
'id': "ID",
'phone': "Phone Number",
'email': "Email Address",
}
g.joiners['email'] = lambda q: q.outerjoin(model.StoreEmailAddress, sa.and_(
def configure_grid(self, g):
super(StoresView, self).configure_grid(g)
g.set_joiner('email', lambda q: q.outerjoin(model.StoreEmailAddress, sa.and_(
model.StoreEmailAddress.parent_uuid == model.Store.uuid,
model.StoreEmailAddress.preference == 1))
g.joiners['phone'] = lambda q: q.outerjoin(model.StorePhoneNumber, sa.and_(
model.StoreEmailAddress.preference == 1)))
g.set_joiner('phone', lambda q: q.outerjoin(model.StorePhoneNumber, sa.and_(
model.StorePhoneNumber.parent_uuid == model.Store.uuid,
model.StorePhoneNumber.preference == 1))
model.StorePhoneNumber.preference == 1)))
g.filters['phone'] = g.make_filter('phone', model.StorePhoneNumber.number)
g.filters['email'] = g.make_filter('email', model.StoreEmailAddress.address)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.sorters['phone'] = g.make_sorter(model.StorePhoneNumber.number)
g.sorters['email'] = g.make_sorter(model.StoreEmailAddress.address)
g.set_sorter('phone', model.StorePhoneNumber.number)
g.set_sorter('email', model.StoreEmailAddress.address)
g.default_sortkey = 'id'
g.set_link('id')
g.set_link('name')
g.set_label('id', "ID")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
def configure_form(self, f):
super(StoresView, self).configure_form(f)
def configure_fieldset(self, fs):
fs.configure(
include=[
fs.id.label("ID"),
fs.name,
fs.database_key,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
])
f.remove_field('employees')
f.remove_field('phones')
f.remove_field('emails')
if self.creating:
f.remove_field('phone')
f.remove_field('email')
else:
f.set_readonly('phone')
f.set_readonly('email')
def get_version_child_classes(self):
return [