Add readonly support for email profile settings.

More to come...  Also this required some form tweaking/overhaul(s).
This commit is contained in:
Lance Edgar 2015-12-04 00:31:22 -06:00
parent ba6bf87ded
commit ef40af814a
13 changed files with 453 additions and 48 deletions

View file

@ -24,7 +24,10 @@
Forms Forms
""" """
from .simpleform import * from formencode import Schema
from .core import Form, Field, FieldSet
from .simpleform import SimpleForm, FormRenderer
from .alchemy import AlchemyForm from .alchemy import AlchemyForm
from .fields import * from .fields import *
from .renderers import * from .renderers import *

View file

@ -64,6 +64,9 @@ class AlchemyForm(Object):
template = '/forms/form.mako' template = '/forms/form.mako'
return render(template, kwargs) return render(template, kwargs)
def render_fields(self):
return self.fieldset.render()
def save(self): def save(self):
self.fieldset.sync() self.fieldset.sync()
Session.flush() Session.flush()

110
tailbone/forms/core.py Normal file
View file

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2015 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 Affero 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 Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Forms Core
"""
from __future__ import unicode_literals, absolute_import
from edbob.util import prettify
from rattail.util import OrderedDict
from pyramid.renderers import render
from formalchemy.helpers import content_tag
class Form(object):
"""
Base class for all forms.
"""
create_label = "Create"
update_label = "Save"
def __init__(self, request, readonly=False, action_url=None):
self.request = request
self.readonly = readonly
self.action_url = action_url
def render(self, **kwargs):
kwargs.setdefault('form', self)
if self.readonly:
template = '/forms/form_readonly.mako'
else:
template = '/forms/form.mako'
return render(template, kwargs)
def render_fields(self, **kwargs):
kwargs.setdefault('fieldset', self.fieldset)
if self.readonly:
template = '/forms/fieldset_readonly.mako'
else:
template = '/forms/fieldset.mako'
return render(template, kwargs)
class Field(object):
"""
Manually create instances of this class to populate a simple form.
"""
def __init__(self, name, value=None, label=None, requires_label=True):
self.name = name
self.value = value
self._label = label or prettify(self.name)
self.requires_label = requires_label
def is_required(self):
return True
def label(self):
return self._label
def label_tag(self, **html_options):
"""
Logic stolen from FormAlchemy so all fields can render their own label.
Original docstring follows.
return the <label /> tag for the field.
"""
html_options.update(for_=self.name)
if 'class_' in html_options:
html_options['class_'] += self.is_required() and ' field_req' or ' field_opt'
else:
html_options['class_'] = self.is_required() and 'field_req' or 'field_opt'
return content_tag('label', self.label(), **html_options)
def render_readonly(self):
if self.value is None:
return ''
return unicode(self.value)
class FieldSet(object):
"""
Generic fieldset for use with manually-created simple forms.
"""
def __init__(self):
self.fields = OrderedDict()
self.render_fields = self.fields

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar # Copyright © 2010-2015 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -21,11 +20,13 @@
# along with Rattail. If not, see <http://www.gnu.org/licenses/>. # along with Rattail. If not, see <http://www.gnu.org/licenses/>.
# #
################################################################################ ################################################################################
""" """
``pyramid_simpleform`` Forms Simple Forms
""" """
from __future__ import unicode_literals, absolute_import
import pyramid_simpleform
from pyramid_simpleform import renderers from pyramid_simpleform import renderers
from webhelpers.html import tags from webhelpers.html import tags
@ -33,8 +34,24 @@ from webhelpers.html import HTML
from edbob.util import prettify from edbob.util import prettify
from tailbone.forms import Form
__all__ = ['FormRenderer']
class SimpleForm(Form):
"""
Customized simple form.
"""
def __init__(self, request, schema, obj=None, **kwargs):
super(SimpleForm, self).__init__(request, **kwargs)
self._form = pyramid_simpleform.Form(request, schema=schema, obj=obj)
def __getattr__(self, attr):
return getattr(self._form, attr)
def render(self, **kwargs):
kwargs['form'] = FormRenderer(self)
return super(SimpleForm, self).render(**kwargs)
class FormRenderer(renderers.FormRenderer): class FormRenderer(renderers.FormRenderer):
@ -42,6 +59,9 @@ class FormRenderer(renderers.FormRenderer):
Customized form renderer. Provides some extra methods for convenience. Customized form renderer. Provides some extra methods for convenience.
""" """
def __getattr__(self, attr):
return getattr(self.form, attr)
def field_div(self, name, field, label=None): def field_div(self, name, field, label=None):
errors = self.errors_for(name) errors = self.errors_for(name)
if errors: if errors:

View file

@ -34,6 +34,8 @@ from sqlalchemy import orm
import formalchemy import formalchemy
from webhelpers import paginate from webhelpers import paginate
from edbob.util import prettify
from tailbone.db import Session from tailbone.db import Session
from tailbone.newgrids import Grid, GridColumn, filters from tailbone.newgrids import Grid, GridColumn, filters
@ -61,8 +63,10 @@ class AlchemyGrid(Grid):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AlchemyGrid, self).__init__(*args, **kwargs) super(AlchemyGrid, self).__init__(*args, **kwargs)
self._fa_grid = formalchemy.Grid(self.model_class, instances=self.data, fa_grid = formalchemy.Grid(self.model_class, instances=self.data,
session=Session(), request=self.request) session=Session(), request=self.request)
fa_grid.prettify = prettify
self._fa_grid = fa_grid
def __delattr__(self, attr): def __delattr__(self, attr):
delattr(self._fa_grid, attr) delattr(self._fa_grid, attr)
@ -150,10 +154,8 @@ class AlchemyGrid(Grid):
Returns an iterator for all currently-visible columns. Returns an iterator for all currently-visible columns.
""" """
for field in self._fa_grid.render_fields.itervalues(): for field in self._fa_grid.render_fields.itervalues():
column = GridColumn() column = GridColumn(field.key, field.label())
column.field = field column.field = field
column.key = field.key
column.label = field.label()
yield column yield column
def iter_rows(self): def iter_rows(self):

View file

@ -26,6 +26,8 @@ Core Grid Classes
from __future__ import unicode_literals from __future__ import unicode_literals
from edbob.util import prettify
from rattail.db.api import get_setting, save_setting from rattail.db.api import get_setting, save_setting
from pyramid.renderers import render from pyramid.renderers import render
@ -136,6 +138,17 @@ class Grid(object):
sorters.update(updates) sorters.update(updates)
return sorters return sorters
def make_sorter(self, key, foldcase=False):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting a data set comprised of dicts, on the given key.
"""
if foldcase:
keyfunc = lambda v: v[key].lower()
else:
keyfunc = lambda v: v[key]
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc')
def load_settings(self, store=True): def load_settings(self, store=True):
""" """
Load current/effective settings for the grid, from the request query Load current/effective settings for the grid, from the request query
@ -150,9 +163,11 @@ class Grid(object):
settings = { settings = {
'sortkey': self.default_sortkey, 'sortkey': self.default_sortkey,
'sortdir': self.default_sortdir, 'sortdir': self.default_sortdir,
'pagesize': self.default_pagesize,
'page': self.default_page,
} }
if self.pageable:
settings['pagesize'] = self.default_pagesize
settings['page'] = self.default_page
if self.filterable:
for filtr in self.iter_filters(): for filtr in self.iter_filters():
settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active
settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb
@ -169,7 +184,7 @@ class Grid(object):
# If request has filter settings, grab those, then grab sort/pager # If request has filter settings, grab those, then grab sort/pager
# settings from request or session. # settings from request or session.
elif self.request_has_settings('filter'): elif self.filterable and self.request_has_settings('filter'):
self.update_filter_settings(settings, 'request') self.update_filter_settings(settings, 'request')
if self.request_has_settings('sort'): if self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request') self.update_sort_settings(settings, 'request')
@ -591,7 +606,7 @@ class Grid(object):
return tags.link_to(action.label, url) return tags.link_to(action.label, url)
def iter_rows(self): def iter_rows(self):
return [] return self.make_visible_data()
def get_row_attrs(self, row, i): def get_row_attrs(self, row, i):
""" """
@ -642,7 +657,7 @@ class Grid(object):
return self.cell_attrs return self.cell_attrs
def render_cell(self, row, column): def render_cell(self, row, column):
return '' return unicode(row[column.key])
def get_pagesize_options(self): def get_pagesize_options(self):
# TODO: Make configurable or something... # TODO: Make configurable or something...
@ -661,8 +676,10 @@ class GridColumn(object):
Human-facing label for the column, i.e. displayed in the header. Human-facing label for the column, i.e. displayed in the header.
""" """
key = None
label = None def __init__(self, key, label=None):
self.key = key
self.label = label or prettify(key)
class GridAction(object): class GridAction(object):
@ -673,7 +690,7 @@ class GridAction(object):
def __init__(self, key, label=None, url='#', icon=None): def __init__(self, key, label=None, url='#', icon=None):
self.key = key self.key = key
self.label = label or key.capitalize() self.label = label or prettify(key)
self.icon = icon self.icon = icon
self.url = url self.url = url

View file

@ -73,6 +73,10 @@ div.field-wrapper div.field {
float: left; float: left;
margin-bottom: 5px; margin-bottom: 5px;
line-height: 25px; line-height: 25px;
/* NOTE: This was added specifically for email settings (description),
who knows what it breaks...hopefully nothing. */
width: 800px;
} }
div.field-wrapper div.field input[type=text], div.field-wrapper div.field input[type=text],

View file

@ -0,0 +1,30 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/view.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<script type="text/javascript">
% if not email.get_template('html'):
$(function() {
$('#preview-html').button('disable');
$('#preview-html').attr('title', "There is no HTML template on file for this email.");
});
% endif
% if not email.get_template('txt'):
$(function() {
$('#preview-txt').button('disable');
$('#preview-txt').attr('title', "There is no TXT template on file for this email.");
});
% endif
</script>
</%def>
${parent.body()}
<form action="${url('email.preview')}" name="send-email-preview" method="post">
<a id="preview-html" class="button" href="${url('email.preview')}?key=${instance['key']}&amp;type=html" target="_blank">Preview HTML</a>
<a id="preview-txt" class="button" href="${url('email.preview')}?key=${instance['key']}&amp;type=txt" target="_blank">Preview TXT</a>
or
<input type="text" name="recipient" value="${request.user.email_address}" />
<input type="submit" name="send_${instance['key']}" value="Send Preview Email" />
</form>

View file

@ -2,7 +2,7 @@
<div class="form"> <div class="form">
${h.form(form.action_url, id=form_id or None, method='post', enctype='multipart/form-data')} ${h.form(form.action_url, id=form_id or None, method='post', enctype='multipart/form-data')}
${form.fieldset.render()|n} ${form.render_fields()|n}
% if buttons: % if buttons:
${buttons|n} ${buttons|n}

View file

@ -1,6 +1,6 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
<div class="form"> <div class="form">
${form.fieldset.render()|n} ${form.render_fields()|n}
% if buttons: % if buttons:
${buttons|n} ${buttons|n}
% endif % endif

View file

@ -1,7 +1,7 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
<%inherit file="/base.mako" /> <%inherit file="/base.mako" />
<%def name="title()">${model_title}: ${unicode(instance)}</%def> <%def name="title()">${model_title}: ${instance_title}</%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
<li>${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}</li> <li>${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}</li>

196
tailbone/views/email.py Normal file
View file

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2015 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 Affero 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 Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Email Views
"""
from __future__ import unicode_literals, absolute_import
from pyramid.httpexceptions import HTTPFound
from rattail import mail
from tailbone import forms
from tailbone.views import MasterView, View
from tailbone.newgrids import Grid, GridColumn
class ProfilesView(MasterView):
"""
Master view for email admin (settings/preview).
"""
normalized_model_name = 'emailprofile'
model_title = "Email Profile"
model_key = 'key'
url_prefix = '/email/profiles'
grid_factory = Grid
filterable = False
pageable = False
creatable = False
editable = False
deletable = False
def get_data(self, session=None):
data = []
for email in mail.iter_emails(self.rattail_config):
key = email.key or email.__name__
email = email(self.rattail_config, key)
data.append(self.normalize(email))
return data
def normalize(self, email):
return {
'key': email.key,
'description': email.__doc__,
'prefix': email.get_prefix(),
'subject': email.get_subject(),
'sender': email.get_sender(),
'replyto': email.get_replyto(),
'to': email.get_recips('to'),
'cc': email.get_recips('cc'),
'bcc': email.get_recips('bcc'),
}
def configure_grid(self, g):
g.columns = [
GridColumn('key'),
GridColumn('prefix'),
GridColumn('subject'),
]
g.sorters['key'] = g.make_sorter('key', foldcase=True)
g.sorters['prefix'] = g.make_sorter('prefix', foldcase=True)
g.sorters['subject'] = g.make_sorter('subject', foldcase=True)
g.default_sortkey = 'subject'
# g.main_actions = []
g.more_actions = []
def get_instance(self):
key = self.request.matchdict['key']
return self.normalize(mail.get_email(self.rattail_config, key))
def get_instance_title(self, email):
return email['key']
def make_form(self, email, **kwargs):
"""
Make a simple form for use with CRUD views.
"""
# TODO: This needs all kinds of help still...
class EmailSchema(forms.Schema):
pass
form = forms.SimpleForm(self.request, schema=EmailSchema(), obj=email)
form.creating = self.creating
form.editing = self.editing
form.readonly = self.viewing
if self.creating:
form.cancel_url = self.get_index_url()
else:
form.cancel_url = self.get_action_url('view', email)
form.fieldset = forms.FieldSet()
form.fieldset.fields['key'] = forms.Field('key', value=email['key'])
form.fieldset.fields['prefix'] = forms.Field('prefix', value=email['prefix'], label="Subject Prefix")
form.fieldset.fields['subject'] = forms.Field('subject', value=email['subject'], label="Subject Text")
form.fieldset.fields['description'] = forms.Field('description', value=email['description'])
form.fieldset.fields['sender'] = forms.Field('sender', value=email['sender'], label="From")
form.fieldset.fields['replyto'] = forms.Field('replyto', value=email['replyto'], label="Reply-To")
form.fieldset.fields['to'] = forms.Field('to', value=', '.join(email['to']) if email['to'] else None)
form.fieldset.fields['cc'] = forms.Field('cc', value=', '.join(email['cc']) if email['cc'] else None)
form.fieldset.fields['bcc'] = forms.Field('bcc', value=', '.join(email['bcc']) if email['bcc'] else None)
return form
def template_kwargs_view(self, **kwargs):
key = self.request.matchdict['key']
kwargs['email'] = mail.get_email(self.rattail_config, key)
return kwargs
class EmailPreview(View):
"""
Lists available email templates, and can show previews of each.
"""
def __call__(self):
# Forms submitted via POST are only used for sending emails.
if self.request.method == 'POST':
self.email_template()
return HTTPFound(location=self.request.get_referrer(
default=self.request.route_url('emailprofiles')))
# Maybe render a preview?
key = self.request.GET.get('key')
if key:
type_ = self.request.GET.get('type', 'html')
return self.preview_template(key, type_)
assert False, "should not be here"
def email_template(self):
recipient = self.request.POST.get('recipient')
if recipient:
keys = filter(lambda k: k.startswith('send_'), self.request.POST.iterkeys())
key = keys[0][5:] if keys else None
if key:
email = mail.get_email(self.rattail_config, key)
data = email.sample_data(self.request)
msg = email.make_message(data)
subject = msg['Subject']
del msg['Subject']
msg['Subject'] = "[preview] {0}".format(subject)
del msg['To']
del msg['Cc']
del msg['Bcc']
msg['To'] = recipient
mail.deliver_message(self.rattail_config, msg)
self.request.session.flash("Preview for '{0}' was emailed to {1}".format(key, recipient))
def preview_template(self, key, type_):
email = mail.get_email(self.rattail_config, key)
template = email.get_template(type_)
data = email.sample_data(self.request)
self.request.response.text = template.render(**data)
if type_ == 'txt':
self.request.response.content_type = b'text/plain'
return self.request.response
def includeme(config):
ProfilesView.defaults(config)
config.add_route('email.preview', '/email/preview/')
config.add_view(EmailPreview, route_name='email.preview',
renderer='/email/preview.mako',
permission='admin')

View file

@ -35,15 +35,18 @@ import formalchemy
from pyramid.renderers import get_renderer, render_to_response from pyramid.renderers import get_renderer, render_to_response
from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from tailbone import forms
from tailbone.views import View from tailbone.views import View
from tailbone.newgrids import filters, AlchemyGrid, GridAction from tailbone.newgrids import filters, AlchemyGrid, GridAction
from tailbone.forms import AlchemyForm
class MasterView(View): class MasterView(View):
""" """
Base "master" view class. All model master views should derive from this. Base "master" view class. All model master views should derive from this.
""" """
filterable = True
pageable = True
creatable = True creatable = True
viewable = True viewable = True
editable = True editable = True
@ -115,7 +118,12 @@ class MasterView(View):
instance = self.get_instance() instance = self.get_instance()
form = self.make_form(instance) form = self.make_form(instance)
return self.render_to_response('view', { return self.render_to_response('view', {
'instance': instance, 'form': form}) 'instance': instance,
'form': form,
'instance_title': self.get_instance_title(instance)})
def get_instance_title(self, instance):
return unicode(instance)
def edit(self): def edit(self):
""" """
@ -156,13 +164,13 @@ class MasterView(View):
############################## ##############################
@classmethod @classmethod
def get_model_class(cls): def get_model_class(cls, error=True):
""" """
Returns the data model class for which the master view exists. Returns the data model class for which the master view exists.
""" """
if not hasattr(cls, 'model_class'): if not hasattr(cls, 'model_class') and error:
raise NotImplementedError("You must define the `model_class` for: {0}".format(cls)) raise NotImplementedError("You must define the `model_class` for: {0}".format(cls))
return cls.model_class return getattr(cls, 'model_class', None)
@classmethod @classmethod
def get_normalized_model_name(cls): def get_normalized_model_name(cls):
@ -172,7 +180,9 @@ class MasterView(View):
otherwise it will be a simple lower-cased version of the associated otherwise it will be a simple lower-cased version of the associated
model class name. model class name.
""" """
return getattr(cls, 'normalized_model_name', cls.get_model_class().__name__.lower()) if hasattr(cls, 'normalized_model_name'):
return cls.normalized_model_name
return cls.get_model_class().__name__.lower()
@classmethod @classmethod
def get_model_key(cls): def get_model_key(cls):
@ -189,7 +199,9 @@ class MasterView(View):
""" """
Return a "humanized" version of the model name, for display in templates. Return a "humanized" version of the model name, for display in templates.
""" """
return getattr(cls, 'model_title', cls.model_class.__name__) if hasattr(cls, 'model_title'):
return cls.model_title
return cls.model_class.__name__
@classmethod @classmethod
def get_model_title_plural(cls): def get_model_title_plural(cls):
@ -313,11 +325,11 @@ class MasterView(View):
""" """
return { return {
'width': 'full', 'width': 'full',
'filterable': True, 'filterable': self.filterable,
'sortable': True, 'sortable': True,
'default_sortkey': getattr(self, 'default_sortkey', None), 'default_sortkey': getattr(self, 'default_sortkey', None),
'sortdir': getattr(self, 'sortdir', 'asc'), 'sortdir': getattr(self, 'sortdir', 'asc'),
'pageable': True, 'pageable': self.pageable,
'main_actions': self.get_main_actions(), 'main_actions': self.get_main_actions(),
'more_actions': self.get_more_actions(), 'more_actions': self.get_more_actions(),
'checkbox': self.checkbox, 'checkbox': self.checkbox,
@ -383,7 +395,11 @@ class MasterView(View):
""" """
Hopefully generic kwarg generator for basic action routes. Hopefully generic kwarg generator for basic action routes.
""" """
try:
mapper = orm.object_mapper(row) mapper = orm.object_mapper(row)
except orm.exc.UnmappedInstanceError:
return {self.model_key: row[self.model_key]}
else:
keys = [k.key for k in mapper.primary_key] keys = [k.key for k in mapper.primary_key]
values = [getattr(row, k) for k in keys] values = [getattr(row, k) for k in keys]
return dict(zip(keys, values)) return dict(zip(keys, values))
@ -394,10 +410,9 @@ class MasterView(View):
""" """
factory = self.get_grid_factory() factory = self.get_grid_factory()
key = self.get_grid_key() key = self.get_grid_key()
data = self.make_query() data = self.get_data()
kwargs = self.make_grid_kwargs() kwargs = self.make_grid_kwargs()
grid = factory(key, self.request, data=data, model_class=self.model_class, **kwargs) grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
grid._fa_grid.prettify = prettify
self.configure_grid(grid) self.configure_grid(grid)
grid.load_settings() grid.load_settings()
return grid return grid
@ -413,12 +428,17 @@ class MasterView(View):
This requirement is a result of using FormAlchemy under the hood, and This requirement is a result of using FormAlchemy under the hood, and
it is in fact a call to :meth:`formalchemy:formalchemy.tables.Grid.configure()`. it is in fact a call to :meth:`formalchemy:formalchemy.tables.Grid.configure()`.
""" """
if hasattr(grid, 'configure'):
grid.configure() grid.configure()
def make_query(self, session=None): def get_data(self, session=None):
""" """
Make the base query to be used for the grid. Subclasses should not Generate the base data set for the grid. This typically will be a
override this method; override :meth:`query()` instead. SQLAlchemy query against the view's model class, but subclasses may
override this to support arbitrary data sets.
Note that if your view is typical and uses a SA model, you should not
override this methid, but override :meth:`query()` instead.
""" """
if session is None: if session is None:
session = self.Session() session = self.Session()
@ -484,7 +504,7 @@ class MasterView(View):
kwargs.setdefault('cancel_url', self.get_index_url()) kwargs.setdefault('cancel_url', self.get_index_url())
else: else:
kwargs.setdefault('cancel_url', self.get_action_url('view', instance)) kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
form = AlchemyForm(self.request, fieldset, **kwargs) form = forms.AlchemyForm(self.request, fieldset, **kwargs)
form.readonly = self.viewing form.readonly = self.viewing
return form return form