feat: add basic Create support for CRUD master view
This commit is contained in:
parent
c46b42f76d
commit
73014964cb
10 changed files with 307 additions and 45 deletions
|
@ -159,6 +159,18 @@ class Form:
|
|||
|
||||
See also :meth:`set_readonly()` and :meth:`is_readonly()`.
|
||||
|
||||
.. attribute:: required_fields
|
||||
|
||||
A dict of "required" field flags. Keys are field names, and
|
||||
values are boolean flags indicating whether the field is
|
||||
required.
|
||||
|
||||
Depending on :attr:`schema`, some fields may be "(not)
|
||||
required" by default. However ``required_fields`` keeps track
|
||||
of any "overrides" per field.
|
||||
|
||||
See also :meth:`set_required()` and :meth:`is_required()`.
|
||||
|
||||
.. attribute:: action_url
|
||||
|
||||
String URL to which the form should be submitted, if applicable.
|
||||
|
@ -256,6 +268,7 @@ class Form:
|
|||
model_instance=None,
|
||||
readonly=False,
|
||||
readonly_fields=[],
|
||||
required_fields={},
|
||||
labels={},
|
||||
action_url=None,
|
||||
cancel_url=None,
|
||||
|
@ -275,6 +288,7 @@ class Form:
|
|||
self.schema = schema
|
||||
self.readonly = readonly
|
||||
self.readonly_fields = set(readonly_fields or [])
|
||||
self.required_fields = required_fields or {}
|
||||
self.labels = labels or {}
|
||||
self.action_url = action_url
|
||||
self.cancel_url = cancel_url
|
||||
|
@ -413,6 +427,41 @@ class Form:
|
|||
return True
|
||||
return False
|
||||
|
||||
def set_required(self, key, required=True):
|
||||
"""
|
||||
Enable or disable the "required" flag for a given field.
|
||||
|
||||
When a field is marked required, a value must be provided
|
||||
or else it fails validation.
|
||||
|
||||
In practice if a field is "not required" then a default
|
||||
"empty" value is assumed, should the user not provide one.
|
||||
|
||||
See also :meth:`is_required()`; this is tracked via
|
||||
:attr:`required_fields`.
|
||||
|
||||
:param key: String key (fieldname) for the field.
|
||||
|
||||
:param required: New required flag for the field. Usually a
|
||||
boolean, but may also be ``None`` to remove any flag and
|
||||
revert to default behavior for the field.
|
||||
"""
|
||||
self.required_fields[key] = required
|
||||
|
||||
def is_required(self, key):
|
||||
"""
|
||||
Returns boolean indicating if the given field is marked as
|
||||
required.
|
||||
|
||||
See also :meth:`set_required()`.
|
||||
|
||||
:param key: Field key/name as string.
|
||||
|
||||
:returns: Value for the flag from :attr:`required_fields` if
|
||||
present; otherwise ``None``.
|
||||
"""
|
||||
return self.required_fields.get(key, None)
|
||||
|
||||
def set_label(self, key, label):
|
||||
"""
|
||||
Set the label for given field name.
|
||||
|
@ -452,6 +501,15 @@ class Form:
|
|||
schema.add(colander.SchemaNode(
|
||||
colander.String(),
|
||||
name=name))
|
||||
|
||||
# apply required flags
|
||||
for key, required in self.required_fields.items():
|
||||
if key in schema:
|
||||
if required is False:
|
||||
# TODO: (why) should we not use colander.null here?
|
||||
#schema[key].missing = colander.null
|
||||
schema[key].missing = None
|
||||
|
||||
self.schema = schema
|
||||
|
||||
else: # no fields
|
||||
|
|
|
@ -362,7 +362,11 @@ class GridAction:
|
|||
Default logic returns the output from :meth:`render_icon()`
|
||||
and :meth:`render_label()`.
|
||||
"""
|
||||
return self.render_icon() + ' ' + self.render_label()
|
||||
html = [
|
||||
self.render_icon(),
|
||||
self.render_label(),
|
||||
]
|
||||
return HTML.literal(' ').join(html)
|
||||
|
||||
def render_icon(self):
|
||||
"""
|
||||
|
|
|
@ -214,13 +214,20 @@
|
|||
<div class="level-left">
|
||||
|
||||
## Current Context
|
||||
<div id="current-context" class="level-item">
|
||||
<div id="current-context" class="level-item"
|
||||
style="display: flex; gap: 1.5rem;">
|
||||
% if index_title:
|
||||
% if index_url:
|
||||
<h1 class="title">${h.link_to(index_title, index_url)}</h1>
|
||||
% else:
|
||||
<h1 class="title">${index_title}</h1>
|
||||
% endif
|
||||
% if master and master.creatable and not master.creating:
|
||||
<wutta-button once type="is-primary"
|
||||
tag="a" href="${url(f'{route_prefix}.create')}"
|
||||
icon-left="plus"
|
||||
label="Create New" />
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
|
||||
|
|
|
@ -54,15 +54,19 @@
|
|||
|
||||
let ${form.vue_component}Data = {
|
||||
|
||||
## field model values
|
||||
% for key in form:
|
||||
% if key in dform:
|
||||
model_${key}: ${form.get_vue_field_value(key)|n},
|
||||
% endif
|
||||
% endfor
|
||||
% if not form.readonly:
|
||||
|
||||
## field model values
|
||||
% for key in form:
|
||||
% if key in dform:
|
||||
model_${key}: ${form.get_vue_field_value(key)|n},
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
% if form.auto_disable_submit:
|
||||
formSubmitting: false,
|
||||
% endif
|
||||
|
||||
% if form.auto_disable_submit:
|
||||
formSubmitting: false,
|
||||
% endif
|
||||
}
|
||||
|
||||
|
|
7
src/wuttaweb/templates/master/create.mako
Normal file
7
src/wuttaweb/templates/master/create.mako
Normal file
|
@ -0,0 +1,7 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/form.mako" />
|
||||
|
||||
<%def name="title()">New ${model_title}</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -168,6 +168,12 @@ class MasterView(View):
|
|||
|
||||
This is optional; see also :meth:`index_get_grid_columns()`.
|
||||
|
||||
.. attribute:: creatable
|
||||
|
||||
Boolean indicating whether the view model supports "creating" -
|
||||
i.e. it should have a :meth:`create()` view. Default value is
|
||||
``True``.
|
||||
|
||||
.. attribute:: viewable
|
||||
|
||||
Boolean indicating whether the view model supports "viewing" -
|
||||
|
@ -206,6 +212,7 @@ class MasterView(View):
|
|||
# features
|
||||
listable = True
|
||||
has_grid = True
|
||||
creatable = True
|
||||
viewable = True
|
||||
editable = True
|
||||
deletable = True
|
||||
|
@ -213,6 +220,7 @@ class MasterView(View):
|
|||
|
||||
# current action
|
||||
listing = False
|
||||
creating = False
|
||||
viewing = False
|
||||
editing = False
|
||||
deleting = False
|
||||
|
@ -335,6 +343,62 @@ class MasterView(View):
|
|||
"""
|
||||
return []
|
||||
|
||||
##############################
|
||||
# create methods
|
||||
##############################
|
||||
|
||||
def create(self):
|
||||
"""
|
||||
View to "create" a new model record.
|
||||
|
||||
This usually corresponds to a URL like ``/widgets/new``.
|
||||
|
||||
By default, this view is included only if :attr:`creatable` is
|
||||
true.
|
||||
|
||||
The default "create" view logic will show a form with field
|
||||
widgets, allowing user to submit new values which are then
|
||||
persisted to the DB (assuming typical SQLAlchemy model).
|
||||
|
||||
Subclass normally should not override this method, but rather
|
||||
one of the related methods which are called (in)directly by
|
||||
this one:
|
||||
|
||||
* :meth:`make_model_form()`
|
||||
* :meth:`configure_form()`
|
||||
* :meth:`create_save_form()`
|
||||
"""
|
||||
self.creating = True
|
||||
form = self.make_model_form(cancel_url_fallback=self.get_index_url())
|
||||
|
||||
if form.validate():
|
||||
obj = self.create_save_form(form)
|
||||
return self.redirect(self.get_action_url('view', obj))
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
}
|
||||
return self.render_to_response('create', context)
|
||||
|
||||
def create_save_form(self, form):
|
||||
"""
|
||||
This method is responsible for "converting" the validated form
|
||||
data to a model instance, and then "saving" the result,
|
||||
e.g. to DB. It is called by :meth:`create()`.
|
||||
|
||||
Subclass may override this, or any of the related methods
|
||||
called by this one:
|
||||
|
||||
* :meth:`objectify()`
|
||||
* :meth:`persist()`
|
||||
|
||||
:returns: Should return the resulting model instance, e.g. as
|
||||
produced by :meth:`objectify()`.
|
||||
"""
|
||||
obj = self.objectify(form)
|
||||
self.persist(obj)
|
||||
return obj
|
||||
|
||||
##############################
|
||||
# view methods
|
||||
##############################
|
||||
|
@ -419,7 +483,7 @@ class MasterView(View):
|
|||
"""
|
||||
This method is responsible for "converting" the validated form
|
||||
data to a model instance, and then "saving" the result,
|
||||
e.g. to DB.
|
||||
e.g. to DB. It is called by :meth:`edit()`.
|
||||
|
||||
Subclass may override this, or any of the related methods
|
||||
called by this one:
|
||||
|
@ -427,8 +491,8 @@ class MasterView(View):
|
|||
* :meth:`objectify()`
|
||||
* :meth:`persist()`
|
||||
|
||||
:returns: This should return the resulting model instance,
|
||||
which was produced by :meth:`objectify()`.
|
||||
:returns: Should return the resulting model instance, e.g. as
|
||||
produced by :meth:`objectify()`.
|
||||
"""
|
||||
obj = self.objectify(form)
|
||||
self.persist(obj)
|
||||
|
@ -1395,6 +1459,13 @@ class MasterView(View):
|
|||
config.add_view(cls, attr='index',
|
||||
route_name=route_prefix)
|
||||
|
||||
# create
|
||||
if cls.creatable:
|
||||
config.add_route(f'{route_prefix}.create',
|
||||
f'{url_prefix}/new')
|
||||
config.add_view(cls, attr='create',
|
||||
route_name=f'{route_prefix}.create')
|
||||
|
||||
# view
|
||||
if cls.viewable:
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
|
|
|
@ -26,8 +26,9 @@ Views for app settings
|
|||
|
||||
from collections import OrderedDict
|
||||
|
||||
from wuttjamaican.db.model import Setting
|
||||
import colander
|
||||
|
||||
from wuttjamaican.db.model import Setting
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.util import get_libver, get_liburl
|
||||
from wuttaweb.db import Session
|
||||
|
@ -48,6 +49,7 @@ class AppInfoView(MasterView):
|
|||
model_title_plural = "App Info"
|
||||
route_prefix = 'appinfo'
|
||||
has_grid = False
|
||||
creatable = False
|
||||
viewable = False
|
||||
editable = False
|
||||
deletable = False
|
||||
|
@ -177,6 +179,10 @@ class SettingView(MasterView):
|
|||
""" """
|
||||
return {
|
||||
'name': setting.name,
|
||||
# TODO: when viewing the record, 'None' is displayed for null
|
||||
# field values. not so if we return colander.null here, but
|
||||
# then that causes other problems..
|
||||
#'value': setting.value if setting.value is not None else colander.null,
|
||||
'value': setting.value,
|
||||
}
|
||||
|
||||
|
@ -188,7 +194,10 @@ class SettingView(MasterView):
|
|||
name = self.request.matchdict['name']
|
||||
setting = session.query(model.Setting).get(name)
|
||||
if setting:
|
||||
return self.normalize_setting(setting)
|
||||
setting = self.normalize_setting(setting)
|
||||
if setting['value'] is None:
|
||||
setting['value'] = colander.null
|
||||
return setting
|
||||
|
||||
return self.notfound()
|
||||
|
||||
|
@ -197,10 +206,19 @@ class SettingView(MasterView):
|
|||
""" """
|
||||
return setting['name']
|
||||
|
||||
# TODO: master should handle this
|
||||
def configure_form(self, f):
|
||||
super().configure_form(f)
|
||||
|
||||
f.set_required('value', False)
|
||||
|
||||
# TODO: master should handle this
|
||||
def persist(self, setting, session=None):
|
||||
""" """
|
||||
name = self.get_instance(session=session)['name']
|
||||
if self.creating:
|
||||
name = setting['name']
|
||||
else:
|
||||
name = self.request.matchdict['name']
|
||||
session = session or Session()
|
||||
self.app.save_setting(session, name, setting['value'])
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue