diff --git a/docs/api.rst b/docs/api.rst index 5dcdfa95..247294a3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -56,12 +56,15 @@ Incoming Request Data .. attribute:: stream - If the incoming form data was not encoded with a known encoding (for - example it was transmitted as JSON) the data is stored unmodified in - this stream for consumption. For example to read the incoming - request data as JSON, one can do the following:: + If the incoming form data was not encoded with a known mimetype + the data is stored unmodified in this stream for consumption. Most + of the time it is a better idea to use :attr:`data` which will give + you that data as a string. The stream only returns the data once. + + .. attribute:: data - json_body = simplejson.load(request.stream) + Contains the incoming request data as string in case it came with + a mimetype Flask does not handle. .. attribute:: files @@ -106,6 +109,20 @@ Incoming Request Data `root_url` ``http://www.example.com/myapplication/`` ============= ====================================================== + .. attribute:: is_xhr + + `True` if the request was triggered via a JavaScript + `XMLHttpRequest`. This only works with libraries that support the + ``X-Requested-With`` header and set it to `XMLHttpRequest`. + Libraries that do that are prototype, jQuery and Mochikit and + probably some more. + + .. attribute:: json + + Contains the parsed body of the JSON request if the mimetype of + the incoming data was `application/json`. This requires Python 2.6 + or an installed version of simplejson. + Response Objects ---------------- @@ -201,6 +218,38 @@ Message Flashing .. autofunction:: get_flashed_messages +Returning JSON +-------------- + +.. autofunction:: jsonify + +.. data:: json + + If JSON support is picked up, this will be the module that Flask is + using to parse and serialize JSON. So instead of doing this yourself:: + + try: + import simplejson as json + except ImportError: + import json + + You can instead just do this:: + + from flask import json + + For usage examples, read the :mod:`json` documentation. + + The :func:`~json.dumps` function of this json module is also available + as filter called ``|tojson`` in Jinja2. Note that inside `script` + tags no escaping must take place, so make sure to disable escaping + with ``|safe`` if you intend to use it inside `script` tags: + + .. sourcecode:: html+jinja + + + Template Rendering ------------------ diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 037f6e11..8122deb7 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -19,3 +19,4 @@ end of the request, the database connection is closed again. wtforms templateinheritance flashing + jquery diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst new file mode 100644 index 00000000..cb5aa234 --- /dev/null +++ b/docs/patterns/jquery.rst @@ -0,0 +1,59 @@ +AJAX With jQuery +================ + +`jQuery`_ is a small JavaScript library commonly used to simplify working +with the DOM and JavaScript in general. It is the perfect tool to make +web applications more dynamic by exchanging JSON between server and +client. + +.. _jQuery: http://jquery.com/ + +Loading jQuery +-------------- + +In order to use jQuery, you have to download it first and place it in the +static folder of your application and then ensure it's loaded. Ideally +you have a layout template that is used for all pages where you just have +to add two script statements to your `head` section. One for jQuery, and +one for your own script (called `app.js` here): + +.. sourcecode:: html + + + + +Where is My Site? +----------------- + +Do you know where your application is? If you are developing the answer +is quite simple: it's on localhost port something and directly on the root +of that server. But what if you later decide to move your application to +a different location? For example to ``http://example.com/myapp``? On +the server side this never was a problem because we were using the handy +:func:`~flask.url_for` function that did could answer that question for +us, but if we are using jQuery we should better not hardcode the path to +the application but make that dynamic, so how can we do that? + +A simple method would be to add a script tag to our page that sets a +global variable to the prefix to the root of the application. Something +like this: + +.. sourcecode:: html+jinja + + + +The ``|safe`` is necessary so that Jinja does not escape the JSON encoded +string with HTML rules. Usually this would be necessary, but we are +inside a `script` block here where different rules apply. + +.. admonition:: Information for Pros + + In HTML the `script` tag is declared `CDATA` which means that entities + will not be parsed. Everything until ```` is handled as script. + This also means that there must never be any `` +jQuery Example + + + +

jQuery Example

+

+ + = + ? +

calculate server side diff --git a/flask.py b/flask.py index cec1aafb..ba26c7f8 100644 --- a/flask.py +++ b/flask.py @@ -15,11 +15,23 @@ import sys from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, SharedDataMiddleware + LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ + cached_property from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException from werkzeug.contrib.securecookie import SecureCookie +# try to load the best simplejson implementation available. If JSON +# is not installed, we add a failing class. +json_available = True +try: + import simplejson as json +except ImportError: + try: + import json + except ImportError: + json_available = False + # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. from werkzeug import abort, redirect @@ -49,6 +61,16 @@ class Request(RequestBase): self.endpoint = None self.view_args = None + @cached_property + def json(self): + """If the mimetype is `application/json` this will contain the + parsed JSON data. + """ + if not json_available: + raise AttributeError('simplejson not available') + if self.mimetype == 'application/json': + return json.loads(self.data) + class Response(ResponseBase): """The response object that is used by default in flask. Works like the @@ -81,7 +103,6 @@ class _NullSession(SecureCookie): del _fail - class _RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the @@ -133,6 +154,8 @@ def get_template_attribute(template_name, attribute): hello = get_template_attribute('_foo.html', 'hello') return hello('World') + .. versionadded:: 0.2 + :param template_name: the name of the template :param attribute: the name of the variable of macro to acccess """ @@ -162,6 +185,35 @@ def get_flashed_messages(): return flashes +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. + + .. versionadded:: 0.2 + """ + return current_app.response_class(json.dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), mimetype='application/json') + + def render_template(template_name, **context): """Renders a template from the template folder with the given context. @@ -326,6 +378,8 @@ class Flask(object): url_for=url_for, get_flashed_messages=get_flashed_messages ) + if json_available: + self.jinja_env.filters['tojson'] = json.dumps def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for diff --git a/tests/flask_tests.py b/tests/flask_tests.py index bb560712..0094f657 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -32,7 +32,7 @@ class ContextTestCase(unittest.TestCase): assert meh() == 'http://localhost/meh' -class BasicFunctionality(unittest.TestCase): +class BasicFunctionalityTestCase(unittest.TestCase): def test_request_dispatching(self): app = flask.Flask(__name__) @@ -167,7 +167,35 @@ class BasicFunctionality(unittest.TestCase): == '/static/index.html' -class Templating(unittest.TestCase): +class JSONTestCase(unittest.TestCase): + + def test_jsonify(self): + d = dict(a=23, b=42, c=[1, 2, 3]) + app = flask.Flask(__name__) + @app.route('/kw') + def return_kwargs(): + return flask.jsonify(**d) + @app.route('/dict') + def return_dict(): + return flask.jsonify(d) + c = app.test_client() + for url in '/kw', '/dict': + rv = c.get(url) + assert rv.mimetype == 'application/json' + assert flask.json.loads(rv.data) == d + + def test_json_attr(self): + app = flask.Flask(__name__) + @app.route('/add', methods=['POST']) + def add(): + return unicode(flask.request.json['a'] + flask.request.json['b']) + c = app.test_client() + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') + assert rv.data == '3' + + +class TemplatingTestCase(unittest.TestCase): def test_context_processing(self): app = flask.Flask(__name__)