CRUD form overhaul..etc.
This commit is contained in:
parent
dbd99303c4
commit
5b5b0c8738
14 changed files with 207 additions and 218 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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; */
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
<%inherit file="/edbob/form.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
3
edbob/pyramid/templates/forms/form_readonly.mako
Normal file
3
edbob/pyramid/templates/forms/form_readonly.mako
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="form">
|
||||
${form.fieldset.render()|n}
|
||||
</div>
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue