CRUD form overhaul..etc.

This commit is contained in:
Lance Edgar 2012-08-15 15:16:57 -07:00
parent dbd99303c4
commit 5b5b0c8738
14 changed files with 207 additions and 218 deletions

View file

@ -36,8 +36,11 @@ from webhelpers.html.tags import literal
import formalchemy import formalchemy
from formalchemy.validators import accepts_none from formalchemy.validators import accepts_none
import edbob
from edbob.lib import pretty from edbob.lib import pretty
from edbob.pyramid import Session
from edbob.time import localize from edbob.time import localize
from edbob.util import requires_impl
from edbob.pyramid.forms.formalchemy.fieldset import * from edbob.pyramid.forms.formalchemy.fieldset import *
from edbob.pyramid.forms.formalchemy.fields import * from edbob.pyramid.forms.formalchemy.fields import *
@ -65,6 +68,50 @@ engine = TemplateEngine()
formalchemy.config.engine = engine 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): class ChildGridField(formalchemy.Field):
""" """
Convenience class for including a child grid within a fieldset as a Convenience class for including a child grid within a fieldset as a

View file

@ -35,7 +35,7 @@ __all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
'YesNoFieldRenderer'] 'YesNoFieldRenderer']
def AutocompleteFieldRenderer(service_url, width='300px'): def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
""" """
Autocomplete renderer. Autocomplete renderer.
""" """
@ -46,10 +46,14 @@ def AutocompleteFieldRenderer(service_url, width='300px'):
def focus_name(self): def focus_name(self):
return self.name + '-textbox' return self.name + '-textbox'
@property
def needs_focus(self):
return not bool(self.value or field_value)
def render(self, **kwargs): def render(self, **kwargs):
kwargs.setdefault('field_name', self.name) kwargs.setdefault('field_name', self.name)
kwargs.setdefault('field_value', self.value) kwargs.setdefault('field_value', self.value or field_value)
kwargs.setdefault('field_display', self.raw_value) kwargs.setdefault('field_display', self.raw_value or field_display)
kwargs.setdefault('service_url', service_url) kwargs.setdefault('service_url', service_url)
kwargs.setdefault('width', width) kwargs.setdefault('width', width)
return render('/forms/field_autocomplete.mako', kwargs) return render('/forms/field_autocomplete.mako', kwargs)

View file

@ -52,10 +52,17 @@ class AlchemyGrid(Grid):
self._formalchemy_grid = formalchemy.Grid( self._formalchemy_grid = formalchemy.Grid(
cls, instances, session=Session(), request=request) cls, instances, session=Session(), request=request)
self._formalchemy_grid.prettify = prettify self._formalchemy_grid.prettify = prettify
self.noclick_fields = []
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self._formalchemy_grid, 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): def checkbox(self, row):
return tags.checkbox('check-'+row.uuid) return tags.checkbox('check-'+row.uuid)

View file

@ -1,4 +1,13 @@
/******************************
* Form Wrapper
******************************/
div.form-wrapper {
overflow: auto;
}
/****************************** /******************************
* Context Menu * Context Menu
******************************/ ******************************/
@ -19,7 +28,6 @@ div.form,
div.fieldset-form, div.fieldset-form,
div.fieldset { div.fieldset {
float: left; float: left;
margin-left: 50px;
margin-top: 10px; margin-top: 10px;
} }

View file

@ -105,6 +105,7 @@ div.grid table tbody td.delete {
background-position: center; background-position: center;
cursor: pointer; cursor: pointer;
min-width: 18px; min-width: 18px;
width: 18px;
} }
div.grid table tbody tr.hovering { div.grid table tbody tr.hovering {
@ -121,6 +122,10 @@ div.grid table.checkable tbody tr {
cursor: pointer; cursor: pointer;
} }
div.grid.clickable table tbody tr td.noclick {
cursor: default;
}
/* div.grid table.selectable tbody tr.selected, */ /* div.grid table.selectable tbody tr.selected, */
/* div.grid table.checkable tbody tr.selected { */ /* div.grid table.checkable tbody tr.selected { */
/* background-color: #666666; */ /* background-color: #666666; */

View file

@ -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> <%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+str(form.fieldset.model)}</%def>
<%def name="context_menu_items()"></%def> ${parent.body()}
<div class="form-wrapper">
<ul class="context-menu">
${self.context_menu_items()}
</ul>
${fieldset.render()|n}
</div>

View file

@ -1,16 +1,13 @@
<%inherit file="/base.mako" /> <%inherit file="/base.mako" />
<%def name="buttons()"></%def> <%def name="context_menu_items()"></%def>
<div class="wrapper"> <div class="form-wrapper">
<div class="right"> <ul class="context-menu">
${self.menu()} ${self.context_menu_items()}
</div> </ul>
<div class="left"> ${form.render()|n}
<% print 'type (2) is', type(form) %>
${form.render(buttons=self.buttons)|n}
</div>
</div> </div>

View file

@ -1,2 +1,3 @@
<%inherit file="/edbob/form.mako" /> <%inherit file="/edbob/form.mako" />
${parent.body()} ${parent.body()}

View file

@ -1,64 +1,39 @@
<% _focus_rendered = False %> <% _focus_rendered = False %>
<div class="fieldset-form ${class_}"> % for error in fieldset.errors.get(None, []):
${h.form(fieldset.action_url+('?uuid='+fieldset.model.uuid) if fieldset.edit else '', enctype='multipart/form-data')} <div class="fieldset-error">${error}</div>
% endfor
% for error in fieldset.errors.get(None, []): % for field in fieldset.render_fields.itervalues():
<div class="fieldset-error">${error}</div>
% endfor
% for field in fieldset.render_fields.itervalues(): % if field.requires_label:
<div class="field-wrapper ${field.name}">
% if field.requires_label: % for error in field.errors:
<div class="field-wrapper ${field.name}"> <div class="field-error">${error}</div>
% for error in field.errors: % endfor
<div class="field-error">${error}</div> ${field.label_tag()|n}
% endfor <div class="field">
${field.label_tag()|n} ${field.render()|n}
<div class="field">
${field.render()|n}
</div>
% if 'instructions' in field.metadata:
<span class="instructions">${field.metadata['instructions']}</span>
% endif
</div> </div>
% if 'instructions' in field.metadata:
% if not _focus_rendered and (fieldset.focus == field or fieldset.focus is True): <span class="instructions">${field.metadata['instructions']}</span>
% if not field.is_readonly():
<script language="javascript" type="text/javascript">
$(function() {
% if hasattr(field.renderer, 'focus_name'):
$('#${field.renderer.focus_name}').focus();
% else:
$('#${field.renderer.name}').focus();
% endif
});
</script>
<% _focus_rendered = True %>
% endif
% endif % endif
% endif </div>
% 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):
<script language="javascript" type="text/javascript">
$(function() {
% if hasattr(field.renderer, 'focus_name'):
$('#${field.renderer.focus_name}').focus();
% else:
$('#${field.renderer.name}').focus();
% endif
});
</script>
<% _focus_rendered = True %>
% endif
% endif
% endif
% if fieldset.allow_continue: % endfor
<div class="checkbox">
${h.checkbox('add-another', checked=True)}
<label for="add-another">Add another after this one</label>
</div>
% endif
<div class="buttons">
${h.submit('submit', "Save")}
<button type="button" class="cancel">Cancel</button>
</div>
${h.end_form()}
</div>
<script language="javascript" type="text/javascript">
$(function() {
$('button.cancel').click(function() {
location.href = '${fieldset.home_url}';
});
});
</script>

View file

@ -1,62 +1,15 @@
<% _focus_rendered = False %>
<div class="form"> <div class="form">
${h.form(form.action_url, enctype='multipart/form-data')} ${h.form(form.action_url, enctype='multipart/form-data')}
% for error in form.errors.get(None, []): ${form.fieldset.render()|n}
<div class="fieldset-error">${error}</div>
% endfor
% for field in form.render_fields.itervalues():
<div class="field-couple ${field.name}">
% for error in field.errors:
<div class="field-error">${error}</div>
% endfor
${field.label_tag()|n}
<div class="field">
${field.render()|n}
</div>
% if 'instructions' in field.metadata:
<span class="instructions">${field.metadata['instructions']}</span>
% endif
</div>
% if (form.focus == field or form.focus is True) and not _focus_rendered:
% if not field.is_readonly():
<script language="javascript" type="text/javascript">
$(function() {
$('#${field.renderer.name}').focus();
});
</script>
<% _focus_rendered = True %>
% endif
% endif
% endfor
% if form.successive:
<div class="checkbox">
${h.checkbox('keep-going', checked=True)}
<label for="keep-going">Add another after this one</label>
</div>
% endif
<div class="buttons"> <div class="buttons">
% if buttons: ${h.submit('create', form.create_label if form.creating else form.update_label)}
${capture(buttons)} % if form.creating and form.allow_successive_creates:
% else: ${h.submit('create_and_continue', form.successive_create_label)}
${h.submit('submit', "Save")}
<button type="button" class="cancel">Cancel</button>
% endif % endif
<button type="button" onclick="location.href = '${form.cancel_url}';">Cancel</button>
</div> </div>
${h.end_form()} ${h.end_form()}
</div> </div>
<script language="javascript" type="text/javascript">
$(function() {
$('button.cancel').click(function() {
location.href = '${form.home_url}';
});
});
</script>

View file

@ -0,0 +1,3 @@
<div class="form">
${form.fieldset.render()|n}
</div>

View file

@ -23,7 +23,7 @@
<td class="checkbox">${grid.checkbox(row)}</td> <td class="checkbox">${grid.checkbox(row)}</td>
% endif % endif
% for field in grid.iter_fields(): % for field in grid.iter_fields():
<td class="${field.name}">${grid.render_field(field)}</td> <td class="${grid.cell_class(field)}">${grid.render_field(field)}</td>
% endfor % endfor
% for col in grid.extra_columns: % for col in grid.extra_columns:
<td class="noclick ${col.name}">${col.callback(row)}</td> <td class="noclick ${col.name}">${col.callback(row)}</td>

View file

@ -33,9 +33,10 @@ from webhelpers.html import literal
from webhelpers.html.tags import link_to from webhelpers.html.tags import link_to
from edbob.pyramid.views.core import * 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.grids import *
from edbob.pyramid.views.crud import *
from edbob.pyramid.views.autocomplete import *
# from edbob.pyramid.views.form import *
def forbidden(request): def forbidden(request):

View file

@ -28,22 +28,20 @@
from pyramid.httpexceptions import HTTPFound from pyramid.httpexceptions import HTTPFound
import formalchemy
from edbob.pyramid import Session 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 from edbob.util import requires_impl
class Crud(object): __all__ = ['CrudView']
routes = ['create', 'read', 'update', 'delete']
route_prefix = None class CrudView(View):
url_prefix = None
template_prefix = None
permission_prefix = None
def __init__(self, request): allow_successive_creates = False
self.request = request
@property @property
@requires_impl(is_property=True) @requires_impl(is_property=True)
@ -51,7 +49,7 @@ class Crud(object):
pass pass
@property @property
def crud_title(self): def pretty_name(self):
return self.mapped_class.__name__ return self.mapped_class.__name__
@property @property
@ -65,64 +63,96 @@ class Crud(object):
@property @property
def cancel_route(self): 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): 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()) kwargs.setdefault('session', Session())
fs = FieldSet(model, **kwargs) fieldset = formalchemy.FieldSet(model, **kwargs)
fs.create = model is self.mapped_class return fieldset
fs.update = not fs.create
return fs
def fieldset(self, obj): def fieldset(self, model):
return self.make_fieldset(obj) return self.make_fieldset(model)
def post_sync(self, fs): def make_form(self, model, **kwargs):
pass self.creating = model is self.mapped_class
self.updating = not self.creating
def validation_failed(self, fs): fieldset = self.fieldset(model)
pass 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): def crud(self, model, readonly=False):
fs = self.fieldset(model) form = self.form(model)
if readonly: if readonly:
fs.readonly = True form.readonly = True
if not fs.readonly and self.request.POST:
fs.rebind(data=self.request.params)
if fs.validate():
fs.sync() if not form.readonly and self.request.POST:
Session.add(fs.model) if form.validate():
Session.flush() form.save()
result = self.post_sync(fs) result = self.post_save(form)
if result: if result:
return result return result
# Session.add(fs.model) if form.creating:
# Session.flush() self.flash_create(form.fieldset.model)
if fs.create:
self.flash_create(fs.model)
else: 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.request.current_route_url())
return HTTPFound(location=self.home_url) return HTTPFound(location=self.home_url)
self.validation_failed(fs) self.validation_failed(form)
if not fs.edit: kwargs = self.template_kwargs(form)
fs.allow_continue = True 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): def create(self):
return self.crud(self.mapped_class) return self.crud(self.mapped_class)
@ -139,6 +169,9 @@ class Crud(object):
assert model assert model
return self.crud(model) return self.crud(model)
def pre_delete(self, model):
pass
def delete(self): def delete(self):
uuid = self.request.matchdict['uuid'] uuid = self.request.matchdict['uuid']
model = Session.query(self.mapped_class).get(uuid) if uuid else None 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. Session.flush() # Don't set flash message if delete fails.
self.flash_delete(model) self.flash_delete(model)
return HTTPFound(location=self.home_url) 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)