-
Notifications
You must be signed in to change notification settings - Fork 13
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
Class-based views #167
Class-based views #167
Conversation
* Remove non-standard JSON mime types * Massage view output into a dictionary if not already one * Accept render_with(json=True) with no template specified
Because of the use of decorators, this take on class views creates tight coupling between methods and routes. It is not possible for a subclass to redefine only the route, or only the method. Consider this situation: class BaseView(ClassView):
@route('')
def view(self):
return "Base view"
@route('/doc')
class SubView(BaseView):
def view(self):
return "Redefined view"
SubView.init_app(app)
>>> client = app.test_client()
>>> client.get('/doc').data
'Base view' Because of the way
@route('/doc')
class SubView(BaseView):
@BaseView.view.reroute
def view(self):
return "Redefined view" Since
|
Should we call this decorator @route('/<domain>/<hashid>')
def DocumentView(CrudView):
@route('/view/<hashid>')
@CrudView.view.reroute # This is inheriting @route('')
def view(self, domain, hashid):
return JobPost.get(hashid=hashid).one_or_404().current_access() Is there a better term than 'reroute' here? |
We now allow sub views to replace view handlers or add additional URL rules like this: class BaseView(ClassView):
@route('')
def first(self):
return 'first'
@route('second')
def second(self):
return 'second'
@route('third')
def third(self):
return 'third'
@route('inherited')
def inherited(self):
return 'inherited'
@route('also-inherited')
def also_inherited(self):
return 'also_inherited'
@route('/subclasstest')
class SubView(BaseView):
@BaseView.first.reroute
def first(self):
return 'rerouted-first'
@route('2')
@BaseView.second.reroute
def second(self):
return 'rerouted-second'
def third(self):
return 'removed-third'
also_inherited = route('/inherited')(
route('inherited2')(
BaseView.get_view('also_inherited')))
SubView.init_app(app) However, it is not possible to:
|
Converting a method into a view can be achieved like this: class SubView(BaseView):
@route('my-action')
def my_action(self):
return super(SubView, self).my_action() This is unnecessary boilerplate though. Maybe we need a call like class SubView(BaseView):
pass
SubView.add_route_for('my_action', 'my-action') Calling this will mutate SubView to replace the existing attribute. |
Replacing @route('/subclasstest')
class SubView(BaseView):
@BaseView.first.reroute
def first(self):
return 'rerouted-first'
@route('2')
@BaseView.second.reroute
def second(self):
return 'rerouted-second'
def third(self):
return 'removed-third'
SubView.add_route_for('also_inherited', '/inherited')
SubView.add_route_for('also_inherited', 'inherited2')
SubView.add_route_for('latent_route', 'latent')
SubView.init_app(app) |
The class BaseView(ClassView):
@route('')
def first(self):
return 'first'
@route('/subclass')
class SubView(BaseView):
@route('first')
@BaseView.first.reroute
def first(self):
return 'rerouted-first'
# Available routes:
# /subclass
# /subclass/first
@route('/anothersubclass')
class AnotherSubView(BaseView):
@route('1')
@BaseView.first.reroute # This is now actually SubView.first.reroute
def first(self):
return 'rerouted-first'
# Available routes:
# /anothersubclass
# /anothersubclass/first
# /anothersubclass/1 We will have to use the descriptor approach we previously rejected. |
@route('/doc/<document>')
class DocumentView(UrlForView, InstanceLoader, ModelView):
model = Document
route_model_map = {
'document': 'name'
}
@route('')
@render_with(json=True)
def view(self):
"""
Views no longer need to bother with loading an instance.
Views are also auto-registered to the model as url_for targets
"""
return self.obj.current_access()
DocumentView.init_app(app)
>>> doc1 = Document(name='test1', title="Test 1")
>>> doc2 = Document(name='test2', title="Test 2")
>>> db.session.add_all([doc1, doc2])
>>> db.session.commit()
>>> client = app.test_client()
>>> rv = self.client.get('/model/test1')
>>> data = json.loads(rv.data)
>>> data['name']
'test1'
>>> doc1.url_for('view')
'/doc/test1'
>>> doc2.url_for('') # 'view' is the default
'/doc/test2' |
This call is a little unwieldy: SubView.add_route_for('also_inherited', '/inherited')
SubView.add_route_for('also_inherited', 'inherited2')
SubView.add_route_for('latent_route', 'latent') Why not make it like this? SubView.also_inherited.add_route('/inherited')
SubView.also_inherited.add_route('inherited2')
SubView.latent_route.add_route('latent') Since we now have a Update: this is tricky because |
coaster/views/classview.py
Outdated
|
||
TODO: Consider allowing :meth:`loader` to place attributes on ``self`` | ||
by itself, to accommodate scenarios where multiple models need to be | ||
loaded. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will affect Funnel's CommentSpace
and VoteSpace
, since there's no backward link from them to Proposal
. The loader must load the Proposal
and join with CommentSpace
/VoteSpace
. In this case, the space goes into self.obj
while the proposal goes into self.proposal
.
@route('/model/<parent>/<document>')
class ScopedDocumentView(DocumentView):
model = ScopedViewDocument
route_model_map = {
'document': 'name',
'parent': 'parent.name',
}
ScopedDocumentView.init_app(app) With this, #78 (load_models should support joined loads) is no longer relevant, with one caveat: we have no framework by which to handle redirect models, as highlighted in that ticket. |
Still missing: access control. We have two frameworks in Coaster: permissions and roles. Roles are appropriate for controlling model attribute access, typically in a flexible framework like GraphQL (see #100), while permissions are appropriate for controlling access to views.
Solution:
Problem: Calling Solution: |
I foresee problems with view mixins and the
Problem: If this is a view mixin (named, say, class MyView(InstanceLoader, UrlRewriteView, ModelView):
…
class MyView(UrlRewriteView, InstanceLoader, ModelView):
… In the first case, |
ClassView (or a mixin) should register itself as |
Problem: |
Also, given multiple transitions that take no/similar parameters, a wildcard view should be possible (n:n mappings): @route('/<mymodel>')
class MyModelView(InstanceLoader, ModelView):
model = MyModel
…
@route('<transition>', methods=['POST']) # Creates /<mymodel>/<transition>
@render_with(json=True)
@transition_view('transition') # String parameter implies transition named in the keyword argument
def all_transitions(self, transition): # Loaded transition is passed as 'transition' parameter to view
transition()
return {'status': 'ok'}
@route('special', methods=['POST']) # Creates /<mymodel>/special
@render_with(json=True)
@transition_view(MyModel.special) # Transition parameter implies this is the transition we want to call
def special_transition(self, transition): # MyModel.transition has turned into self.model.transition
transition()
return {'status': 'ok', 'special': True} |
* ModelView no longer strips the view’s keyword arguments * Mixin classes can add their own __decorators__ * before_request can no longer override the view response. Use a decorator instead * Consequently, there’s no longer an after_load method
This is unnecessarily verbose. Why not: class MyModelView(InstanceLoader, TransitionView):
model = MyModel
transition_forms = {
'special': SpecialForm
}
MyModelView.add_route_for('call_transition', '<transition>', methods=['POST']) Note that this cannot offer the |
@shreyas-satish @iambibhas I think |
New take on class-based views, as the foundation for model-based views and CRUD views.