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:
parent
2b5aaa0753
commit
4dcd89fba7
18 changed files with 718 additions and 59 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
134
tailbone/views/master3.py
Normal 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()
|
|
@ -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():
|
||||
|
|
|
@ -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 [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue