From 5b5b0c87385b23b4dd6fec7887b3cedd8218de13 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 15 Aug 2012 15:16:57 -0700 Subject: [PATCH] CRUD form overhaul..etc. --- edbob/pyramid/forms/formalchemy/__init__.py | 47 ++++++ edbob/pyramid/forms/formalchemy/renderers.py | 10 +- edbob/pyramid/grids/alchemy.py | 7 + edbob/pyramid/static/css/forms.css | 10 +- edbob/pyramid/static/css/grids.css | 5 + edbob/pyramid/templates/edbob/crud.mako | 16 +- edbob/pyramid/templates/edbob/form.mako | 15 +- edbob/pyramid/templates/form.mako | 1 + edbob/pyramid/templates/forms/fieldset.mako | 87 ++++------ edbob/pyramid/templates/forms/form.mako | 59 +------ .../templates/forms/form_readonly.mako | 3 + edbob/pyramid/templates/grids/grid.mako | 2 +- edbob/pyramid/views/__init__.py | 5 +- edbob/pyramid/views/crud.py | 158 +++++++++--------- 14 files changed, 207 insertions(+), 218 deletions(-) create mode 100644 edbob/pyramid/templates/forms/form_readonly.mako diff --git a/edbob/pyramid/forms/formalchemy/__init__.py b/edbob/pyramid/forms/formalchemy/__init__.py index 6ab93d5..9789988 100644 --- a/edbob/pyramid/forms/formalchemy/__init__.py +++ b/edbob/pyramid/forms/formalchemy/__init__.py @@ -36,8 +36,11 @@ from webhelpers.html.tags import literal import formalchemy from formalchemy.validators import accepts_none +import edbob from edbob.lib import pretty +from edbob.pyramid import Session from edbob.time import localize +from edbob.util import requires_impl from edbob.pyramid.forms.formalchemy.fieldset import * from edbob.pyramid.forms.formalchemy.fields import * @@ -65,6 +68,50 @@ engine = TemplateEngine() formalchemy.config.engine = engine +class AlchemyForm(edbob.Object): + """ + Form to contain a :class:`formalchemy.FieldSet` instance. + """ + + create_label = "Create" + update_label = "Update" + + allow_successive_creates = False + + def __init__(self, request, fieldset, **kwargs): + edbob.Object.__init__(self, **kwargs) + self.request = request + self.fieldset = fieldset + + def _get_readonly(self): + return self.fieldset.readonly + + def _set_readonly(self, val): + self.fieldset.readonly = val + + readonly = property(_get_readonly, _set_readonly) + + @property + def successive_create_label(self): + return "%s and continue" % self.create_label + + def render(self, **kwargs): + kwargs['form'] = self + if self.readonly: + template = '/forms/form_readonly.mako' + else: + template = '/forms/form.mako' + return render(template, kwargs) + + def save(self): + self.fieldset.sync() + Session.flush() + + def validate(self): + self.fieldset.rebind(data=self.request.params) + return self.fieldset.validate() + + class ChildGridField(formalchemy.Field): """ Convenience class for including a child grid within a fieldset as a diff --git a/edbob/pyramid/forms/formalchemy/renderers.py b/edbob/pyramid/forms/formalchemy/renderers.py index 783d323..263f728 100644 --- a/edbob/pyramid/forms/formalchemy/renderers.py +++ b/edbob/pyramid/forms/formalchemy/renderers.py @@ -35,7 +35,7 @@ __all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', 'YesNoFieldRenderer'] -def AutocompleteFieldRenderer(service_url, width='300px'): +def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'): """ Autocomplete renderer. """ @@ -46,10 +46,14 @@ def AutocompleteFieldRenderer(service_url, width='300px'): def focus_name(self): return self.name + '-textbox' + @property + def needs_focus(self): + return not bool(self.value or field_value) + def render(self, **kwargs): kwargs.setdefault('field_name', self.name) - kwargs.setdefault('field_value', self.value) - kwargs.setdefault('field_display', self.raw_value) + kwargs.setdefault('field_value', self.value or field_value) + kwargs.setdefault('field_display', self.raw_value or field_display) kwargs.setdefault('service_url', service_url) kwargs.setdefault('width', width) return render('/forms/field_autocomplete.mako', kwargs) diff --git a/edbob/pyramid/grids/alchemy.py b/edbob/pyramid/grids/alchemy.py index be84d0a..eb3b7d7 100644 --- a/edbob/pyramid/grids/alchemy.py +++ b/edbob/pyramid/grids/alchemy.py @@ -52,10 +52,17 @@ class AlchemyGrid(Grid): self._formalchemy_grid = formalchemy.Grid( cls, instances, session=Session(), request=request) self._formalchemy_grid.prettify = prettify + self.noclick_fields = [] def __getattr__(self, attr): return getattr(self._formalchemy_grid, attr) + def cell_class(self, field): + classes = [field.name] + if field.name in self.noclick_fields: + classes.append('noclick') + return ' '.join(classes) + def checkbox(self, row): return tags.checkbox('check-'+row.uuid) diff --git a/edbob/pyramid/static/css/forms.css b/edbob/pyramid/static/css/forms.css index 5843669..086f882 100644 --- a/edbob/pyramid/static/css/forms.css +++ b/edbob/pyramid/static/css/forms.css @@ -1,4 +1,13 @@ +/****************************** + * Form Wrapper + ******************************/ + +div.form-wrapper { + overflow: auto; +} + + /****************************** * Context Menu ******************************/ @@ -19,7 +28,6 @@ div.form, div.fieldset-form, div.fieldset { float: left; - margin-left: 50px; margin-top: 10px; } diff --git a/edbob/pyramid/static/css/grids.css b/edbob/pyramid/static/css/grids.css index 209c55f..9ceeb0c 100644 --- a/edbob/pyramid/static/css/grids.css +++ b/edbob/pyramid/static/css/grids.css @@ -105,6 +105,7 @@ div.grid table tbody td.delete { background-position: center; cursor: pointer; min-width: 18px; + width: 18px; } div.grid table tbody tr.hovering { @@ -121,6 +122,10 @@ div.grid table.checkable tbody tr { cursor: pointer; } +div.grid.clickable table tbody tr td.noclick { + cursor: default; +} + /* div.grid table.selectable tbody tr.selected, */ /* div.grid table.checkable tbody tr.selected { */ /* background-color: #666666; */ diff --git a/edbob/pyramid/templates/edbob/crud.mako b/edbob/pyramid/templates/edbob/crud.mako index c874825..885dba3 100644 --- a/edbob/pyramid/templates/edbob/crud.mako +++ b/edbob/pyramid/templates/edbob/crud.mako @@ -1,15 +1,5 @@ -<%inherit file="/base.mako" /> +<%inherit file="/form.mako" /> -<%def name="title()">${(fieldset.crud_title+' : '+fieldset.get_display_text() if fieldset.edit else 'New '+fieldset.crud_title) if crud else ''|n} +<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+str(form.fieldset.model)} -<%def name="context_menu_items()"> - -
- - - - ${fieldset.render()|n} - -
+${parent.body()} diff --git a/edbob/pyramid/templates/edbob/form.mako b/edbob/pyramid/templates/edbob/form.mako index ad9e14a..095d638 100644 --- a/edbob/pyramid/templates/edbob/form.mako +++ b/edbob/pyramid/templates/edbob/form.mako @@ -1,16 +1,13 @@ <%inherit file="/base.mako" /> -<%def name="buttons()"> +<%def name="context_menu_items()"> -
+
-
- ${self.menu()} -
+
    + ${self.context_menu_items()} +
-
- <% print 'type (2) is', type(form) %> - ${form.render(buttons=self.buttons)|n} -
+ ${form.render()|n}
diff --git a/edbob/pyramid/templates/form.mako b/edbob/pyramid/templates/form.mako index cae0447..7298619 100644 --- a/edbob/pyramid/templates/form.mako +++ b/edbob/pyramid/templates/form.mako @@ -1,2 +1,3 @@ <%inherit file="/edbob/form.mako" /> + ${parent.body()} diff --git a/edbob/pyramid/templates/forms/fieldset.mako b/edbob/pyramid/templates/forms/fieldset.mako index 7af621b..0e5a6d4 100644 --- a/edbob/pyramid/templates/forms/fieldset.mako +++ b/edbob/pyramid/templates/forms/fieldset.mako @@ -1,64 +1,39 @@ <% _focus_rendered = False %> -
- ${h.form(fieldset.action_url+('?uuid='+fieldset.model.uuid) if fieldset.edit else '', enctype='multipart/form-data')} +% for error in fieldset.errors.get(None, []): +
${error}
+% endfor - % for error in fieldset.errors.get(None, []): -
${error}
- % endfor +% for field in fieldset.render_fields.itervalues(): - % for field in fieldset.render_fields.itervalues(): - - % if field.requires_label: -
- % for error in field.errors: -
${error}
- % endfor - ${field.label_tag()|n} -
- ${field.render()|n} -
- % if 'instructions' in field.metadata: - ${field.metadata['instructions']} - % endif + % if field.requires_label: +
+ % for error in field.errors: +
${error}
+ % endfor + ${field.label_tag()|n} +
+ ${field.render()|n}
- - % if not _focus_rendered and (fieldset.focus == field or fieldset.focus is True): - % if not field.is_readonly(): - - <% _focus_rendered = True %> - % endif + % if 'instructions' in field.metadata: + ${field.metadata['instructions']} % endif - % endif +
- % endfor + % if not _focus_rendered and (fieldset.focus == field or fieldset.focus is True): + % if not field.is_readonly() and getattr(field.renderer, 'needs_focus', True): + + <% _focus_rendered = True %> + % endif + % endif + % endif - % if fieldset.allow_continue: -
- ${h.checkbox('add-another', checked=True)} - -
- % endif - -
- ${h.submit('submit', "Save")} - -
- ${h.end_form()} -
- - +% endfor diff --git a/edbob/pyramid/templates/forms/form.mako b/edbob/pyramid/templates/forms/form.mako index 63b7d50..ea25d01 100644 --- a/edbob/pyramid/templates/forms/form.mako +++ b/edbob/pyramid/templates/forms/form.mako @@ -1,62 +1,15 @@ -<% _focus_rendered = False %> -
${h.form(form.action_url, enctype='multipart/form-data')} - % for error in form.errors.get(None, []): -
${error}
- % endfor - - % for field in form.render_fields.itervalues(): - -
- % for error in field.errors: -
${error}
- % endfor - ${field.label_tag()|n} -
- ${field.render()|n} -
- % if 'instructions' in field.metadata: - ${field.metadata['instructions']} - % endif -
- - % if (form.focus == field or form.focus is True) and not _focus_rendered: - % if not field.is_readonly(): - - <% _focus_rendered = True %> - % endif - % endif - - % endfor - - % if form.successive: -
- ${h.checkbox('keep-going', checked=True)} - -
- % endif + ${form.fieldset.render()|n}
- % if buttons: - ${capture(buttons)} - % else: - ${h.submit('submit', "Save")} - + ${h.submit('create', form.create_label if form.creating else form.update_label)} + % if form.creating and form.allow_successive_creates: + ${h.submit('create_and_continue', form.successive_create_label)} % endif +
+ ${h.end_form()}
- - diff --git a/edbob/pyramid/templates/forms/form_readonly.mako b/edbob/pyramid/templates/forms/form_readonly.mako new file mode 100644 index 0000000..9692042 --- /dev/null +++ b/edbob/pyramid/templates/forms/form_readonly.mako @@ -0,0 +1,3 @@ +
+ ${form.fieldset.render()|n} +
diff --git a/edbob/pyramid/templates/grids/grid.mako b/edbob/pyramid/templates/grids/grid.mako index 3bbc9ad..d0767b6 100644 --- a/edbob/pyramid/templates/grids/grid.mako +++ b/edbob/pyramid/templates/grids/grid.mako @@ -23,7 +23,7 @@ ${grid.checkbox(row)} % endif % for field in grid.iter_fields(): - ${grid.render_field(field)} + ${grid.render_field(field)} % endfor % for col in grid.extra_columns: ${col.callback(row)} diff --git a/edbob/pyramid/views/__init__.py b/edbob/pyramid/views/__init__.py index f51f74c..d2b4847 100644 --- a/edbob/pyramid/views/__init__.py +++ b/edbob/pyramid/views/__init__.py @@ -33,9 +33,10 @@ from webhelpers.html import literal from webhelpers.html.tags import link_to from edbob.pyramid.views.core import * -from edbob.pyramid.views.autocomplete import * -from edbob.pyramid.views.form import * from edbob.pyramid.views.grids import * +from edbob.pyramid.views.crud import * +from edbob.pyramid.views.autocomplete import * +# from edbob.pyramid.views.form import * def forbidden(request): diff --git a/edbob/pyramid/views/crud.py b/edbob/pyramid/views/crud.py index c51c4b8..8078cab 100644 --- a/edbob/pyramid/views/crud.py +++ b/edbob/pyramid/views/crud.py @@ -28,22 +28,20 @@ from pyramid.httpexceptions import HTTPFound +import formalchemy + from edbob.pyramid import Session -from edbob.pyramid.forms.formalchemy.fieldset import FieldSet +from edbob.pyramid.forms.formalchemy import AlchemyForm +from edbob.pyramid.views.core import View from edbob.util import requires_impl -class Crud(object): +__all__ = ['CrudView'] - routes = ['create', 'read', 'update', 'delete'] - route_prefix = None - url_prefix = None - template_prefix = None - permission_prefix = None +class CrudView(View): - def __init__(self, request): - self.request = request + allow_successive_creates = False @property @requires_impl(is_property=True) @@ -51,7 +49,7 @@ class Crud(object): pass @property - def crud_title(self): + def pretty_name(self): return self.mapped_class.__name__ @property @@ -65,64 +63,96 @@ class Crud(object): @property def cancel_route(self): - return None + return self.home_route + + @property + def cancel_url(self): + return self.request.route_url(self.cancel_route) def make_fieldset(self, model, **kwargs): - if 'action_url' not in kwargs: - kwargs['action_url'] = self.request.current_route_url() - if 'home_url' not in kwargs: - kwargs['home_url'] = self.home_url - kwargs.setdefault('session', Session()) - fs = FieldSet(model, **kwargs) - fs.create = model is self.mapped_class - fs.update = not fs.create - return fs + fieldset = formalchemy.FieldSet(model, **kwargs) + return fieldset - def fieldset(self, obj): - return self.make_fieldset(obj) + def fieldset(self, model): + return self.make_fieldset(model) - def post_sync(self, fs): - pass + def make_form(self, model, **kwargs): + self.creating = model is self.mapped_class + self.updating = not self.creating - def validation_failed(self, fs): - pass + fieldset = self.fieldset(model) + kwargs.setdefault('pretty_name', self.pretty_name) + kwargs.setdefault('action_url', self.request.current_route_url()) + kwargs.setdefault('cancel_url', self.cancel_url) + kwargs.setdefault('creating', self.creating) + kwargs.setdefault('updating', self.updating) + form = AlchemyForm(self.request, fieldset, **kwargs) + + if form.creating: + if hasattr(self, 'create_label'): + form.create_label = self.create_label + if self.allow_successive_creates: + form.allow_successive_creates = True + if hasattr(self, 'successive_create_label'): + form.successive_create_label = self.successive_create_label + + return form + + def form(self, model): + return self.make_form(model) def crud(self, model, readonly=False): - fs = self.fieldset(model) + form = self.form(model) if readonly: - fs.readonly = True - if not fs.readonly and self.request.POST: - fs.rebind(data=self.request.params) - if fs.validate(): + form.readonly = True - fs.sync() - Session.add(fs.model) - Session.flush() + if not form.readonly and self.request.POST: + if form.validate(): + form.save() - result = self.post_sync(fs) + result = self.post_save(form) if result: return result - # Session.add(fs.model) - # Session.flush() - if fs.create: - self.flash_create(fs.model) + if form.creating: + self.flash_create(form.fieldset.model) else: - self.flash_update(fs.model) + self.flash_update(form.fieldset.model) - if self.request.params.get('add-another') == '1': + if (form.creating and form.allow_successive_creates + and self.request.params.get('create_and_continue')): return HTTPFound(location=self.request.current_route_url()) return HTTPFound(location=self.home_url) - self.validation_failed(fs) + self.validation_failed(form) - if not fs.edit: - fs.allow_continue = True + kwargs = self.template_kwargs(form) + kwargs['form'] = form + return kwargs - return {'fieldset': fs, 'crud': True} + def template_kwargs(self, form): + return {} + + def post_save(self, form): + pass + + def validation_failed(self, form): + pass + + def flash_create(self, model): + self.request.session.flash("%s \"%s\" has been created." % + (self.pretty_name, model)) + + def flash_delete(self, model): + self.request.session.flash("%s \"%s\" has been deleted." % + (self.pretty_name, model)) + + def flash_update(self, model): + self.request.session.flash("%s \"%s\" has been updated." % + (self.pretty_name, model)) def create(self): return self.crud(self.mapped_class) @@ -139,6 +169,9 @@ class Crud(object): assert model return self.crud(model) + def pre_delete(self, model): + pass + def delete(self): uuid = self.request.matchdict['uuid'] model = Session.query(self.mapped_class).get(uuid) if uuid else None @@ -150,38 +183,3 @@ class Crud(object): Session.flush() # Don't set flash message if delete fails. self.flash_delete(model) return HTTPFound(location=self.home_url) - - def flash_create(self, model): - self.request.session.flash("%s \"%s\" has been created." % - (self.crud_title, model)) - - def flash_delete(self, model): - self.request.session.flash("%s \"%s\" has been deleted." % - (self.crud_title, model)) - - def flash_update(self, model): - self.request.session.flash("%s \"%s\" has been updated." % - (self.crud_title, model)) - - def pre_delete(self, model): - pass - - @classmethod - def add_routes(cls, config): - route_name_prefix = cls.route_prefix or cls.mapped_class.__name__.lower() - route_url_prefix = cls.url_prefix or '/%ss' % route_name_prefix - renderer_prefix = cls.template_prefix or route_url_prefix - permission_prefix = cls.permission_prefix or '%ss' % route_name_prefix - - for route in cls.routes: - kw = dict( - attr=route, - route_name='%s.%s' % (route_name_prefix, route), - renderer='%s/%s.mako' % (renderer_prefix, route), - permission='%s.%s' % (permission_prefix, route), - ) - if route == 'create': - config.add_route(kw['route_name'], '%s/new' % route_url_prefix) - else: - config.add_route(kw['route_name'], '%s/{uuid}/%s' % (route_url_prefix, route)) - config.add_view(cls, http_cache=0, **kw)