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
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

View file

@ -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)

View file

@ -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)

View file

@ -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;
}

View file

@ -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; */

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>
<div class="form-wrapper">
<ul class="context-menu">
${self.context_menu_items()}
</ul>
${fieldset.render()|n}
</div>
${parent.body()}

View file

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

View file

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

View file

@ -1,64 +1,39 @@
<% _focus_rendered = False %>
<div class="fieldset-form ${class_}">
${h.form(fieldset.action_url+('?uuid='+fieldset.model.uuid) if fieldset.edit else '', enctype='multipart/form-data')}
% for error in fieldset.errors.get(None, []):
<div class="fieldset-error">${error}</div>
% endfor
% for error in fieldset.errors.get(None, []):
<div class="fieldset-error">${error}</div>
% endfor
% for field in fieldset.render_fields.itervalues():
% for field in fieldset.render_fields.itervalues():
% if field.requires_label:
<div class="field-wrapper ${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
% if field.requires_label:
<div class="field-wrapper ${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 not _focus_rendered and (fieldset.focus == field or fieldset.focus is True):
% 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
% if 'instructions' in field.metadata:
<span class="instructions">${field.metadata['instructions']}</span>
% 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:
<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>
% endfor

View file

@ -1,62 +1,15 @@
<% _focus_rendered = False %>
<div class="form">
${h.form(form.action_url, enctype='multipart/form-data')}
% for error in form.errors.get(None, []):
<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
${form.fieldset.render()|n}
<div class="buttons">
% if buttons:
${capture(buttons)}
% else:
${h.submit('submit', "Save")}
<button type="button" class="cancel">Cancel</button>
${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
<button type="button" onclick="location.href = '${form.cancel_url}';">Cancel</button>
</div>
${h.end_form()}
</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>
% endif
% 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
% for col in grid.extra_columns:
<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 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):

View file

@ -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)