-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CRUD forms #80
Comments
Perhaps the decorators should be named |
This is still too verbose. A full set of CRUD routes is still 20-30 lines of boilerplate. CrudView should be designed to produce boilerplate automatically, allowing the user to override only as necessary. |
As we've learnt from hasgeek/coaster#150 (StateManager), preserving flexibility for special behaviour is important, so that has to be the accounted for. We need these endpoints in order of priority, with these possible outcomes:
Notes:
|
The sample code above does not account for the |
Proper CRUD requires using the HTTP verbs
Mapped to the seven handlers above, this gets us (assuming a base URL of
|
Flask provides a MethodView that makes the REST URLs easier to implement, but it's not good enough as we have nowhere to serve Update-Get, Create-Get and Delete-Get from. |
If we take the |
CrudView needs a way to connect a URL to a loaded instance.
|
CrudView could be loader-agnostic by doing something like this: def returns_loaded(parent, instance):
return instance
class MyView(CrudView):
model = MyModel
parent = ParentModel
form = MyForm
loader = load_models(
(ParentModel: {'name': 'parent'}, 'parent'),
(MyModel, {'name': 'identity', 'parent': 'parent'}, 'instance')
)(returns_loaded)
MyView.register_views(app, '/<parent>') This sample code should be the entire boilerplate for registering CRUD views with default handlers. |
To ensure proxy = obj.access_for(g.user)
form.populate_obj(proxy) Do note that |
CrudView should also account for form context defined in #112. |
Another API proposal: docview = ModelView(app, Document)
# docview is a helper for making views and offers the following methods:
# add_create, add_read, add_update, add_delete, add_view
docview.add_read(
route='/<parent>/<document>',
query=lambda parent, document: Document.query.join(DocParent).filter(Document.name == document, Document.parent.name == parent),
template='document.html.jinja2', # Passed to render_with, so this can be a dictionary as well
json=True, # Adds JSON output support
permission='view', # Optional (tested with `permission in document.permissions(current_auth.actor)`)
roles={'all'}, # Optional (tested with `roles.intersection(document.current_roles)`)
decorators=[], # Optional decorators to be applied to the view (can be `requestargs`, `cors`, etc)
)
Problem: routes and route validation can be complicated, as this snippet from Hasjob shows: @app.route('/<domain>/<hashid>', methods=('GET', 'POST'), subdomain='<subdomain>')
@app.route('/<domain>/<hashid>', methods=('GET', 'POST'))
@app.route('/view/<hashid>', defaults={'domain': None}, methods=('GET', 'POST'), subdomain='<subdomain>')
@app.route('/view/<hashid>', defaults={'domain': None}, methods=('GET', 'POST'))
def jobdetail(domain, hashid):
is_siteadmin = lastuser.has_permission('siteadmin')
query = JobPost.fetch(hashid).options(
db.subqueryload('locations'), db.subqueryload('taglinks'))
post = query.first_or_404()
# If we're on a board (that's not 'www') and this post isn't on this board,
# redirect to (a) the first board it is on, or (b) on the root domain (which may
# be the 'www' board, which is why we don't bother to redirect if we're currently
# in the 'www' board)
if g.board and g.board.not_root and post.link_to_board(g.board) is None:
blink = post.postboards.first()
if blink:
return redirect(post.url_for(subdomain=blink.board.name, _external=True))
else:
return redirect(post.url_for(subdomain=None, _external=True))
# If this post is past pending state and the domain doesn't match, redirect there
if post.status not in POSTSTATUS.UNPUBLISHED and post.email_domain != domain:
return redirect(post.url_for(), code=301)
if post.status in POSTSTATUS.UNPUBLISHED:
if not ((g.user and post.admin_is(g.user))):
abort(403)
if post.status in POSTSTATUS.GONE:
abort(410) Where do we accommodate (a) additional routes and (b) view validation?
docread = docview.add_read(…)
@docread.validate
def docread_validate(context, document):
pass
@docread.prepare
def docread_prepare(context, document):
# This is what happens if no `prepare` handler is provided
return document.current_access() ( Problem: The We haven't had this ambiguous parentage problem anywhere else ( |
Here is yet another way to think about views. We make these assumptions:
We've had an early attempt at view consolidation with Nodular's NodeView. It allowed view construction like this: class MyNodeView(NodeView):
@NodeView.route('/')
def index(self):
return u'index view'
@NodeView.route('/edit', methods=['GET', 'POST'])
@NodeView.route('/', methods=['PUT'])
@NodeView.requires_permission('edit', 'siteadmin')
def edit(self):
return u'edit view'
@NodeView.route('/delete', methods=['GET', 'POST'])
@NodeView.route('/', methods=['DELETE'])
@NodeView.requires_permission('delete', 'siteadmin')
def delete(self):
return u'delete view' NodeView is heavy, creating a new router for the view. The app's router lands on |
CRUD views need a foundation. This is coming in hasgeek/coaster#167 with class-based views. |
CRUD views are a recurring pattern in HasGeek apps:
/new
handler exists that presents a blank form usingrender_form
and creates a new instance if the form validates, following which the user is redirected to the new instance./edit
handler exists that loads the object into the form and otherwise behaves exactly like/new
./delete
handler exists that asks for confirmation, then deletes the object and redirects to the parent object.This pattern can be abstracted away so that (a) we can get past the pain of making views and (b) add new features in a centralised place instead of updating every view in every app. Sample usage:
The text was updated successfully, but these errors were encountered: