diff --git a/Makefile b/Makefile index 94ad0077..62d763d2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc test +.PHONY: clean-pyc test upload-docs all: clean-pyc test diff --git a/docs/api.rst b/docs/api.rst index 0d8b6cf5..a6714d25 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -183,6 +183,13 @@ To access the current session you can use the :class:`session` object: # so mark it as modified yourself session.modified = True + .. attribute:: permanent + + If set to `True` the session life for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to `False` (which is the default) the + session will be deleted when the user closes the browser. + Application Globals ------------------- diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 22da533c..f777a294 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -91,3 +91,52 @@ Here the code:: Notice that this assumes an instanciated `cache` object is available, see :ref:`caching-pattern` for more information. + + +Templating Decorator +-------------------- + +A common pattern invented by the TurboGears guys a while back is a +templating decorator. The idea of that decorator is that you return a +dictionary with the values passed to the template from the view function +and the template is automatically rendered. With that, the following +three examples do exactly the same:: + + @app.route('/') + def index(): + return render_template('index.html', value=42) + + @app.route('/') + @templated('index.html') + def index(): + return dict(value=42) + + @app.route('/') + @templated() + def index(): + return dict(value=42) + +As you can see, if no template name is provided it will use the endpoint +of the URL map + ``'.html'``. Otherwise the provided template name is +used. When the decorated function returns, the dictionary returned is +passed to the template rendering function. If `None` is returned, an +empty dictionary is assumed. + +Here the code for that decorator:: + + from functools import wraps + from flask import request + + def templated(template=None): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + template_name = template + if template_name is None: + template_name = request.endpoint + '.html' + ctx = f(*args, **kwargs) + if ctx is None: + ctx = {} + return render_template(template_name, **ctx) + return decorated_function + return decorator diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index bbceee8a..d62c5bd3 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -42,7 +42,7 @@ In the view function, the usage of this form looks like this:: form.password.data) db_session.add(user) flash('Thanks for registering') - redirect(url_for('login')) + return redirect(url_for('login')) return render_template('register.html', form=form) Notice that we are implying that the view is using SQLAlchemy here diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 6d641d26..59e36bcc 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -160,6 +160,8 @@ The following converters exist: `path` like the default but also accepts slashes =========== =========================================== +.. _url-building: + URL Building ```````````` @@ -167,7 +169,8 @@ If it can match URLs, can it also generate them? Of course you can. To build a URL to a specific function you can use the :func:`~flask.url_for` function. It accepts the name of the function as first argument and a number of keyword arguments, each corresponding to the variable part of -the URL rule. Here some examples: +the URL rule. Unknown variable parts are appended to the URL as query +parameter. Here some examples: >>> from flask import Flask, url_for >>> app = Flask(__name__) @@ -184,9 +187,11 @@ the URL rule. Here some examples: ... print url_for('index') ... print url_for('login') ... print url_for('profile', username='John Doe') +... print url_for('login', next='/') ... / /login +/login?next=/ /user/John%20Doe (This also uses the :meth:`~flask.Flask.test_request_context` method @@ -452,7 +457,7 @@ transmitted in a `POST` or `PUT` request) you can use the :attr:`~flask.request.form` attribute. Here a full example of the two attributes mentioned above:: - @app.route('/login', method=['POST', 'GET']) + @app.route('/login', methods=['POST', 'GET']) def login(): error = None if request.method == 'POST': diff --git a/docs/testing.rst b/docs/testing.rst index 0901792b..be72e746 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -195,3 +195,22 @@ suite. .. _MiniTwit Example: http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ + + +Other Testing Tricks +-------------------- + +Besides using the test client we used above there is also the +:meth:`~flask.Flask.test_request_context` method that in combination with +the `with` statement can be used to activate a request context +temporarily. With that you can access the :class:`~flask.request`, +:class:`~flask.g` and :class:`~flask.session` objects like in view +functions. Here a full example that showcases this:: + + app = flask.Flask(__name__) + + with app.test_request_context('/?name=Peter'): + assert flask.request.path == '/' + assert flask.request.args['name'] == 'Peter' + +All the other objects that are context bound can be used the same. diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index 3d1aa806..c3075e3a 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -2,8 +2,7 @@ Bonus: Testing the Application =============================== Now that you have finished the application and everything works as -expected, it's probably not the best idea to add automated tests to -simplify modifications in the future. The application above is used as a -basic example of how to perform unittesting in the :ref:`testing` section -of the documentation. Go there to see how easy it is to test Flask -applications. +expected, it's probably not a good idea to add automated tests to simplify +modifications in the future. The application above is used as a basic +example of how to perform unittesting in the :ref:`testing` section of the +documentation. Go there to see how easy it is to test Flask applications. diff --git a/flask.py b/flask.py index 1ba6e4d1..15891ea1 100644 --- a/flask.py +++ b/flask.py @@ -13,6 +13,7 @@ from __future__ import with_statement import os import sys import types +from datetime import datetime, timedelta from itertools import chain from jinja2 import Environment, PackageLoader, FileSystemLoader @@ -93,7 +94,20 @@ class _RequestGlobals(object): pass -class _NullSession(SecureCookie): +class Session(SecureCookie): + """Expands the session for support for switching between permanent + and non-permanent sessions. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + def _set_permanent(self, value): + self['_permanent'] = bool(value) + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + +class _NullSession(Session): """Class used to generate nicer error messages if sessions are not available. Will still allow read-only access to the empty session but fail on setting. @@ -158,6 +172,11 @@ def url_for(endpoint, **values): any ``'admin.index'`` `index` of the `admin` module ==================== ======================= ============================= + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. + + For more information, head over to the :ref:`Quickstart `. + :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule """ @@ -511,6 +530,11 @@ class Flask(_PackageBoundObject): #: The secure cookie uses this for the name of the session cookie session_cookie_name = 'session' + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + permanent_session_lifetime = timedelta(days=31) + #: options that are passed directly to the Jinja2 environment jinja_options = ImmutableDict( autoescape=True, @@ -664,8 +688,8 @@ class Flask(_PackageBoundObject): """ key = self.secret_key if key is not None: - return SecureCookie.load_cookie(request, self.session_cookie_name, - secret_key=key) + return Session.load_cookie(request, self.session_cookie_name, + secret_key=key) def save_session(self, session, response): """Saves the session if it needs updates. For the default @@ -676,7 +700,11 @@ class Flask(_PackageBoundObject): object) :param response: an instance of :attr:`response_class` """ - session.save_cookie(response, self.session_cookie_name) + expires = None + if session.permanent: + expires = datetime.utcnow() + self.permanent_session_lifetime + session.save_cookie(response, self.session_cookie_name, + expires=expires, httponly=True) def register_module(self, module, **options): """Registers a module with this application. The keyword argument diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6a4ae50c..9a36a8d2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -11,11 +11,14 @@ """ from __future__ import with_statement import os +import re import sys import flask import unittest import tempfile import warnings +from datetime import datetime +from werkzeug import parse_date example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -118,6 +121,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): expect_exception(flask.session.__setitem__, 'foo', 42) expect_exception(flask.session.pop, 'foo') + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + assert expires.year == expected.year + assert expires.month == expected.month + assert expires.day == expected.day + + permanent = False + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + assert match is None + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey'