diff --git a/docs/api.rst b/docs/api.rst index ddc5a088..c2ea5b62 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -413,3 +413,16 @@ Signals operations, including connecting. .. _blinker: http://pypi.python.org/pypi/blinker + +Class Based Views +----------------- + +.. versionadded:: 0.7 + +.. currentmodule:: None + +.. autoclass:: flask.views.View + :members: + +.. autoclass:: flask.views.MethodView + :members: diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 6c49a878..7a8ebb12 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -17,6 +17,7 @@ instructions for web development with Flask. errorhandling config signals + views reqcontext blueprints shell diff --git a/docs/views.rst b/docs/views.rst new file mode 100644 index 00000000..a5b960e7 --- /dev/null +++ b/docs/views.rst @@ -0,0 +1,137 @@ +.. _views: + +Pluggable Views +=============== + +.. versionadded:: 0.7 + +Flask 0.7 introduces pluggable views inspired by the generic views from +Django which are based on classes instead of functions. The main +intention is that you can replace parts of the implementations and this +way have customizable pluggable views. + +Basic Principle +--------------- + +Consider you have a function that loads a list of objects from the +database and renders into a template:: + + @app.route('/users/') + def show_users(page): + users = User.query.all() + return render_template('users.html', users=users) + +This is simple and flexible, but if you want to provide this view in a +generic fashion that can be adapted to other models and templates as well +you might want more flexibility. This is where pluggable class based +views come into place. As the first step to convert this into a class +based view you would do this:: + + + from flask.views import View + + class ShowUsers(View): + + def dispatch_request(self): + users = User.query.all() + return render_template('users.html', objects=users) + + app.add_url_rule('/users/', ShowUsers.as_view('show_users')) + +As you can see what you have to do is to create a subclass of +:class:`flask.views.View` and implement +:meth:`~flask.views.View.dispatch_request`. Then we have to convert that +class into an actual view function by using the +:meth:`~flask.views.View.as_view` class method. The string you pass to +that function is the name of the endpoint that view will then have. But +this by itself is not helpful, so let's refactor the code a bit:: + + + from flask.views import View + + class ListView(View): + + def get_template_name(self): + raise NotImplementedError() + + def render_template(self, context): + return render_template(self.get_template_name(), **context) + + def dispatch_request(self): + context = {'objects': self.get_objects()} + return self.render_template(context) + + class UserView(ListView): + + def get_template_name(self): + return 'users.html' + + def get_objects(self): + return User.query.all() + +This of course is not that helpful for such a small example, but it's good +enough to explain the basic principle. When you have a class based view +the question comes up what `self` points to. The way this works is that +whenever the request is dispatched a new instance of the class is created +and the :meth:`~flask.views.View.dispatch_request` method is called with +the parameters from the URL rule. The class itself is instanciated with +the parameters passed to the :meth:`~flask.views.View.as_view` function. +For instance you can write a class like this:: + + class RenderTemplateView(View): + def __init__(self, template_name): + self.template_name = template_name + def dispatch_request(self): + return render_template(self.template_name) + +And then you can register it like this:: + + app.add_url_view('/about', RenderTemplateView.as_view( + 'about_page', template_name='about.html')) + +Method Hints +------------ + +Pluggable views are attached to the application like a regular function by +either using :func:`~flask.Flask.route` or better +:meth:`~flask.Flask.add_url_rule`. That however also means that you would +have to provide the names of the HTTP methods the view supports when you +attach this. In order to move that information to the class you can +provide a :attr:`~flask.views.View.methods` attribute that has this +information:: + + class MyView(View): + methods = ['GET', 'POST'] + + def dispatch_request(self): + if request.method == 'POST': + ... + ... + + app.add_url_view('/myview', MyView.as_view('myview')) + +Method Based Dispatching +------------------------ + +For RESTful APIs it's especially helpful to execute a different function +for each HTTP method. With the :class:`flask.views.MethodView` you can +easily do that. Each HTTP method maps to a function with the same name +(just in lowercase):: + + from flask.views import MethodView + + class UserAPI(MethodView): + + def get(self): + users = User.query.all() + ... + + def post(self): + user = User.from_form_data(request.form) + ... + + app.add_url_view('/users/', UserAPI.as_view('users')) + +That way you also don't have to provide the +:attr:`~flask.views.View.methods` attribute. It's automatically set based +on the methods defined in the class. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 80ccaffb..e04c2458 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1787,8 +1787,49 @@ class ViewTestCase(unittest.TestCase): return 'POST' app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + def test_view_patching(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + 1/0 + def post(self): + 1/0 + + class Other(Index): + def get(self): + return 'GET' + def post(self): + return 'POST' + + view = Index.as_view('index') + view.view_class = Other + app.add_url_rule('/', view_func=view) + self.common_test(app) + + def test_view_inheritance(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + class BetterIndex(Index): + def delete(self): + return 'DELETE' + + app.add_url_rule('/', view_func=BetterIndex.as_view('index')) + c = app.test_client() + + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assertEqual(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + + class DeprecationsTestCase(unittest.TestCase): def test_init_jinja_globals(self):