diff --git a/tailbone/forms/__init__.py b/tailbone/forms/__init__.py
index 07870812..ae08b390 100644
--- a/tailbone/forms/__init__.py
+++ b/tailbone/forms/__init__.py
@@ -24,7 +24,10 @@
Forms
"""
-from .simpleform import *
+from formencode import Schema
+
+from .core import Form, Field, FieldSet
+from .simpleform import SimpleForm, FormRenderer
from .alchemy import AlchemyForm
from .fields import *
from .renderers import *
diff --git a/tailbone/forms/alchemy.py b/tailbone/forms/alchemy.py
index fd5b5651..9f8eede4 100644
--- a/tailbone/forms/alchemy.py
+++ b/tailbone/forms/alchemy.py
@@ -64,6 +64,9 @@ class AlchemyForm(Object):
template = '/forms/form.mako'
return render(template, kwargs)
+ def render_fields(self):
+ return self.fieldset.render()
+
def save(self):
self.fieldset.sync()
Session.flush()
diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py
new file mode 100644
index 00000000..2508b639
--- /dev/null
+++ b/tailbone/forms/core.py
@@ -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 .
+#
+################################################################################
+"""
+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 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
diff --git a/tailbone/forms/simpleform.py b/tailbone/forms/simpleform.py
index de482bd5..b1510148 100644
--- a/tailbone/forms/simpleform.py
+++ b/tailbone/forms/simpleform.py
@@ -1,9 +1,8 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2012 Lance Edgar
+# Copyright © 2010-2015 Lance Edgar
#
# This file is part of Rattail.
#
@@ -21,11 +20,13 @@
# along with Rattail. If not, see .
#
################################################################################
-
"""
-``pyramid_simpleform`` Forms
+Simple Forms
"""
+from __future__ import unicode_literals, absolute_import
+
+import pyramid_simpleform
from pyramid_simpleform import renderers
from webhelpers.html import tags
@@ -33,8 +34,24 @@ from webhelpers.html import HTML
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):
@@ -42,6 +59,9 @@ class FormRenderer(renderers.FormRenderer):
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):
errors = self.errors_for(name)
if errors:
diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py
index 5c4e249e..dca0e35d 100644
--- a/tailbone/newgrids/alchemy.py
+++ b/tailbone/newgrids/alchemy.py
@@ -34,6 +34,8 @@ from sqlalchemy import orm
import formalchemy
from webhelpers import paginate
+from edbob.util import prettify
+
from tailbone.db import Session
from tailbone.newgrids import Grid, GridColumn, filters
@@ -61,8 +63,10 @@ class AlchemyGrid(Grid):
def __init__(self, *args, **kwargs):
super(AlchemyGrid, self).__init__(*args, **kwargs)
- self._fa_grid = formalchemy.Grid(self.model_class, instances=self.data,
- session=Session(), request=self.request)
+ fa_grid = formalchemy.Grid(self.model_class, instances=self.data,
+ session=Session(), request=self.request)
+ fa_grid.prettify = prettify
+ self._fa_grid = fa_grid
def __delattr__(self, attr):
delattr(self._fa_grid, attr)
@@ -150,10 +154,8 @@ class AlchemyGrid(Grid):
Returns an iterator for all currently-visible columns.
"""
for field in self._fa_grid.render_fields.itervalues():
- column = GridColumn()
+ column = GridColumn(field.key, field.label())
column.field = field
- column.key = field.key
- column.label = field.label()
yield column
def iter_rows(self):
diff --git a/tailbone/newgrids/core.py b/tailbone/newgrids/core.py
index e01160fb..ea169f7f 100644
--- a/tailbone/newgrids/core.py
+++ b/tailbone/newgrids/core.py
@@ -26,6 +26,8 @@ Core Grid Classes
from __future__ import unicode_literals
+from edbob.util import prettify
+
from rattail.db.api import get_setting, save_setting
from pyramid.renderers import render
@@ -136,6 +138,17 @@ class Grid(object):
sorters.update(updates)
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):
"""
Load current/effective settings for the grid, from the request query
@@ -150,13 +163,15 @@ class Grid(object):
settings = {
'sortkey': self.default_sortkey,
'sortdir': self.default_sortdir,
- 'pagesize': self.default_pagesize,
- 'page': self.default_page,
}
- for filtr in self.iter_filters():
- settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active
- settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb
- settings['filter.{0}.value'.format(filtr.key)] = filtr.default_value
+ if self.pageable:
+ settings['pagesize'] = self.default_pagesize
+ settings['page'] = self.default_page
+ if self.filterable:
+ for filtr in self.iter_filters():
+ settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active
+ settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb
+ settings['filter.{0}.value'.format(filtr.key)] = filtr.default_value
# If user has default settings on file, apply those first.
if self.user_has_defaults():
@@ -169,7 +184,7 @@ class Grid(object):
# If request has filter settings, grab those, then grab sort/pager
# 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')
if self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
@@ -591,7 +606,7 @@ class Grid(object):
return tags.link_to(action.label, url)
def iter_rows(self):
- return []
+ return self.make_visible_data()
def get_row_attrs(self, row, i):
"""
@@ -642,7 +657,7 @@ class Grid(object):
return self.cell_attrs
def render_cell(self, row, column):
- return ''
+ return unicode(row[column.key])
def get_pagesize_options(self):
# TODO: Make configurable or something...
@@ -661,8 +676,10 @@ class GridColumn(object):
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):
@@ -673,7 +690,7 @@ class GridAction(object):
def __init__(self, key, label=None, url='#', icon=None):
self.key = key
- self.label = label or key.capitalize()
+ self.label = label or prettify(key)
self.icon = icon
self.url = url
diff --git a/tailbone/static/css/forms.css b/tailbone/static/css/forms.css
index 728dec19..ee132baf 100644
--- a/tailbone/static/css/forms.css
+++ b/tailbone/static/css/forms.css
@@ -73,6 +73,10 @@ div.field-wrapper div.field {
float: left;
margin-bottom: 5px;
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],
diff --git a/tailbone/templates/email/profiles/view.mako b/tailbone/templates/email/profiles/view.mako
new file mode 100644
index 00000000..4e28aa2b
--- /dev/null
+++ b/tailbone/templates/email/profiles/view.mako
@@ -0,0 +1,30 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/master/view.mako" />
+
+<%def name="head_tags()">
+ ${parent.head_tags()}
+
+%def>
+
+${parent.body()}
+
+
diff --git a/tailbone/templates/forms/form.mako b/tailbone/templates/forms/form.mako
index 60cb633a..6b9f82c1 100644
--- a/tailbone/templates/forms/form.mako
+++ b/tailbone/templates/forms/form.mako
@@ -2,7 +2,7 @@
${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:
${buttons|n}
diff --git a/tailbone/templates/forms/form_readonly.mako b/tailbone/templates/forms/form_readonly.mako
index 0e4a73f8..c8aa0543 100644
--- a/tailbone/templates/forms/form_readonly.mako
+++ b/tailbone/templates/forms/form_readonly.mako
@@ -1,6 +1,6 @@
## -*- coding: utf-8 -*-
- ${form.fieldset.render()|n}
+ ${form.render_fields()|n}
% if buttons:
${buttons|n}
% endif
diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako
index d419d9a8..25520036 100644
--- a/tailbone/templates/master/view.mako
+++ b/tailbone/templates/master/view.mako
@@ -1,7 +1,7 @@
## -*- coding: utf-8 -*-
<%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()">
${h.link_to("Back to {0}".format(model_title_plural), url(route_prefix))}
diff --git a/tailbone/views/email.py b/tailbone/views/email.py
new file mode 100644
index 00000000..06e05ef6
--- /dev/null
+++ b/tailbone/views/email.py
@@ -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 .
+#
+################################################################################
+"""
+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')
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 40990168..8b60099c 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -35,15 +35,18 @@ import formalchemy
from pyramid.renderers import get_renderer, render_to_response
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
+from tailbone import forms
from tailbone.views import View
from tailbone.newgrids import filters, AlchemyGrid, GridAction
-from tailbone.forms import AlchemyForm
class MasterView(View):
"""
Base "master" view class. All model master views should derive from this.
"""
+ filterable = True
+ pageable = True
+
creatable = True
viewable = True
editable = True
@@ -115,7 +118,12 @@ class MasterView(View):
instance = self.get_instance()
form = self.make_form(instance)
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):
"""
@@ -156,13 +164,13 @@ class MasterView(View):
##############################
@classmethod
- def get_model_class(cls):
+ def get_model_class(cls, error=True):
"""
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))
- return cls.model_class
+ return getattr(cls, 'model_class', None)
@classmethod
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
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
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 getattr(cls, 'model_title', cls.model_class.__name__)
+ if hasattr(cls, 'model_title'):
+ return cls.model_title
+ return cls.model_class.__name__
@classmethod
def get_model_title_plural(cls):
@@ -313,11 +325,11 @@ class MasterView(View):
"""
return {
'width': 'full',
- 'filterable': True,
+ 'filterable': self.filterable,
'sortable': True,
'default_sortkey': getattr(self, 'default_sortkey', None),
'sortdir': getattr(self, 'sortdir', 'asc'),
- 'pageable': True,
+ 'pageable': self.pageable,
'main_actions': self.get_main_actions(),
'more_actions': self.get_more_actions(),
'checkbox': self.checkbox,
@@ -383,10 +395,14 @@ class MasterView(View):
"""
Hopefully generic kwarg generator for basic action routes.
"""
- mapper = orm.object_mapper(row)
- keys = [k.key for k in mapper.primary_key]
- values = [getattr(row, k) for k in keys]
- return dict(zip(keys, values))
+ try:
+ 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]
+ values = [getattr(row, k) for k in keys]
+ return dict(zip(keys, values))
def make_grid(self):
"""
@@ -394,10 +410,9 @@ class MasterView(View):
"""
factory = self.get_grid_factory()
key = self.get_grid_key()
- data = self.make_query()
+ data = self.get_data()
kwargs = self.make_grid_kwargs()
- grid = factory(key, self.request, data=data, model_class=self.model_class, **kwargs)
- grid._fa_grid.prettify = prettify
+ grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
self.configure_grid(grid)
grid.load_settings()
return grid
@@ -413,12 +428,17 @@ class MasterView(View):
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()`.
"""
- grid.configure()
+ if hasattr(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
- override this method; override :meth:`query()` instead.
+ Generate the base data set for the grid. This typically will be a
+ 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:
session = self.Session()
@@ -484,7 +504,7 @@ class MasterView(View):
kwargs.setdefault('cancel_url', self.get_index_url())
else:
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
return form