Merge branch '2.1.x'

This commit is contained in:
David Lord 2022-06-11 14:16:51 -07:00
commit dcd1a1e0b6
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
11 changed files with 287 additions and 171 deletions

View file

@ -29,7 +29,7 @@ collected in the following pages.
wtforms
templateinheritance
flashing
jquery
javascript
lazyloading
mongoengine
favicon

View file

@ -0,0 +1,259 @@
JavaScript, ``fetch``, and JSON
===============================
You may want to make your HTML page dynamic, by changing data without
reloading the entire page. Instead of submitting an HTML ``<form>`` and
performing a redirect to re-render the template, you can add
`JavaScript`_ that calls |fetch|_ and replaces content on the page.
|fetch|_ is the modern, built-in JavaScript solution to making
requests from a page. You may have heard of other "AJAX" methods and
libraries, such as |XHR|_ or `jQuery`_. These are no longer needed in
modern browsers, although you may choose to use them or another library
depending on your application's requirements. These docs will only focus
on built-in JavaScript features.
.. _JavaScript: https://developer.mozilla.org/Web/JavaScript
.. |fetch| replace:: ``fetch()``
.. _fetch: https://developer.mozilla.org/Web/API/Fetch_API
.. |XHR| replace:: ``XMLHttpRequest()``
.. _XHR: https://developer.mozilla.org/Web/API/XMLHttpRequest
.. _jQuery: https://jquery.com/
Rendering Templates
-------------------
It is important to understand the difference between templates and
JavaScript. Templates are rendered on the server, before the response is
sent to the user's browser. JavaScript runs in the user's browser, after
the template is rendered and sent. Therefore, it is impossible to use
JavaScript to affect how the Jinja template is rendered, but is is
possible to render data into the JavaScript that will run.
To provide data to JavaScript when rendering the template, use the
:func:`~jinja-filters.tojson` filter in a ``<script>`` block. This will
convert the data to a valid JavaScript object, and ensure that any
unsafe HTML characters are rendered safely. If you do not use the
``tojson`` filter, you will get a ``SyntaxError`` in the browser
console.
.. code-block:: python
data = generate_report()
return render_template("report.html", chart_data=data)
.. code-block:: jinja
<script>
const chart_data = {{ chart_data|tojson }}
chartLib.makeChart(chart_data)
</script>
A less common pattern is to add the data to a ``data-`` attribute on an
HTML tag. In this case, you must use single quotes around the value, not
double quotes, otherwise you will produce invalid or unsafe HTML.
.. code-block:: jinja
<div data-chart='{{ chart_data|tojson }}'></div>
Generating URLs
---------------
The other way to get data from the server to JavaScript is to make a
request for it. First, you need to know the URL to request.
The simplest way to generate URLs is to continue to use
:func:`~flask.url_for` when rendering the template. For example:
.. code-block:: javascript
const user_url = {{ url_for("user", id=current_user.id)|tojson }}
fetch(user_url).then(...)
However, you might need to generate a URL based on information you only
know in JavaScript. As discussed above, JavaScript runs in the user's
browser, not as part of the template rendering, so you can't use
``url_for`` at that point.
In this case, you need to know the "root URL" under which your
application is served. In simple setups, this is ``/``, but it might
also be something else, like ``https://example.com/myapp/``.
A simple way to tell your JavaScript code about this root is to set it
as a global variable when rendering the template. Then you can use it
when generating URLs from JavaScript.
.. code-block:: javascript
const SCRIPT_ROOT = {{ request.script_root|tojson }}
let user_id = ... // do something to get a user id from the page
let user_url = `${SCRIPT_ROOT}/user/${user_id}`
fetch(user_url).then(...)
Making a Request with ``fetch``
-------------------------------
|fetch|_ takes two arguments, a URL and an object with other options,
and returns a |Promise|_. We won't cover all the available options, and
will only use ``then()`` on the promise, not other callbacks or
``await`` syntax. Read the linked MDN docs for more information about
those features.
By default, the GET method is used. If the response contains JSON, it
can be used with a ``then()`` callback chain.
.. code-block:: javascript
const room_url = {{ url_for("room_detail", id=room.id)|tojson }}
fetch(room_url)
.then(response => response.json())
.then(data => {
// data is a parsed JSON object
})
To send data, use a data method such as POST, and pass the ``body``
option. The most common types for data are form data or JSON data.
To send form data, pass a populated |FormData|_ object. This uses the
same format as an HTML form, and would be accessed with ``request.form``
in a Flask view.
.. code-block:: javascript
let data = new FormData()
data.append("name": "Flask Room")
data.append("description": "Talk about Flask here.")
fetch(room_url, {
"method": "POST",
"body": data,
}).then(...)
In general, prefer sending request data as form data, as would be used
when submitting an HTML form. JSON can represent more complex data, but
unless you need that it's better to stick with the simpler format. When
sending JSON data, the ``Content-Type: application/json`` header must be
sent as well, otherwise Flask will return a 400 error.
.. code-block:: javascript
let data = {
"name": "Flask Room",
"description": "Talk about Flask here.",
}
fetch(room_url, {
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": JSON.stringify(data),
}).then(...)
.. |Promise| replace:: ``Promise``
.. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise
.. |FormData| replace:: ``FormData``
.. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData
Following Redirects
-------------------
A response might be a redirect, for example if you logged in with
JavaScript instead of a traditional HTML form, and your view returned
a redirect instead of JSON. JavaScript requests do follow redirects, but
they don't change the page. If you want to make the page change you can
inspect the response and apply the redirect manually.
.. code-block:: javascript
fetch("/login", {"body": ...}).then(
response => {
if (response.redirected) {
window.location = response.url
} else {
showLoginError()
}
}
)
Replacing Content
-----------------
A response might be new HTML, either a new section of the page to add or
replace, or an entirely new page. In general, if you're returning the
entire page, it would be better to handle that with a redirect as shown
in the previous section. The following example shows how to a ``<div>``
with the HTML returned by a request.
.. code-block:: html
<div id="geology-fact">
{{ include "geology_fact.html" }}
</div>
<script>
const geology_url = {{ url_for("geology_fact")|tojson }}
const geology_div = getElementById("geology-fact")
fetch(geology_url)
.then(response => response.text)
.then(text => geology_div.innerHtml = text)
</script>
Return JSON from Views
----------------------
To return a JSON object from your API view, you can directly return a
dict from the view. It will be serialized to JSON automatically.
.. code-block:: python
@app.route("/user/<int:id>")
def user_detail(id):
user = User.query.get_or_404(id)
return {
"username": User.username,
"email": User.email,
"picture": url_for("static", filename=f"users/{id}/profile.png"),
}
If you want to return another JSON type, use the
:func:`~flask.json.jsonify` function, which creates a response object
with the given data serialized to JSON.
.. code-block:: python
from flask import jsonify
@app.route("/users")
def user_list():
users = User.query.order_by(User.name).all()
return jsonify([u.to_json() for u in users])
It is usually not a good idea to return file data in a JSON response.
JSON cannot represent binary data directly, so it must be base64
encoded, which can be slow, takes more bandwidth to send, and is not as
easy to cache. Instead, serve files using one view, and generate a URL
to the desired file to include in the JSON. Then the client can make a
separate request to get the linked resource after getting the JSON.
Receiving JSON in Views
-----------------------
Use the :attr:`~flask.Request.json` property of the
:data:`~flask.request` object to decode the request's body as JSON. If
the body is not valid JSON, or the ``Content-Type`` header is not set to
``application/json``, a 400 Bad Request error will be raised.
.. code-block:: python
from flask import request
@app.post("/user/<int:id>")
def user_update(id):
user = User.query.get_or_404(id)
user.update_from_json(request.json)
db.session.commit()
return user.to_json()

View file

@ -1,148 +1,6 @@
:orphan:
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.
JSON itself is a very lightweight transport format, very similar to how
Python primitives (numbers, strings, dicts and lists) look like which is
widely supported and very easy to parse. It became popular a few years
ago and quickly replaced XML as transport format in web applications.
.. _jQuery: https://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 a script statement to the bottom of your ``<body>`` to load jQuery:
.. sourcecode:: html
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
Another method is using Google's `AJAX Libraries API
<https://developers.google.com/speed/libraries/>`_ to load jQuery:
.. sourcecode:: html
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{{
url_for('static', filename='jquery.js') }}">\x3C/script>')</script>
In this case you have to put jQuery into your static folder as a fallback, but it will
first try to load it directly from Google. This has the advantage that your
website will probably load faster for users if they went to at least one
other website before using the same jQuery version from Google because it
will already be in the browser cache.
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 could answer that question for
us, but if we are using jQuery we should 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
<script>
$SCRIPT_ROOT = {{ request.script_root|tojson }};
</script>
JSON View Functions
-------------------
Now let's create a server side function that accepts two URL arguments of
numbers which should be added together and then sent back to the
application in a JSON object. This is a really ridiculous example and is
something you usually would do on the client side alone, but a simple
example that shows how you would use jQuery and Flask nonetheless::
from flask import Flask, jsonify, render_template, request
app = Flask(__name__)
@app.route('/_add_numbers')
def add_numbers():
a = request.args.get('a', 0, type=int)
b = request.args.get('b', 0, type=int)
return jsonify(result=a + b)
@app.route('/')
def index():
return render_template('index.html')
As you can see I also added an `index` method here that renders a
template. This template will load jQuery as above and have a little form where
we can add two numbers and a link to trigger the function on the server
side.
Note that we are using the :meth:`~werkzeug.datastructures.MultiDict.get` method here
which will never fail. If the key is missing a default value (here ``0``)
is returned. Furthermore it can convert values to a specific type (like
in our case `int`). This is especially handy for code that is
triggered by a script (APIs, JavaScript etc.) because you don't need
special error reporting in that case.
The HTML
--------
Your index.html template either has to extend a :file:`layout.html` template with
jQuery loaded and the `$SCRIPT_ROOT` variable set, or do that on the top.
Here's the HTML code needed for our little application (:file:`index.html`).
Notice that we also drop the script directly into the HTML here. It is
usually a better idea to have that in a separate script file:
.. sourcecode:: html
<script>
$(function() {
$('a#calculate').bind('click', function() {
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
a: $('input[name="a"]').val(),
b: $('input[name="b"]').val()
}, function(data) {
$("#result").text(data.result);
});
return false;
});
});
</script>
<h1>jQuery Example</h1>
<p><input type=text size=5 name=a> +
<input type=text size=5 name=b> =
<span id=result>?</span>
<p><a href=# id=calculate>calculate server side</a>
I won't go into detail here about how jQuery works, just a very quick
explanation of the little bit of code above:
1. ``$(function() { ... })`` specifies code that should run once the
browser is done loading the basic parts of the page.
2. ``$('selector')`` selects an element and lets you operate on it.
3. ``element.bind('event', func)`` specifies a function that should run
when the user clicked on the element. If that function returns
`false`, the default behavior will not kick in (in this case, navigate
to the `#` URL).
4. ``$.getJSON(url, data, func)`` sends a ``GET`` request to `url` and will
send the contents of the `data` object as query parameters. Once the
data arrived, it will call the given function with the return value as
argument. Note that we can use the `$SCRIPT_ROOT` variable here that
we set earlier.
Check out the :gh:`example source <examples/javascript>` for a full
application demonstrating the code on this page, as well as the same
thing using ``XMLHttpRequest`` and ``fetch``.
Obsolete, see :doc:`/patterns/javascript` instead.

View file

@ -3,15 +3,15 @@ JavaScript Ajax Example
Demonstrates how to post form data and process a JSON response using
JavaScript. This allows making requests without navigating away from the
page. Demonstrates using |XMLHttpRequest|_, |fetch|_, and
|jQuery.ajax|_. See the `Flask docs`_ about jQuery and Ajax.
.. |XMLHttpRequest| replace:: ``XMLHttpRequest``
.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and
|jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax.
.. |fetch| replace:: ``fetch``
.. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
.. |XMLHttpRequest| replace:: ``XMLHttpRequest``
.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
.. |jQuery.ajax| replace:: ``jQuery.ajax``
.. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/
@ -21,7 +21,7 @@ page. Demonstrates using |XMLHttpRequest|_, |fetch|_, and
Install
-------
::
.. code-block:: text
$ python3 -m venv venv
$ . venv/bin/activate
@ -31,7 +31,7 @@ Install
Run
---
::
.. code-block:: text
$ export FLASK_APP=js_example
$ flask run
@ -42,7 +42,7 @@ Open http://127.0.0.1:5000 in a browser.
Test
----
::
.. code-block:: text
$ pip install -e '.[test]'
$ coverage run -m pytest

View file

@ -1,7 +1,7 @@
<!doctype html>
<title>JavaScript Example</title>
<link rel="stylesheet" href="https://unpkg.com/sakura.css@1.0.0/css/normalize.css">
<link rel="stylesheet" href="https://unpkg.com/sakura.css@1.0.0/css/sakura-earthly.css">
<link rel="stylesheet" href="https://unpkg.com/normalize.css@8.0.1/normalize.css">
<link rel="stylesheet" href="https://unpkg.com/sakura.css@1.3.1/css/sakura.css">
<style>
ul { margin: 0; padding: 0; display: flex; list-style-type: none; }
li > * { padding: 1em; }
@ -13,10 +13,10 @@
</style>
<ul>
<li><span>Type:</span>
<li class="{% if js == 'plain' %}active{% endif %}">
<a href="{{ url_for('index', js='plain') }}">Plain</a>
<li class="{% if js == 'fetch' %}active{% endif %}">
<a href="{{ url_for('index', js='fetch') }}">Fetch</a>
<li class="{% if js == 'xhr' %}active{% endif %}">
<a href="{{ url_for('index', js='xhr') }}">XHR</a>
<li class="{% if js == 'jquery' %}active{% endif %}">
<a href="{{ url_for('index', js='jquery') }}">jQuery</a>
</ul>

View file

@ -2,14 +2,11 @@
{% block intro %}
<a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch"><code>fetch</code></a>
is the <em>new</em> plain JavaScript way to make requests. It's
supported in all modern browsers except IE, which requires a
<a href="https://github.com/github/fetch">polyfill</a>.
is the <em>modern</em> plain JavaScript way to make requests. It's
supported in all modern browsers.
{% endblock %}
{% block script %}
<script src="https://unpkg.com/promise-polyfill@7.1.2/dist/polyfill.min.js"></script>
<script src="https://unpkg.com/whatwg-fetch@2.0.4/fetch.js"></script>
<script>
function addSubmit(ev) {
ev.preventDefault();

View file

@ -2,8 +2,9 @@
{% block intro %}
<a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest"><code>XMLHttpRequest</code></a>
is the plain JavaScript way to make requests. It's natively supported
by all browsers.
is the original JavaScript way to make requests. It's natively supported
by all browsers, but has been superseded by
<a href="{{ url_for("index", js="fetch") }}"><code>fetch</code></a>.
{% endblock %}
{% block script %}

View file

@ -5,8 +5,8 @@ from flask import request
from js_example import app
@app.route("/", defaults={"js": "plain"})
@app.route("/<any(plain, jquery, fetch):js>")
@app.route("/", defaults={"js": "fetch"})
@app.route("/<any(xhr, jquery, fetch):js>")
def index(js):
return render_template(f"{js}.html", js=js)

View file

@ -1,6 +1,6 @@
[metadata]
name = js_example
version = 1.0.0
version = 1.1.0
url = https://flask.palletsprojects.com/patterns/jquery/
license = BSD-3-Clause
maintainer = Pallets

View file

@ -5,8 +5,8 @@ from flask import template_rendered
@pytest.mark.parametrize(
("path", "template_name"),
(
("/", "plain.html"),
("/plain", "plain.html"),
("/", "xhr.html"),
("/plain", "xhr.html"),
("/fetch", "fetch.html"),
("/jquery", "jquery.html"),
),

View file

@ -602,7 +602,8 @@ def send_from_directory(
If the final path does not point to an existing regular file,
raises a 404 :exc:`~werkzeug.exceptions.NotFound` error.
:param directory: The directory that ``path`` must be located under.
:param directory: The directory that ``path`` must be located under,
relative to the current application's root path.
:param path: The path to the file to send, relative to
``directory``.
:param kwargs: Arguments to pass to :func:`send_file`.