diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a9bcace65f..36b7df3ab9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -75,7 +75,7 @@ First time setup .. _latest version of git: https://git-scm.com/downloads .. _username: https://help.github.com/articles/setting-your-username-in-git/ .. _email: https://help.github.com/articles/setting-your-email-in-git/ -.. _Fork: https://github.com/pallets/flask/pull/2305#fork-destination-box +.. _Fork: https://github.com/pallets/flask/fork .. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork Start coding diff --git a/docs/_static/flaskr.png b/docs/_static/flaskr.png deleted file mode 100644 index 838f760472..0000000000 Binary files a/docs/_static/flaskr.png and /dev/null differ diff --git a/docs/conf.py b/docs/conf.py index 682391f341..a037476e27 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,13 +11,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. from __future__ import print_function + +import datetime import os import sys -import pkg_resources import time -import datetime -from sphinx.application import Sphinx +import pkg_resources BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))) @@ -300,7 +300,7 @@ def update_wrapper(wrapper, wrapped, *a, **kw): del unwrap_decorators -def setup(app: Sphinx): +def setup(app): def cut_module_meta(app, what, name, obj, options, lines): """Remove metadata from autodoc output.""" if what != 'module': @@ -312,3 +312,34 @@ def cut_module_meta(app, what, name, obj, options, lines): ] app.connect('autodoc-process-docstring', cut_module_meta) + + def github_link( + name, rawtext, text, lineno, inliner, + options=None, content=None + ): + app = inliner.document.settings.env.app + release = app.config.release + base_url = 'https://github.com/pallets/flask/tree/' + + if text.endswith('>'): + words, text = text[:-1].rsplit('<', 1) + words = words.strip() + else: + words = None + + if release.endswith('dev'): + url = '{0}master/{1}'.format(base_url, text) + else: + url = '{0}{1}/{2}'.format(base_url, release, text) + + if words is None: + words = url + + from docutils.nodes import reference + from docutils.parsers.rst.roles import set_classes + options = options or {} + set_classes(options) + node = reference(rawtext, words, refuri=url, **options) + return [node], [] + + app.add_role('gh', github_link) diff --git a/docs/installation.rst b/docs/installation.rst index 88b9af092e..e32ec6c7b6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -104,6 +104,8 @@ On Windows: \Python27\Scripts\virtualenv.exe venv +.. _install-activate-env: + Activate the environment ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index d136d5b42d..24569a531e 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -162,7 +162,5 @@ explanation of the little bit of code above: argument. Note that we can use the `$SCRIPT_ROOT` variable here that we set earlier. -If you don't get the whole picture, download the `sourcecode -for this example -`_ -from GitHub. +If you don't get the whole picture, download the :gh:`sourcecode +for this example `. diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 6b0ee7ad65..f6b5161429 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -17,9 +17,8 @@ this:: login.html ... -If you find yourself stuck on something, feel free -to take a look at the source code for this example. -You'll find `the full src for this example here`_. +The :ref:`tutorial ` is structured this way, see the +:gh:`example code `. Simple Packages --------------- @@ -59,21 +58,21 @@ a big problem, just add a new file called :file:`setup.py` next to the inner ], ) -In order to run the application you need to export an environment variable -that tells Flask where to find the application instance:: +In order to run the application you need to export an environment variable +that tells Flask where to find the application instance:: export FLASK_APP=yourapplication -If you are outside of the project directory make sure to provide the exact +If you are outside of the project directory make sure to provide the exact path to your application directory. Similarly you can turn on the development features like this:: export FLASK_ENV=development -In order to install and run the application you need to issue the following +In order to install and run the application you need to issue the following commands:: - pip install -e . + pip install -e . flask run What did we gain from this? Now we can restructure the application a bit @@ -134,7 +133,6 @@ You should then end up with something like that:: .. _working-with-modules: -.. _the full src for this example here: https://github.com/pallets/flask/tree/master/examples/patterns/largerapp Working with Blueprints ----------------------- diff --git a/docs/testing.rst b/docs/testing.rst index 4a272df6dc..bfbc1d9165 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -28,10 +28,7 @@ The Application First, we need an application to test; we will use the application from the :ref:`tutorial`. If you don't have that application yet, get the -source code from `the examples`_. - -.. _the examples: - https://github.com/pallets/flask/tree/master/examples/flaskr/ +source code from :gh:`the examples `. The Testing Skeleton -------------------- diff --git a/docs/tutorial/blog.rst b/docs/tutorial/blog.rst new file mode 100644 index 0000000000..4511d61b74 --- /dev/null +++ b/docs/tutorial/blog.rst @@ -0,0 +1,336 @@ +.. currentmodule:: flask + +Blog Blueprint +============== + +You'll use the same techniques you learned about when writing the +authentication blueprint to write the blog blueprint. The blog should +list all posts, allow logged in users to create posts, and allow the +author of a post to edit or delete it. + +As you implement each view, keep the development server running. As you +save your changes, try going to the URL in your browser and testing them +out. + +The Blueprint +------------- + +Define the blueprint and register it in the application factory. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for + ) + from werkzeug.exceptions import abort + + from flaskr.auth import login_required + from flaskr.db import get_db + + bp = Blueprint('blog', __name__) + +Import and register the blueprint from the factory using +:meth:`app.register_blueprint() `. Place the +new code at the end of the factory function before returning the app. + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + def create_app(): + app = ... + # existing code omitted + + from . import blog + app.register_blueprint(blog.bp) + app.add_url_rule('/', endpoint='index') + + return app + + +Unlike the auth blueprint, the blog blueprint does not have a +``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` +view at ``/create``, and so on. The blog is the main feature of Flaskr, +so it makes sense that the blog index will be the main index. + +However, the endpoint for the ``index`` view defined below will be +``blog.index``. Some of the authentication views referred to a plain +``index`` endpoint. :meth:`app.add_url_rule() ` +associates the endpoint name ``'index'`` with the ``/`` url so that +``url_for('index')`` or ``url_for('blog.index')`` will both work, +generating the same ``/`` URL either way. + +In another application you might give the blog blueprint a +``url_prefix`` and define a separate ``index`` view in the application +factory, similar to the ``hello`` view. Then the ``index`` and +``blog.index`` endpoints and URLs would be different. + + +Index +----- + +The index will show all of the posts, most recent first. A ``JOIN`` is +used so that the author information from the ``user`` table is +available in the result. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('/') + def index(): + db = get_db() + posts = db.execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' ORDER BY created DESC' + ).fetchall() + return render_template('blog/index.html', posts=posts) + +.. code-block:: html+jinja + :caption: ``flaskr/templates/blog/index.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Posts{% endblock %}

+ {% if g.user %} + New + {% endif %} + {% endblock %} + + {% block content %} + {% for post in posts %} +
+
+
+

{{ post['title'] }}

+
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
+
+ {% if g.user['id'] == post['author_id'] %} + Edit + {% endif %} +
+

{{ post['body'] }}

+
+ {% if not loop.last %} +
+ {% endif %} + {% endfor %} + {% endblock %} + +When a user is logged in, the ``header`` block adds a link to the +``create`` view. When the user is the author of a post, they'll see an +"Edit" link to the ``update`` view for that post. ``loop.last`` is a +special variable available inside `Jinja for loops`_. It's used to +display a line after each post except the last one, to visually separate +them. + +.. _Jinja for loops: http://jinja.pocoo.org/docs/templates/#for + + +Create +------ + +The ``create`` view works the same as the auth ``register`` view. Either +the form is displayed, or the posted data is validated and the post is +added to the database or an error is shown. + +The ``login_required`` decorator you wrote earlier is used on the blog +views. A user must be logged in to visit these views, otherwise they +will be redirected to the login page. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('/create', methods=('GET', 'POST')) + @login_required + def create(): + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'INSERT INTO post (title, body, author_id)' + ' VALUES (?, ?, ?)', + (title, body, g.user['id']) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/create.html') + +.. code-block:: html+jinja + :caption: ``flaskr/templates/blog/create.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}New Post{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+ {% endblock %} + + +Update +------ + +Both the ``update`` and ``delete`` views will need to fetch a ``post`` +by ``id`` and check if the author matches the logged in user. To avoid +duplicating code, you can write a function to get the ``post`` and call +it from each view. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + def get_post(id, check_author=True): + post = get_db().execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' WHERE p.id = ?', + (id,) + ).fetchone() + + if post is None: + abort(404, "Post id {0} doesn't exist.".format(id)) + + if check_author and post['author_id'] != g.user['id']: + abort(403) + + return post + +:func:`abort` will raise a special exception that returns an HTTP status +code. It takes an optional message to show with the error, otherwise a +default message is used. ``404`` means "Not Found", and ``403`` means +"Forbidden". (``401`` means "Unauthorized", but you redirect to the +login page instead of returning that status.) + +The ``check_author`` argument is defined so that the function can be +used to get a ``post`` without checking the author. This would be useful +if you wrote a view to show an individual post on a page, where the user +doesn't matter because they're not modifying the post. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('//update', methods=('GET', 'POST')) + @login_required + def update(id): + post = get_post(id) + + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'UPDATE post SET title = ?, body = ?' + ' WHERE id = ?', + (title, body, id) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/update.html', post=post) + +Unlike the views you've written so far, the ``update`` function takes +an argument, ``id``. That corresponds to the ```` in the route. +A real URL will look like ``/1/update``. Flask will capture the ``1``, +ensure it's an :class:`int`, and pass it as the ``id`` argument. If you +don't specify ``int:`` and instead do ````, it will be a string. +To generate a URL to the update page, :func:`url_for` needs to be passed +the ``id`` so it knows what to fill in: +``url_for('blog.update', id=post['id'])``. This is also in the +``index.html`` file above. + +The ``create`` and ``update`` views look very similar. The main +difference is that the ``update`` view uses a ``post`` object and an +``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, +you could use one view and template for both actions, but for the +tutorial it's clearer to keep them separate. + +.. code-block:: html+jinja + :caption: ``flaskr/templates/blog/update.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+
+
+ +
+ {% endblock %} + +This template has two forms. The first posts the edited data to the +current page (``//update``). The other form contains only a button +and specifies an ``action`` attribute that posts to the delete view +instead. The button uses some JavaScript to show a confirmation dialog +before submitting. + +The pattern ``{{ request.form['title'] or post['title'] }}`` is used to +choose what data appears in the form. When the form hasn't been +submitted, the original ``post`` data appears, but if invalid form data +was posted you want to display that so the user can fix the error, so +``request.form`` is used instead. :data:`request` is another variable +that's automatically available in templates. + + +Delete +------ + +The delete view doesn't have its own template, the delete button is part +of ``update.html`` and posts to the ``//delete`` URL. Since there +is no template, it will only handle the ``POST`` method then redirect +to the ``index`` view. + +.. code-block:: python + :caption: ``flaskr/blog.py`` + + @bp.route('//delete', methods=('POST',)) + @login_required + def delete(id): + get_post(id) + db = get_db() + db.execute('DELETE FROM post WHERE id = ?', (id,)) + db.commit() + return redirect(url_for('blog.index')) + +Congratulations, you've now finished writing your application! Take some +time to try out everything in the browser. However, there's still more +to do before the project is complete. + +Continue to :doc:`install`. diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst deleted file mode 100644 index 56414657c1..0000000000 --- a/docs/tutorial/css.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _tutorial-css: - -Step 8: Adding Style -==================== - -Now that everything else works, it's time to add some style to the -application. Just create a stylesheet called :file:`style.css` in the -:file:`static` folder: - -.. sourcecode:: css - - body { font-family: sans-serif; background: #eee; } - a, h1, h2 { color: #377ba8; } - h1, h2 { font-family: 'Georgia', serif; margin: 0; } - h1 { border-bottom: 2px solid #eee; } - h2 { font-size: 1.2em; } - - .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; - padding: 0.8em; background: white; } - .entries { list-style: none; margin: 0; padding: 0; } - .entries li { margin: 0.8em 1.2em; } - .entries li h2 { margin-left: -1em; } - .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } - .add-entry dl { font-weight: bold; } - .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; - margin-bottom: 1em; background: #fafafa; } - .flash { background: #cee5F5; padding: 0.5em; - border: 1px solid #aacbe2; } - .error { background: #f0d6d6; padding: 0.5em; } - -Continue with :ref:`tutorial-testing`. diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst new file mode 100644 index 0000000000..51f20b615c --- /dev/null +++ b/docs/tutorial/database.rst @@ -0,0 +1,213 @@ +.. currentmodule:: flask + +Define and Access the Database +============================== + +The application will use a `SQLite`_ database to store users and posts. +Python comes with built-in support for SQLite in the :mod:`sqlite3` +module. + +SQLite is convenient because it doesn't require setting up a separate +database server and is built-in to Python. However, if concurrent +requests try to write to the database at the same time, they will slow +down as each write happens sequentially. Small applications won't notice +this. Once you become big, you may want to switch to a different +database. + +The tutorial doesn't go into detail about SQL. If you are not familiar +with it, the SQLite docs describe the `language`_. + +.. _SQLite: https://sqlite.org/about.html +.. _language: https://sqlite.org/lang.html + + +Connect to the Database +----------------------- + +The first thing to do when working with a SQLite database (and most +other Python database libraries) is to create a connection to it. Any +queries and operations are performed using the connection, which is +closed after the work is finished. + +In web applications this connection is typically tied to the request. It +is created at some point when handling a request, and closed before the +response is sent. + +.. code-block:: python + :caption: ``flaskr/db.py`` + + import sqlite3 + + import click + from flask import current_app, g + from flask.cli import with_appcontext + + + def get_db(): + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + + def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +:data:`g` is a special object that is unique for each request. It is +used to store data that might be accessed by multiple functions during +the request. The connection is stored and reused instead of creating a +new connection if ``get_db`` is called a second time in the same +request. + +:data:`current_app` is another special object that points to the Flask +application handling the request. Since you used an application factory, +there is no application object when writing the rest of your code. +``get_db`` will be called when the application has been created and is +handling a request, so :data:`current_app` can be used. + +:func:`sqlite3.connect` establishes a connection to the file pointed at +by the ``DATABASE`` configuration key. This file doesn't have to exist +yet, and won't until you initialize the database later. + +:class:`sqlite3.Row` tells the connection to return rows that behave +like dicts. This allows accessing the columns by name. + +``close_db`` checks if a connection was created by checking if ``g.db`` +was set. If the connection exists, it is closed. Further down you will +tell your application about the ``close_db`` function in the application +factory so that it is called after each request. + + +Create the Tables +----------------- + +In SQLite, data is stored in *tables* and *columns*. These need to be +created before you can store and retrieve data. Flaskr will store users +in the ``user`` table, and posts in the ``post`` table. Create a file +with the SQL commands needed to create empty tables: + +.. code-block:: sql + :caption: ``flaskr/schema.sql`` + + DROP TABLE IF EXISTS user; + DROP TABLE IF EXISTS post; + + CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL + ); + + CREATE TABLE post ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title TEXT NOT NULL, + body TEXT NOT NULL, + FOREIGN KEY (author_id) REFERENCES user (id) + ); + +Add the Python functions that will run these SQL commands to the +``db.py`` file: + +.. code-block:: python + :caption: ``flaskr/db.py`` + + def init_db(): + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + + + @click.command('init-db') + @with_appcontext + def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +:meth:`open_resource() ` opens a file relative to +the ``flaskr`` package, which is useful since you won't necessarily know +where that location is when deploying the application later. ``get_db`` +returns a database connection, which is used to execute the commands +read from the file. + +:func:`click.command` defines a command line command called ``init-db`` +that calls the ``init_db`` function and shows a success message to the +user. You can read :ref:`cli` to learn more about writing commands. + + +Register with the Application +----------------------------- + +The ``close_db`` and ``init_db_command`` functions need to be registered +with the application instance, otherwise they won't be used by the +application. However, since you're using a factory function, that +instance isn't available when writing the functions. Instead, write a +function that takes an application and does the registration. + +.. code-block:: python + :caption: ``flaskr/db.py`` + + def init_app(app): + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) + +:meth:`app.teardown_appcontext() ` tells +Flask to call that function when cleaning up after returning the +response. + +:meth:`app.cli.add_command() ` adds a new +command that can be called with the ``flask`` command. + +Import and call this function from the factory. Place the new code at +the end of the factory function before returning the app. + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + def create_app(): + app = ... + # existing code omitted + + from . import db + db.init_app(app) + + return app + + +Initialize the Database File +---------------------------- + +Now that ``init-db`` has been registered with the app, it can be called +using the ``flask`` command, similar to the ``run`` command from the +previous page. + +.. note:: + + If you're still running the server from the previous page, you can + either stop the server, or run this command in a new terminal. If + you use a new terminal, remember to change to your project directory + and activate the env as described in :ref:`install-activate-env`. + You'll also need to set ``FLASK_APP`` and ``FLASK_ENV`` as shown on + the previous page. + +Run the ``init-db`` command: + +.. code-block:: none + + flask init-db + Initialized the database. + +There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in +your project. + +Continue to :doc:`views`. diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst deleted file mode 100644 index 179c962b90..0000000000 --- a/docs/tutorial/dbcon.rst +++ /dev/null @@ -1,78 +0,0 @@ -.. _tutorial-dbcon: - -Step 4: Database Connections ----------------------------- - -Let's continue building our code in the ``flaskr.py`` file. -(Scroll to the end of the page for more about project layout.) - -You currently have a function for establishing a database connection with -`connect_db`, but by itself, it is not particularly useful. Creating and -closing database connections all the time is very inefficient, so you will -need to keep it around for longer. Because database connections -encapsulate a transaction, you will need to make sure that only one -request at a time uses the connection. An elegant way to do this is by -utilizing the *application context*. - -Flask provides two contexts: the *application context* and the -*request context*. For the time being, all you have to know is that there -are special variables that use these. For instance, the -:data:`~flask.request` variable is the request object associated with -the current request, whereas :data:`~flask.g` is a general purpose -variable associated with the current application context. The tutorial -will cover some more details of this later on. - -For the time being, all you have to know is that you can store information -safely on the :data:`~flask.g` object. - -So when do you put it on there? To do that you can make a helper -function. The first time the function is called, it will create a database -connection for the current context, and successive calls will return the -already established connection:: - - def get_db(): - """Opens a new database connection if there is none yet for the - current application context. - """ - if not hasattr(g, 'sqlite_db'): - g.sqlite_db = connect_db() - return g.sqlite_db - -Now you know how to connect, but how can you properly disconnect? For -that, Flask provides us with the :meth:`~flask.Flask.teardown_appcontext` -decorator. It's executed every time the application context tears down:: - - @app.teardown_appcontext - def close_db(error): - """Closes the database again at the end of the request.""" - if hasattr(g, 'sqlite_db'): - g.sqlite_db.close() - -Functions marked with :meth:`~flask.Flask.teardown_appcontext` are called -every time the app context tears down. What does this mean? -Essentially, the app context is created before the request comes in and is -destroyed (torn down) whenever the request finishes. A teardown can -happen because of two reasons: either everything went well (the error -parameter will be ``None``) or an exception happened, in which case the error -is passed to the teardown function. - -Curious about what these contexts mean? Have a look at the -:ref:`app-context` documentation to learn more. - -Continue to :ref:`tutorial-dbinit`. - -.. hint:: Where do I put this code? - - If you've been following along in this tutorial, you might be wondering - where to put the code from this step and the next. A logical place is to - group these module-level functions together, and put your new - ``get_db`` and ``close_db`` functions below your existing - ``connect_db`` function (following the tutorial line-by-line). - - If you need a moment to find your bearings, take a look at how the `example - source`_ is organized. In Flask, you can put all of your application code - into a single Python module. You don't have to, and if your app :ref:`grows - larger `, it's a good idea not to. - -.. _example source: - https://github.com/pallets/flask/tree/master/examples/flaskr/ diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst deleted file mode 100644 index 484354ba08..0000000000 --- a/docs/tutorial/dbinit.rst +++ /dev/null @@ -1,80 +0,0 @@ -.. _tutorial-dbinit: - -Step 5: Creating The Database -============================= - -As outlined earlier, Flaskr is a database powered application, and more -precisely, it is an application powered by a relational database system. Such -systems need a schema that tells them how to store that information. -Before starting the server for the first time, it's important to create -that schema. - -Such a schema could be created by piping the ``schema.sql`` file into the -``sqlite3`` command as follows:: - - sqlite3 /tmp/flaskr.db < schema.sql - -However, the downside of this is that it requires the ``sqlite3`` command -to be installed, which is not necessarily the case on every system. This -also requires that you provide the path to the database, which can introduce -errors. - -Instead of the ``sqlite3`` command above, it's a good idea to add a function -to our application that initializes the database for you. To do this, you -can create a function and hook it into a :command:`flask` command that -initializes the database. - -Take a look at the code segment below. A good place to add this function, -and command, is just below the ``connect_db`` function in :file:`flaskr.py`:: - - def init_db(): - db = get_db() - - with app.open_resource('schema.sql', mode='r') as f: - db.cursor().executescript(f.read()) - - db.commit() - - - @app.cli.command('initdb') - def initdb_command(): - """Initializes the database.""" - - init_db() - print('Initialized the database.') - -The ``app.cli.command()`` decorator registers a new command with the -:command:`flask` script. When the command executes, Flask will automatically -create an application context which is bound to the right application. -Within the function, you can then access :attr:`flask.g` and other things as -you might expect. When the script ends, the application context tears down -and the database connection is released. - -You will want to keep an actual function around that initializes the database, -though, so that we can easily create databases in unit tests later on. (For -more information see :ref:`testing`.) - -The :func:`~flask.Flask.open_resource` method of the application object -is a convenient helper function that will open a resource that the -application provides. This function opens a file from the resource -location (the :file:`flaskr/flaskr` folder) and allows you to read from it. -It is used in this example to execute a script on the database connection. - -The connection object provided by SQLite can give you a cursor object. -On that cursor, there is a method to execute a complete script. Finally, you -only have to commit the changes. SQLite3 and other transactional -databases will not commit unless you explicitly tell it to. - -Now, in a terminal, from the application root directory :file:`flaskr/` it is -possible to create a database with the :command:`flask` script:: - - flask initdb - Initialized the database. - -.. admonition:: Troubleshooting - - If you get an exception later on stating that a table cannot be found, check - that you did execute the ``initdb`` command and that your table names are - correct (singular vs. plural, for example). - -Continue with :ref:`tutorial-views` diff --git a/docs/tutorial/deploy.rst b/docs/tutorial/deploy.rst new file mode 100644 index 0000000000..a0c052ea6f --- /dev/null +++ b/docs/tutorial/deploy.rst @@ -0,0 +1,121 @@ +Deploy to Production +==================== + +This part of the tutorial assumes you have a server that you want to +deploy your application to. It gives an overview of how to create the +distribution file and install it, but won't go into specifics about +what server or software to use. You can set up a new environment on your +development computer to try out the instructions below, but probably +shouldn't use it for hosting a real public application. See +:doc:`/deploying/index` for a list of many different ways to host your +application. + + +Build and Install +----------------- + +When you want to deploy your application elsewhere, you build a +distribution file. The current standard for Python distribution is the +*wheel* format, with the ``.whl`` extension. Make sure the wheel library +is installed first: + +.. code-block:: none + + pip install wheel + +Running ``setup.py`` with Python gives you a command line tool to issue +build-related commands. The ``bdist_wheel`` command will build a wheel +distribution file. + +.. code-block:: none + + python setup.py bdist_wheel + +You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The +file name is the name of the project, the version, and some tags about +the file can install. + +Copy this file to another machine, +:ref:`set up a new virtualenv `, then install the +file with ``pip``. + +.. code-block:: none + + pip install flaskr-1.0.0-py3-none-any.whl + +Pip will install your project along with its dependencies. + +Since this is a different machine, you need to run ``init-db`` again to +create the database in the instance folder. + +.. code-block:: none + + export FLASK_APP=flaskr + flask init-db + +When Flask detects that it's installed (not in editable mode), it uses +a different directory for the instance folder. You can find it at +``venv/var/flaskr-instance`` instead. + + +Configure the Secret Key +------------------------ + +In the beginning of the tutorial that you gave a default value for +:data:`SECRET_KEY`. This should be changed to some random bytes in +production. Otherwise, attackers could use the public ``'dev'`` key to +modify the session cookie, or anything else that uses the secret key. + +You can use the following command to output a random secret key: + +.. code-block:: none + + python -c 'import os; print(os.urandom(16))' + + b'_5#y2L"F4Q8z\n\xec]/' + +Create the ``config.py`` file in the instance folder, which the factory +will read from if it exists. Copy the generated value into it. + +.. code-block:: python + :caption: ``venv/var/flaskr-instance/config.py`` + + SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' + +You can also set any other necessary configuration here, although +``SECRET_KEY`` is the only one needed for Flaskr. + + +Run with a Production Server +---------------------------- + +When running publicly rather than in development, you should not use the +built-in development server (``flask run``). The development server is +provided by Werkzeug for convenience, but is not designed to be +particularly efficient, stable, or secure. + +Instead, use a production WSGI server. For example, to use `Waitress`_, +first install it in the virtual environment: + +.. code-block:: none + + pip install waitress + +You need to tell Waitress about your application, but it doesn't use +``FLASK_APP`` like ``flask run`` does. You need to tell it to import and +call the application factory to get an application object. + +.. code-block:: none + + waitress-serve --call 'flaskr:create_app' + + Serving on http://0.0.0.0:8080 + +See :doc:`/deploying/index` for a list of many different ways to host +your application. Waitress is just an example, chosen for the tutorial +because it supports both Windows and Linux. There are many more WSGI +servers and deployment options that you may choose for your project. + +.. _Waitress: https://docs.pylonsproject.org/projects/waitress/ + +Continue to :doc:`next`. diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst new file mode 100644 index 0000000000..62462e1cd7 --- /dev/null +++ b/docs/tutorial/factory.rst @@ -0,0 +1,177 @@ +.. currentmodule:: flask + +Application Setup +================= + +A Flask application is an instance of the :class:`Flask` class. +Everything about the application, such as configuration and URLs, will +be registered with this class. + +The most straightforward way to create a Flask application is to create +a global :class:`Flask` instance directly at the top of your code, like +how the "Hello, World!" example did on the previous page. While this is +simple and useful in some cases, it can cause some tricky issues as the +project grows. + +Instead of creating a :class:`Flask` instance globally, you will create +it inside a function. This function is known as the *application +factory*. Any configuration, registration, and other setup the +application needs will happen inside the function, then the application +will be returned. + + +The Application Factory +----------------------- + +It's time to start coding! Create the ``flaskr`` directory and add the +``__init__.py`` file. The ``__init__.py`` serves double duty: it will +contain the application factory, and it tells Python that the ``flaskr`` +directory should be treated as a package. + +.. code-block:: none + + mkdir flaskr + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + import os + + from flask import Flask + + + def create_app(test_config=None): + # create and configure the app + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.from_mapping(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # a simple page that says hello + @app.route('/hello') + def hello(): + return 'Hello, World!' + + return app + +``create_app`` is the application factory function. You'll add to it +later in the tutorial, but it already does a lot. + +#. ``app = Flask(__name__, instance_relative_config=True)`` creates the + :class:`Flask` instance. + + * ``__name__`` is the name of the current Python module. The app + needs to know where it's located to set up some paths, and + ``__name__`` is a convenient way to tell it that. + + * ``instance_relative_config=True`` tells the app that + configuration files are relative to the + :ref:`instance folder `. The instance folder + is located outside the ``flaskr`` package and can hold local + data that shouldn't be committed to version control, such as + configuration secrets and the database file. + +#. :meth:`app.config.from_mapping() ` sets + some default configuration that the app will use: + + * :data:`SECRET_KEY` is used by Flask and extensions to keep data + safe. It's set to ``'dev'`` to provide a convenient value + during development, but it should be overridden with a random + value when deploying. + + * ``DATABASE`` is the path where the SQLite database file will be + saved. It's under + :attr:`app.instance_path `, which is the + path that Flask has chosen for the instance folder. You'll learn + more about the database in the next section. + +#. :meth:`app.config.from_pyfile() ` overrides + the default configuration with values taken from the ``config.py`` + file in the instance folder if it exists. For example, when + deploying, this can be used to set a real ``SECRET_KEY``. + + * ``test_config`` can also be passed to the factory, and will be + used instead of the instance configuration. This is so the tests + you'll write later in the tutorial can be configured + independently of any development values you have configured. + +#. :func:`os.makedirs` ensures that + :attr:`app.instance_path ` exists. Flask + doesn't create the instance folder automatically, but it needs to be + created because your project will create the SQLite database file + there. + +#. :meth:`@app.route() ` creates a simple route so you can + see the application working before getting into the rest of the + tutorial. It creates a connection between the URL ``/hello`` and a + function that returns a response, the string ``'Hello, World!'`` in + this case. + + +Run The Application +------------------- + +Now you can run your application using the ``flask`` command. From the +terminal, tell Flask where to find your application, then run it in +development mode. + +Development mode shows an interactive debugger whenever a page raises an +exception, and restarts the server whenever you make changes to the +code. You can leave it running and just reload the browser page as you +follow the tutorial. + +For Linux and Mac: + +.. code-block:: none + + export FLASK_APP=flaskr + export FLASK_ENV=development + flask run + +For Windows cmd, use ``set`` instead of ``export``: + +.. code-block:: none + + set FLASK_APP=flaskr + set FLASK_ENV=development + flask run + +For Windows PowerShell, use ``$env:`` instead of ``export``: + +.. code-block:: none + + $env:FLASK_APP = "flaskr" + $env:FLASK_ENV = "development" + flask run + +You'll see output similar to this: + +.. code-block:: none + + * Serving Flask app "flaskr" + * Environment: development + * Debug mode: on + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + * Restarting with stat + * Debugger is active! + * Debugger PIN: 855-212-761 + +Visit http://127.0.0.1:5000/hello in a browser and you should see the +"Hello, World!" message. Congratulations, you're now running your Flask +web application! + +Continue to :doc:`database`. diff --git a/docs/tutorial/flaskr_edit.png b/docs/tutorial/flaskr_edit.png new file mode 100644 index 0000000000..6cd6e3980f Binary files /dev/null and b/docs/tutorial/flaskr_edit.png differ diff --git a/docs/tutorial/flaskr_index.png b/docs/tutorial/flaskr_index.png new file mode 100644 index 0000000000..aa2b50f552 Binary files /dev/null and b/docs/tutorial/flaskr_index.png differ diff --git a/docs/tutorial/flaskr_login.png b/docs/tutorial/flaskr_login.png new file mode 100644 index 0000000000..d482c64541 Binary files /dev/null and b/docs/tutorial/flaskr_login.png differ diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst deleted file mode 100644 index 23fefaece8..0000000000 --- a/docs/tutorial/folders.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. _tutorial-folders: - -Step 0: Creating The Folders -============================ - -It is recommended to install your Flask application within a virtualenv. Please -read the :ref:`installation` section to set up your environment. - -Now that you have installed Flask, you will need to create the folders required -for this tutorial. Your directory structure will look like this:: - - /flaskr - /flaskr - /static - /templates - -The application will be installed and run as Python package. This is the -recommended way to install and run Flask applications. You will see exactly -how to run ``flaskr`` later on in this tutorial. - -For now go ahead and create the applications directory structure. In the next -few steps you will be creating the database schema as well as the main module. - -As a quick side note, the files inside of the :file:`static` folder are -available to users of the application via HTTP. This is the place where CSS and -JavaScript files go. Inside the :file:`templates` folder, Flask will look for -`Jinja2`_ templates. You will see examples of this later on. - -For now you should continue with :ref:`tutorial-schema`. - -.. _Jinja2: http://jinja.pocoo.org/ diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 7eee5fa08d..9b43c510fa 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -3,31 +3,64 @@ Tutorial ======== -Learn by example to develop an application with Python and Flask. +.. toctree:: + :caption: Contents: + :maxdepth: 1 -In this tutorial, we will create a simple blogging application. It only -supports one user, only allows text entries, and has no feeds or comments. + layout + factory + database + views + templates + static + blog + install + tests + deploy + next -While very simple, this example still features everything you need to get -started. In addition to Flask, we will use SQLite for the database, which is -built-in to Python, so there is nothing else you need. +This tutorial will walk you through creating a basic blog application +called Flaskr. Users will be able to register, log in, create posts, +and edit or delete their own posts. You will be able to package and +install the application on other computers. -If you want the full source code in advance or for comparison, check out -the `example source`_. +.. image:: flaskr_index.png + :align: center + :class: screenshot + :alt: screenshot of index page -.. _example source: https://github.com/pallets/flask/tree/master/examples/flaskr/ +It's assumed that you're already familiar with Python. The `official +tutorial`_ in the Python docs is a great way to learn or review first. -.. toctree:: - :maxdepth: 2 - - introduction - folders - schema - setup - packaging - dbcon - dbinit - views - templates - css - testing +.. _official tutorial: https://docs.python.org/3/tutorial/ + +While it's designed to give a good starting point, the tutorial doesn't +cover all of Flask's features. Check out the :ref:`quickstart` for an +overview of what Flask can do, then dive into the docs to find out more. +The tutorial only uses what's provided by Flask and Python. In another +project, you might decide to use :ref:`extensions` or other libraries to +make some tasks simpler. + +.. image:: flaskr_login.png + :align: center + :class: screenshot + :alt: screenshot of login page + +Flask is flexible. It doesn't require you to use any particular project +or code layout. However, when first starting, it's helpful to use a more +structured approach. This means that the tutorial will require a bit of +boilerplate up front, but it's done to avoid many common pitfalls that +new developers encounter, and it creates a project that's easy to expand +on. Once you become more comfortable with Flask, you can step out of +this structure and take full advantage of Flask's flexibility. + +.. image:: flaskr_edit.png + :align: center + :class: screenshot + :alt: screenshot of login page + +:gh:`The tutorial project is available as an example in the Flask +repository `, if you want to compare your project +with the final product as you follow the tutorial. + +Continue to :doc:`layout`. diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst new file mode 100644 index 0000000000..fff0b52ce4 --- /dev/null +++ b/docs/tutorial/install.rst @@ -0,0 +1,113 @@ +Make the Project Installable +============================ + +Making your project installable means that you can build a +*distribution* file and install that in another environment, just like +you installed Flask in your project's environment. This makes deploying +your project the same as installing any other library, so you're using +all the standard Python tools to manage everything. + +Installing also comes with other benefits that might not be obvious from +the tutorial or as a new Python user, including: + +* Currently, Python and Flask understand how to use the ``flaskr`` + package only because you're running from your project's directory. + Installing means you can import it no matter where you run from. + +* You can manage your project's dependencies just like other packages + do, so ``pip install yourproject.whl`` installs them. + +* Test tools can isolate your test environment from your development + environment. + +.. note:: + This is being introduced late in the tutorial, but in your future + projects you should always start with this. + + +Describe the Project +-------------------- + +The ``setup.py`` file describes your project and the files that belong +to it. + +.. code-block:: python + :caption: ``setup.py`` + + from setuptools import find_packages, setup + + setup( + name='flaskr', + version='1.0.0', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=[ + 'flask', + ], + ) + + +``packages`` tells Python what package directories (and the Python files +they contain) to include. ``find_packages()`` finds these directories +automatically so you don't have to type them out. To include other +files, such as the static and templates directories, +``include_package_data`` is set. Python needs another file named +``MANIFEST.in`` to tell what this other data is. + +.. code-block:: none + :caption: ``MANIFEST.in`` + + include flaskr/schema.sql + graft flaskr/static + graft flaskr/templates + global-exclude *.pyc + +This tells Python to copy everything in the ``static`` and ``templates`` +directories, and the ``schema.sql`` file, but to exclude all bytecode +files. + +See the `official packaging guide`_ for another explanation of the files +and options used. + +.. _official packaging guide: https://packaging.python.org/tutorials/distributing-packages/ + + +Install the Project +------------------- + +Use ``pip`` to install your project in the virtual environment. + +.. code-block:: none + + pip install -e . + +This tells pip to find ``setup.py`` in the current directory and install +it in *editable* or *development* mode. Editable mode means that as you +make changes to your local code, you'll only need to re-install if you +change the metadata about the project, such as its dependencies. + +You can observe that the project is now installed with ``pip list``. + +.. code-block:: none + + pip list + + Package Version Location + -------------- --------- ---------------------------------- + click 6.7 + Flask 1.0 + flaskr 1.0.0 /home/user/Projects/flask-tutorial + itsdangerous 0.24 + Jinja2 2.10 + MarkupSafe 1.0 + pip 9.0.3 + setuptools 39.0.1 + Werkzeug 0.14.1 + wheel 0.30.0 + +Nothing changes from how you've been running your project so far. +``FLASK_APP`` is still set to ``flaskr`` and ``flask run`` still runs +the application. + +Continue to :doc:`tests`. diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst deleted file mode 100644 index ed98471550..0000000000 --- a/docs/tutorial/introduction.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. _tutorial-introduction: - -Introducing Flaskr -================== - -This tutorial will demonstrate a blogging application named Flaskr, but feel -free to choose your own less Web-2.0-ish name ;) Essentially, it will do the -following things: - -1. Let the user sign in and out with credentials specified in the - configuration. Only one user is supported. -2. When the user is logged in, they can add new entries to the page - consisting of a text-only title and some HTML for the text. This HTML - is not sanitized because we trust the user here. -3. The index page shows all entries so far in reverse chronological order - (newest on top) and the user can add new ones from there if logged in. - -SQLite3 will be used directly for this application because it's good enough -for an application of this size. For larger applications, however, -it makes a lot of sense to use `SQLAlchemy`_, as it handles database -connections in a more intelligent way, allowing you to target different -relational databases at once and more. You might also want to consider -one of the popular NoSQL databases if your data is more suited for those. - -.. warning:: - If you're following the tutorial from a specific version of the docs, be - sure to check out the same tag in the repository, otherwise the tutorial - may be different than the example. - -Here is a screenshot of the final application: - -.. image:: ../_static/flaskr.png - :align: center - :class: screenshot - :alt: screenshot of the final application - -Continue with :ref:`tutorial-folders`. - -.. _SQLAlchemy: https://www.sqlalchemy.org/ diff --git a/docs/tutorial/layout.rst b/docs/tutorial/layout.rst new file mode 100644 index 0000000000..2d7ddebee8 --- /dev/null +++ b/docs/tutorial/layout.rst @@ -0,0 +1,110 @@ +Project Layout +============== + +Create a project directory and enter it: + +.. code-block:: none + + mkdir flask-tutorial + cd flask-tutorial + +Then follow the :doc:`installation instructions ` to set +up a Python virtual environment and install Flask for your project. + +The tutorial will assume you're working from the ``flask-tutorial`` +directory from now on. The file names at the top of each code block are +relative to this directory. + +---- + +A Flask application can be as simple as a single file. + +.. code-block:: python + :caption: ``hello.py`` + + from flask import Flask + + app = Flask(__name__) + + + @app.route('/') + def hello(): + return 'Hello, World!' + +However, as a project get bigger, it becomes overwhelming to keep all +the code in one file. Python projects use *packages* to organize code +into multiple modules that can be imported where needed, and the +tutorial will do this as well. + +The project directory will contain: + +* ``flaskr/``, a Python package containing your application code and + files. +* ``tests/``, a directory containing test modules. +* ``venv/``, a Python virtual environment where Flask and other + dependencies are installed. +* Installation files telling Python how to install your project. +* Version control config, such as `git`_. You should make a habit of + using some type of version control for all your projects, no matter + the size. +* Any other project files you might add in the future. + +.. _git: https://git-scm.com/ + +By the end, your project layout will look like this: + +.. code-block:: none + + /home/user/Projects/flask-tutorial + ├── flaskr/ + │   ├── __init__.py + │   ├── db.py + │   ├── schema.sql + │   ├── auth.py + │   ├── blog.py + │   ├── templates/ + │   │ ├── base.html + │   │ ├── auth/ + │   │ │   ├── login.html + │   │ │   └── register.html + │   │ └── blog/ + │   │ ├── create.html + │   │ ├── index.html + │   │ └── update.html + │   └── static/ + │      └── style.css + ├── tests/ + │   ├── conftest.py + │   ├── data.sql + │   ├── test_factory.py + │   ├── test_db.py + │  ├── test_auth.py + │  └── test_blog.py + ├── venv/ + ├── setup.py + └── MANIFEST.in + +If you're using version control, the following files that are generated +while running your project should be ignored. There may be other files +based on the editor you use. In general, ignore files that you didn't +write. For example, with git: + +.. code-block:: none + :caption: ``.gitignore`` + + venv/ + + *.pyc + __pycache__/ + + instance/ + + .pytest_cache/ + .coverage + htmlcov/ + + dist/ + build/ + *.egg-info/ + +Continue to :doc:`factory`. diff --git a/docs/tutorial/next.rst b/docs/tutorial/next.rst new file mode 100644 index 0000000000..07bbc04886 --- /dev/null +++ b/docs/tutorial/next.rst @@ -0,0 +1,38 @@ +Keep Developing! +================ + +You've learned about quite a few Flask and Python concepts throughout +the tutorial. Go back and review the tutorial and compare your code with +the steps you took to get there. Compare your project to the +:gh:`example project `, which might look a bit +different due to the step-by-step nature of the tutorial. + +There's a lot more to Flask than what you've seen so far. Even so, +you're now equipped to start developing your own web applications. Check +out the :ref:`quickstart` for an overview of what Flask can do, then +dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_, +`Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have +their own documentation too. You'll also be interested in +:ref:`extensions` which make tasks like working with the database or +validating form data easier and more powerful. + +If you want to keep developing your Flaskr project, here are some ideas +for what to try next: + +* A detail view to show a single post. Click a post's title to go to + its page. +* Like / unlike a post. +* Comments. +* Tags. Clicking a tag shows all the posts with that tag. +* A search box that filters the index page by name. +* Paged display. Only show 5 posts per page. +* Upload an image to go along with a post. +* Format posts using Markdown. +* An RSS feed of new posts. + +Have fun and make awesome applications! + +.. _Jinja: https://palletsprojects.com/p/jinja/ +.. _Click: https://palletsprojects.com/p/click/ +.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ +.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst deleted file mode 100644 index e08f26fadc..0000000000 --- a/docs/tutorial/packaging.rst +++ /dev/null @@ -1,108 +0,0 @@ -.. _tutorial-packaging: - -Step 3: Installing flaskr as a Package -====================================== - -Flask is now shipped with built-in support for `Click`_. Click provides -Flask with enhanced and extensible command line utilities. Later in this -tutorial you will see exactly how to extend the ``flask`` command line -interface (CLI). - -A useful pattern to manage a Flask application is to install your app -following the `Python Packaging Guide`_. Presently this involves -creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the -projects root directory. You also need to add an :file:`__init__.py` -file to make the :file:`flaskr/flaskr` directory a package. After these -changes, your code structure should be:: - - /flaskr - /flaskr - __init__.py - /static - /templates - flaskr.py - schema.sql - setup.py - MANIFEST.in - -Create the ``setup.py`` file for ``flaskr`` with the following content:: - - from setuptools import setup - - setup( - name='flaskr', - packages=['flaskr'], - include_package_data=True, - install_requires=[ - 'flask', - ], - ) - -When using setuptools, it is also necessary to specify any special files -that should be included in your package (in the :file:`MANIFEST.in`). -In this case, the static and templates directories need to be included, -as well as the schema. - -Create the :file:`MANIFEST.in` and add the following lines:: - - graft flaskr/templates - graft flaskr/static - include flaskr/schema.sql - -Next, to simplify locating the application, create the file, -:file:`flaskr/__init__.py` containing only the following import statement:: - - from .flaskr import app - -This import statement brings the application instance into the top-level -of the application package. When it is time to run the application, the -Flask development server needs the location of the app instance. This -import statement simplifies the location process. Without the above -import statement, the export statement a few steps below would need to be -``export FLASK_APP=flaskr.flaskr``. - -At this point you should be able to install the application. As usual, it -is recommended to install your Flask application within a `virtualenv`_. -With that said, from the ``flaskr/`` directory, go ahead and install the -application with:: - - pip install --editable . - -The above installation command assumes that it is run within the projects -root directory, ``flaskr/``. The ``editable`` flag allows editing -source code without having to reinstall the Flask app each time you make -changes. The flaskr app is now installed in your virtualenv (see output -of ``pip freeze``). - -With that out of the way, you should be able to start up the application. -Do this on Mac or Linux with the following commands in ``flaskr/``:: - - export FLASK_APP=flaskr - export FLASK_ENV=development - flask run - -(In case you are on Windows you need to use ``set`` instead of ``export``). -Exporting ``FLASK_ENV=development`` turns on all development features -such as enabling the interactive debugger. - -*Never leave debug mode activated in a production system*, because it will -allow users to execute code on the server! - -You will see a message telling you that server has started along with -the address at which you can access it in a browser. - -When you head over to the server in your browser, you will get a 404 error -because we don't have any views yet. That will be addressed a little later, -but first, you should get the database working. - -.. admonition:: Externally Visible Server - - Want your server to be publicly available? Check out the - :ref:`externally visible server ` section for more - information. - -Continue with :ref:`tutorial-dbcon`. - -.. _Click: http://click.pocoo.org -.. _Python Packaging Guide: https://packaging.python.org -.. _virtualenv: https://virtualenv.pypa.io diff --git a/docs/tutorial/schema.rst b/docs/tutorial/schema.rst deleted file mode 100644 index 00f56f0911..0000000000 --- a/docs/tutorial/schema.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _tutorial-schema: - -Step 1: Database Schema -======================= - -In this step, you will create the database schema. Only a single table is -needed for this application and it will only support SQLite. All you need to do -is put the following contents into a file named :file:`schema.sql` in the -:file:`flaskr/flaskr` folder: - -.. sourcecode:: sql - - drop table if exists entries; - create table entries ( - id integer primary key autoincrement, - title text not null, - 'text' text not null - ); - -This schema consists of a single table called ``entries``. Each row in -this table has an ``id``, a ``title``, and a ``text``. The ``id`` is an -automatically incrementing integer and a primary key, the other two are -strings that must not be null. - -Continue with :ref:`tutorial-setup`. diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst deleted file mode 100644 index 5c69ecca01..0000000000 --- a/docs/tutorial/setup.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. _tutorial-setup: - -Step 2: Application Setup Code -============================== - -Next, we will create the application module, :file:`flaskr.py`. Just like the -:file:`schema.sql` file you created in the previous step, this file should be -placed inside of the :file:`flaskr/flaskr` folder. - -For this tutorial, all the Python code we use will be put into this file -(except for one line in ``__init__.py``, and any testing or optional files you -decide to create). - -The first several lines of code in the application module are the needed import -statements. After that there will be a few lines of configuration code. - -For small applications like ``flaskr``, it is possible to drop the configuration -directly into the module. However, a cleaner solution is to create a separate -``.py`` file, load that, and import the values from there. - -Here are the import statements (in :file:`flaskr.py`):: - - import os - import sqlite3 - - from flask import (Flask, request, session, g, redirect, url_for, abort, - render_template, flash) - -The next couple lines will create the actual application instance and -initialize it with the config from the same file in :file:`flaskr.py`:: - - app = Flask(__name__) # create the application instance :) - app.config.from_object(__name__) # load config from this file , flaskr.py - - # Load default config and override config from an environment variable - app.config.update( - DATABASE=os.path.join(app.root_path, 'flaskr.db'), - SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/', - USERNAME='admin', - PASSWORD='default' - ) - app.config.from_envvar('FLASKR_SETTINGS', silent=True) - -In the above code, the :class:`~flask.Config` object works similarly to a -dictionary, so it can be updated with new values. - -.. admonition:: Database Path - - Operating systems know the concept of a current working directory for - each process. Unfortunately, you cannot depend on this in web - applications because you might have more than one application in the - same process. - - For this reason the ``app.root_path`` attribute can be used to - get the path to the application. Together with the ``os.path`` module, - files can then easily be found. In this example, we place the - database right next to it. - - For a real-world application, it's recommended to use - :ref:`instance-folders` instead. - -Usually, it is a good idea to load a separate, environment-specific -configuration file. Flask allows you to import multiple configurations and it -will use the setting defined in the last import. This enables robust -configuration setups. :meth:`~flask.Config.from_envvar` can help achieve -this. :: - - app.config.from_envvar('FLASKR_SETTINGS', silent=True) - -If you want to do this (not required for this tutorial) simply define the -environment variable :envvar:`FLASKR_SETTINGS` that points to a config file -to be loaded. The silent switch just tells Flask to not complain if no such -environment key is set. - -In addition to that, you can use the :meth:`~flask.Config.from_object` -method on the config object and provide it with an import name of a -module. Flask will then initialize the variable from that module. Note -that in all cases, only variable names that are uppercase are considered. - -The :data:`SECRET_KEY` is needed to keep the client-side sessions secure. -Choose that key wisely and as hard to guess and complex as possible. - -Lastly, add a method that allows for easy connections to the specified -database. :: - - def connect_db(): - """Connects to the specific database.""" - - rv = sqlite3.connect(app.config['DATABASE']) - rv.row_factory = sqlite3.Row - return rv - -This can be used to open a connection on request and also from the -interactive Python shell or a script. This will come in handy later. -You can create a simple database connection through SQLite and then tell -it to use the :class:`sqlite3.Row` object to represent rows. This allows -the rows to be treated as if they were dictionaries instead of tuples. - -In the next section you will see how to run the application. - -Continue with :ref:`tutorial-packaging`. diff --git a/docs/tutorial/static.rst b/docs/tutorial/static.rst new file mode 100644 index 0000000000..29548e049e --- /dev/null +++ b/docs/tutorial/static.rst @@ -0,0 +1,72 @@ +Static Files +============ + +The authentication views and templates work, but they look very plain +right now. Some `CSS`_ can be added to add style to the HTML layout you +constructed. The style won't change, so it's a *static* file rather than +a template. + +Flask automatically adds a ``static`` view that takes a path relative +to the ``flaskr/static`` directory and serves it. The ``base.html`` +template already has a link to the ``style.css`` file: + +.. code-block:: html+jinja + + {{ url_for('static', filename='style.css') }} + +Besides CSS, other types of static files might be files with JavaScript +functions, or a logo image. They are all placed under the +``flaskr/static`` directory and referenced with +``url_for('static', filename='...')``. + +This tutorial isn't focused on how to write CSS, so you can just copy +the following into the ``flaskr/static/style.css`` file: + +.. code-block:: css + :caption: ``flaskr/static/style.css`` + + html { font-family: sans-serif; background: #eee; padding: 1rem; } + body { max-width: 960px; margin: 0 auto; background: white; } + h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } + a { color: #377ba8; } + hr { border: none; border-top: 1px solid lightgray; } + nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } + nav h1 { flex: auto; margin: 0; } + nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } + nav ul { display: flex; list-style: none; margin: 0; padding: 0; } + nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } + .content { padding: 0 1rem 1rem; } + .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } + .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } + .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } + .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } + .post > header > div:first-of-type { flex: auto; } + .post > header h1 { font-size: 1.5em; margin-bottom: 0; } + .post .about { color: slategray; font-style: italic; } + .post .body { white-space: pre-line; } + .content:last-child { margin-bottom: 0; } + .content form { margin: 1em 0; display: flex; flex-direction: column; } + .content label { font-weight: bold; margin-bottom: 0.5em; } + .content input, .content textarea { margin-bottom: 1em; } + .content textarea { min-height: 12em; resize: vertical; } + input.danger { color: #cc2f2e; } + input[type=submit] { align-self: start; min-width: 10em; } + +You can find a less compact version of ``style.css`` in the +:gh:`example code `. + +Go to http://127.0.0.1/auth/login and the page should look like the +screenshot below. + +.. image:: flaskr_login.png + :align: center + :class: screenshot + :alt: screenshot of login page + +You can read more about CSS from `Mozilla's documentation `_. If +you change a static file, refresh the browser page. If the change +doesn't show up, try clearing your browser's cache. + +.. _CSS: https://developer.mozilla.org/docs/Web/CSS + +Continue to :doc:`blog`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst index 12a555e7b8..226081c939 100644 --- a/docs/tutorial/templates.rst +++ b/docs/tutorial/templates.rst @@ -1,113 +1,187 @@ -.. _tutorial-templates: +.. currentmodule:: flask + +Templates +========= + +You've written the authentication views for your application, but if +you're running the server and try to go to any of the URLs, you'll see a +``TemplateNotFound`` error. That's because the views are calling +:func:`render_template`, but you haven't written the templates yet. +The template files will be stored in the ``templates`` directory inside +the ``flaskr`` package. + +Templates are files that contain static data as well as placeholders +for dynamic data. A template is rendered with specific data to produce a +final document. Flask uses the `Jinja`_ template library to render +templates. + +In your application, you will use templates to render `HTML`_ which +will display in the user's browser. In Flask, Jinja is configured to +*autoescape* any data that is rendered in HTML templates. This means +that it's safe to render user input; any characters they've entered that +could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with +*safe* values that look the same in the browser but don't cause unwanted +effects. + +Jinja looks and behaves mostly like Python. Special delimiters are used +to distinguish Jinja syntax from the static data in the template. +Anything between ``{{`` and ``}}`` is an expression that will be output +to the final document. ``{%`` and ``%}`` denotes a control flow +statement like ``if`` and ``for``. Unlike Python, blocks are denoted +by start and end tags rather than indentation since static text within +a block could change indentation. + +.. _Jinja: http://jinja.pocoo.org/docs/templates/ +.. _HTML: https://developer.mozilla.org/docs/Web/HTML + + +The Base Layout +--------------- + +Each page in the application will have the same basic layout around a +different body. Instead of writing the entire HTML structure in each +template, each template will *extend* a base template and override +specific sections. + +.. code-block:: html+jinja + :caption: ``flaskr/templates/base.html`` -Step 7: The Templates -===================== + + {% block title %}{% endblock %} - Flaskr + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
-Now it is time to start working on the templates. As you may have -noticed, if you make requests with the app running, you will get -an exception that Flask cannot find the templates. The templates -are using `Jinja2`_ syntax and have autoescaping enabled by -default. This means that unless you mark a value in the code with -:class:`~flask.Markup` or with the ``|safe`` filter in the template, -Jinja2 will ensure that special characters such as ``<`` or ``>`` are -escaped with their XML equivalents. +:data:`g` is automatically available in templates. Based on if +``g.user`` is set (from ``load_logged_in_user``), either the username +and a log out link are displayed, otherwise links to register and log in +are displayed. :func:`url_for` is also automatically available, and is +used to generate URLs to views instead of writing them out manually. -We are also using template inheritance which makes it possible to reuse -the layout of the website in all pages. +After the page title, and before the content, the template loops over +each message returned by :func:`get_flashed_messages`. You used +:func:`flash` in the views to show error messages, and this is the code +that will display them. -Create the follwing three HTML files and place them in the -:file:`templates` folder: +There are three blocks defined here that will be overridden in the other +templates: -.. _Jinja2: http://jinja.pocoo.org/docs/templates +#. ``{% block title %}`` will change the title displayed in the + browser's tab and window title. -layout.html ------------ +#. ``{% block header %}`` is similar to ``title`` but will change the + title displayed on the page. -This template contains the HTML skeleton, the header and a link to log in -(or log out if the user was already logged in). It also displays the -flashed messages if there are any. The ``{% block body %}`` block can be -replaced by a block of the same name (``body``) in a child template. +#. ``{% block content %}`` is where the content of each page goes, such + as the login form or a blog post. -The :class:`~flask.session` dict is available in the template as well and -you can use that to check if the user is logged in or not. Note that in -Jinja you can access missing attributes and items of objects / dicts which -makes the following code work, even if there is no ``'logged_in'`` key in -the session: +The base template is directly in the ``templates`` directory. To keep +the others organized, the templates for a blueprint will be placed in a +directory with the same name as the blueprint. -.. sourcecode:: html+jinja - - Flaskr - -
-

Flaskr

-
- {% if not session.logged_in %} - log in - {% else %} - log out - {% endif %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block body %}{% endblock %} -
- -show_entries.html ------------------ - -This template extends the :file:`layout.html` template from above to display the -messages. Note that the ``for`` loop iterates over the messages we passed -in with the :func:`~flask.render_template` function. Notice that the form is -configured to submit to the `add_entry` view function and use ``POST`` as -HTTP method: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} - {% if session.logged_in %} -
-
-
Title: -
-
Text: -
-
-
-
- {% endif %} -
    - {% for entry in entries %} -
  • {{ entry.title }}

    {{ entry.text|safe }}
  • - {% else %} -
  • Unbelievable. No entries here so far
  • - {% endfor %} -
+Register +-------- + +.. code-block:: html+jinja + :caption: ``flaskr/templates/auth/register.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Register{% endblock %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+ {% endblock %} + +``{% extends 'base.html' %}`` tells Jinja that this template should +replace the blocks from the base template. All the rendered content must +appear inside ``{% block %}`` tags that override blocks from the base +template. + +A useful pattern used here is to place ``{% block title %}`` inside +``{% block header %}``. This will set the title block and then output +the value of it into the header block, so that both the window and page +share the same title without writing it twice. + +The ``input`` tags are using the ``required`` attribute here. This tells +the browser not to submit the form until those fields are filled in. If +the user is using an older browser that doesn't support that attribute, +or if they are using something besides a browser to make requests, you +still want to validate the data in the Flask view. It's important to +always fully validate the data on the server, even if the client does +some validation as well. + + +Log In +------ + +This is identical to the register template except for the title and +submit button. + +.. code-block:: html+jinja + :caption: ``flaskr/templates/auth/login.html`` + + {% extends 'base.html' %} + + {% block header %} +

{% block title %}Log In{% endblock %}

{% endblock %} -login.html ----------- - -This is the login template, which basically just displays a form to allow -the user to login: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

Login

- {% if error %}

Error: {{ error }}{% endif %} -

-
-
Username: -
-
Password: -
-
-
+ {% block content %} + + + + + +
{% endblock %} -Continue with :ref:`tutorial-css`. + +Register A User +--------------- + +Now that the authentication templates are written, you can register a +user. Make sure the server is still running (``flask run`` if it's not), +then go to http://127.0.0.1:5000/auth/register. + +Try clicking the "Register" button without filling out the form and see +that the browser shows an error message. Try removing the ``required`` +attributes from the ``register.html`` template and click "Register" +again. Instead of the browser showing an error, the page will reload and +the error from :func:`flash` in the view will be shown. + +Fill out a username and password and you'll be redirected to the login +page. Try entering an incorrect username, or the correct username and +incorrect password. If you log in you'll get an error because there's +no ``index`` view to redirect to yet. + +Continue to :doc:`static`. diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst deleted file mode 100644 index 260993752d..0000000000 --- a/docs/tutorial/testing.rst +++ /dev/null @@ -1,96 +0,0 @@ -.. _tutorial-testing: - -Bonus: Testing the Application -============================== - -Now that you have finished the application and everything works as -expected, it's probably not a bad idea to add automated tests to simplify -modifications in the future. The application above is used as a basic -example of how to perform unit testing in the :ref:`testing` section of the -documentation. Go there to see how easy it is to test Flask applications. - -Adding tests to flaskr ----------------------- - -Assuming you have seen the :ref:`testing` section and have either written -your own tests for ``flaskr`` or have followed along with the examples -provided, you might be wondering about ways to organize the project. - -One possible and recommended project structure is:: - - flaskr/ - flaskr/ - __init__.py - static/ - templates/ - tests/ - test_flaskr.py - setup.py - MANIFEST.in - -For now go ahead a create the :file:`tests/` directory as well as the -:file:`test_flaskr.py` file. - -Running the tests ------------------ - -At this point you can run the tests. Here ``pytest`` will be used. - -.. note:: Make sure that ``pytest`` is installed in the same virtualenv - as flaskr. Otherwise ``pytest`` test will not be able to import the - required components to test the application:: - - pip install -e . - pip install pytest - -Run and watch the tests pass, within the top-level :file:`flaskr/` -directory as:: - - pytest - -Testing + setuptools --------------------- - -One way to handle testing is to integrate it with ``setuptools``. Here -that requires adding a couple of lines to the :file:`setup.py` file and -creating a new file :file:`setup.cfg`. One benefit of running the tests -this way is that you do not have to install ``pytest``. Go ahead and -update the :file:`setup.py` file to contain:: - - from setuptools import setup - - setup( - name='flaskr', - packages=['flaskr'], - include_package_data=True, - install_requires=[ - 'flask', - ], - setup_requires=[ - 'pytest-runner', - ], - tests_require=[ - 'pytest', - ], - ) - -Now create :file:`setup.cfg` in the project root (alongside -:file:`setup.py`):: - - [aliases] - test=pytest - -Now you can run:: - - python setup.py test - -This calls on the alias created in :file:`setup.cfg` which in turn runs -``pytest`` via ``pytest-runner``, as the :file:`setup.py` script has -been called. (Recall the `setup_requires` argument in :file:`setup.py`) -Following the standard rules of test-discovery your tests will be -found, run, and hopefully pass. - -This is one possible way to run and manage testing. Here ``pytest`` is -used, but there are other options such as ``nose``. Integrating testing -with ``setuptools`` is convenient because it is not necessary to actually -download ``pytest`` or any other testing framework one might use. diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst new file mode 100644 index 0000000000..565450f9a4 --- /dev/null +++ b/docs/tutorial/tests.rst @@ -0,0 +1,561 @@ +.. currentmodule:: flask + +Test Coverage +============= + +Writing unit tests for your application lets you check that the code +you wrote works the way you expect. Flask provides a test client that +simulates requests to the application and returns the response data. + +You should test as much of your code as possible. Code in functions only +runs when the function is called, and code in branches, such as ``if`` +blocks, only runs when the condition is met. You want to make sure that +each function is tested with data that covers each branch. + +The closer you get to 100% coverage, the more comfortable you can be +that making a change won't unexpectedly change other behavior. However, +100% coverage doesn't guarantee that your application doesn't have bugs. +In particular, it doesn't test how the user interacts with the +application in the browser. Despite this, test coverage is an important +tool to use during development. + +.. note:: + This is being introduced late in the tutorial, but in your future + projects you should test as you develop. + +You'll use `pytest`_ and `coverage`_ to test and measure your code. +Install them both: + +.. code-block:: none + + pip install pytest coverage + +.. _pytest: https://pytest.readthedocs.io/ +.. _coverage: https://coverage.readthedocs.io/ + + +Setup and Fixtures +------------------ + +The test code is located in the ``tests`` directory. This directory is +*next to* the ``flaskr`` package, not inside it. The +``tests/conftest.py`` file contains setup functions called *fixtures* +that each test will use. Tests are in Python modules that start with +``test_``, and each test function in those modules also starts with +``test_``. + +Each test will create a new temporary database file and populate some +data that will be used in the tests. Write a SQL file to insert that +data. + +.. code-block:: sql + :caption: ``tests/data.sql`` + + INSERT INTO user (username, password) + VALUES + ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), + ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); + + INSERT INTO post (title, body, author_id, created) + VALUES + ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); + +The ``app`` fixture will call the factory and pass ``test_config`` to +configure the application and database for testing instead of using your +local development configuration. + +.. code-block:: python + :caption: ``tests/conftest.py`` + + import os + import tempfile + + import pytest + from flaskr import create_app + from flaskr.db import get_db, init_db + + with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: + _data_sql = f.read().decode('utf8') + + + @pytest.fixture + def app(): + db_fd, db_path = tempfile.mkstemp() + + app = create_app({ + 'TESTING': True, + 'DATABASE': db_path, + }) + + with app.app_context(): + init_db() + get_db().executescript(_data_sql) + + yield app + + os.close(db_fd) + os.unlink(db_path) + + + @pytest.fixture + def client(app): + return app.test_client() + + + @pytest.fixture + def runner(app): + return app.test_cli_runner() + +:func:`tempfile.mkstemp` creates and opens a temporary file, returning +the file object and the path to it. The ``DATABASE`` path is +overridden so it points to this temporary path instead of the instance +folder. After setting the path, the database tables are created and the +test data is inserted. After the test is over, the temporary file is +closed and removed. + +:data:`TESTING` tells Flask that the app is in test mode. Flask changes +some internal behavior so it's easier to test, and other extensions can +also use the flag to make testing them easier. + +The ``client`` fixture calls +:meth:`app.test_client() ` with the application +object created by the ``app`` fixture. Tests will use the client to make +requests to the application without running the server. + +The ``runner`` fixture is similar to ``client``. +:meth:`app.test_cli_runner() ` creates a runner +that can call the Click commands registered with the application. + +Pytest uses fixtures by matching their function names with the names +of arguments in the test functions. For example, the ``test_hello`` +function you'll write next takes a ``client`` argument. Pytest matches +that with the ``client`` fixture function, calls it, and passes the +returned value to the test function. + + +Factory +------- + +There's not much to test about the factory itself. Most of the code will +be executed for each test already, so if something fails the other tests +will notice. + +The only behavior that can change is passing test config. If config is +not passed, there should be some default configuration, otherwise the +configuration should be overridden. + +.. code-block:: python + :caption: ``tests/test_factory.py`` + + from flaskr import create_app + + + def test_config(): + assert not create_app().testing + assert create_app({'TESTING': True}).testing + + + def test_hello(client): + response = client.get('/hello') + assert response.data == b'Hello, World!' + +You added the ``hello`` route as an example when writing the factory at +the beginning of the tutorial. It returns "Hello, World!", so the test +checks that the response data matches. + + +Database +-------- + +Within an application context, ``get_db`` should return the same +connection each time it's called. After the context, the connection +should be closed. + +.. code-block:: python + :caption: ``tests/test_db.py`` + + import sqlite3 + + import pytest + from flaskr.db import get_db + + + def test_get_close_db(app): + with app.app_context(): + db = get_db() + assert db is get_db() + + with pytest.raises(sqlite3.ProgrammingError) as e: + db.execute('SELECT 1') + + assert 'closed' in str(e) + +The ``init-db`` command should call the ``init_db`` function and output +a message. + +.. code-block:: python + :caption: ``tests/test_db.py`` + + def test_init_db_command(runner, monkeypatch): + class Recorder(object): + called = False + + def fake_init_db(): + Recorder.called = True + + monkeypatch.setattr('flaskr.db.init_db', fake_init_db) + result = runner.invoke(args=['init-db']) + assert 'Initialized' in result.output + assert Recorder.called + +This test uses Pytest's ``monkeypatch`` fixture to replace the +``init_db`` function with one that records that it's been called. The +``runner`` fixture you wrote above is used to call the ``init-db`` +command by name. + + +Authentication +-------------- + +For most of the views, a user needs to be logged in. The easiest way to +do this in tests is to make a ``POST`` request to the ``login`` view +with the client. Rather than writing that out every time, you can write +a class with methods to do that, and use a fixture to pass it the client +for each test. + +.. code-block:: python + :caption: ``tests/conftest.py`` + + class AuthActions(object): + def __init__(self, client): + self._client = client + + def login(self, username='test', password='test'): + return self._client.post( + '/auth/login', + data={'username': username, 'password': password} + ) + + def logout(self): + return self._client.get('/auth/logout') + + + @pytest.fixture + def auth(client): + return AuthActions(client) + +With the ``auth`` fixture, you can call ``auth.login()`` in a test to +log in as the ``test`` user, which was inserted as part of the test +data in the ``app`` fixture. + +The ``register`` view should render successfully on ``GET``. On ``POST`` +with valid form data, it should redirect to the login URL and the user's +data should be in the database. Invalid data should display error +messages. + +.. code-block:: python + :caption: ``tests/test_auth.py`` + + import pytest + from flask import g, session + from flaskr.db import get_db + + + def test_register(client, app): + assert client.get('/auth/register').status_code == 200 + response = client.post( + '/auth/register', data={'username': 'a', 'password': 'a'} + ) + assert 'http://localhost/auth/login' == response.headers['Location'] + + with app.app_context(): + assert get_db().execute( + "select * from user where username = 'a'", + ).fetchone() is not None + + + @pytest.mark.parametrize(('username', 'password', 'message'), ( + ('', '', b'Username is required.'), + ('a', '', b'Password is required.'), + ('test', 'test', b'already registered'), + )) + def test_register_validate_input(client, username, password, message): + response = client.post( + '/auth/register', + data={'username': username, 'password': password} + ) + assert message in response.data + +:meth:`client.get() ` makes a ``GET`` request +and returns the :class:`Response` object returned by Flask. Similarly, +:meth:`client.post() ` makes a ``POST`` +request, converting the ``data`` dict into form data. + +To test that the page renders successfully, a simple request is made and +checked for a ``200 OK`` :attr:`~Response.status_code`. If +rendering failed, Flask would return a ``500 Internal Server Error`` +code. + +:attr:`~Response.headers` will have a ``Location`` header with the login +URL when the register view redirects to the login view. + +:attr:`~Response.data` contains the body of the response as bytes. If +you expect a certain value to render on the page, check that it's in +``data``. Bytes must be compared to bytes. If you want to compare +Unicode text, use :meth:`get_data(as_text=True) ` +instead. + +``pytest.mark.parametrize`` tells Pytest to run the same test function +with different arguments. You use it here to test different invalid +input and error messages without writing the same code three times. + +The tests for the ``login`` view are very similar to those for +``register``. Rather than testing the data in the database, +:data:`session` should have ``user_id`` set after logging in. + +.. code-block:: python + :caption: ``tests/test_auth.py`` + + def test_login(client, auth): + assert client.get('/auth/login').status_code == 200 + response = auth.login() + assert response.headers['Location'] == 'http://localhost/' + + with client: + client.get('/') + assert session['user_id'] == 1 + assert g.user['username'] == 'test' + + + @pytest.mark.parametrize(('username', 'password', 'message'), ( + ('a', 'test', b'Incorrect username.'), + ('test', 'a', b'Incorrect password.'), + )) + def test_login_validate_input(auth, username, password, message): + response = auth.login(username, password) + assert message in response.data + +Using ``client`` in a ``with`` block allows accessing context variables +such as :data:`session` after the response is returned. Normally, +accessing ``session`` outside of a request would raise an error. + +Testing ``logout`` is the opposite of ``login``. :data:`session` should +not contain ``user_id`` after logging out. + +.. code-block:: python + :caption: ``tests/test_auth.py`` + + def test_logout(client, auth): + auth.login() + + with client: + auth.logout() + assert 'user_id' not in session + + +Blog +---- + +All the blog views use the ``auth`` fixture you wrote earlier. Call +``auth.login()`` and subsequent requests from the client will be logged +in as the ``test`` user. + +The ``index`` view should display information about the post that was +added with the test data. When logged in as the author, there should be +a link to edit the post. + +You can also test some more authentication behavior while testing the +``index`` view. When not logged in, each page shows links to log in or +register. When logged in, there's a link to log out. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + import pytest + from flaskr.db import get_db + + + def test_index(client, auth): + response = client.get('/') + assert b"Log In" in response.data + assert b"Register" in response.data + + auth.login() + response = client.get('/') + assert b'Log Out' in response.data + assert b'test title' in response.data + assert b'by test on 2018-01-01' in response.data + assert b'test\nbody' in response.data + assert b'href="/1/update"' in response.data + +A user must be logged in to access the ``create``, ``update``, and +``delete`` views. The logged in user must be the author of the post to +access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status +is returned. If a ``post`` with the given ``id`` doesn't exist, +``update`` and ``delete`` should return ``404 Not Found``. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + @pytest.mark.parametrize('path', ( + '/create', + '/1/update', + '/1/delete', + )) + def test_login_required(client, path): + response = client.post(path) + assert response.headers['Location'] == 'http://localhost/auth/login' + + + def test_author_required(app, client, auth): + # change the post author to another user + with app.app_context(): + db = get_db() + db.execute('UPDATE post SET author_id = 2 WHERE id = 1') + db.commit() + + auth.login() + # current user can't modify other user's post + assert client.post('/1/update').status_code == 403 + assert client.post('/1/delete').status_code == 403 + # current user doesn't see edit link + assert b'href="/1/update"' not in client.get('/').data + + + @pytest.mark.parametrize('path', ( + '/2/update', + '/2/delete', + )) + def test_exists_required(client, auth, path): + auth.login() + assert client.post(path).status_code == 404 + +The ``create`` and ``update`` views should render and return a +``200 OK`` status for a ``GET`` request. When valid data is sent in a +``POST`` request, ``create`` should insert the new post data into the +database, and ``update`` should modify the existing data. Both pages +should show an error message on invalid data. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + def test_create(client, auth, app): + auth.login() + assert client.get('/create').status_code == 200 + client.post('/create', data={'title': 'created', 'body': ''}) + + with app.app_context(): + db = get_db() + count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] + assert count == 2 + + + def test_update(client, auth, app): + auth.login() + assert client.get('/1/update').status_code == 200 + client.post('/1/update', data={'title': 'updated', 'body': ''}) + + with app.app_context(): + db = get_db() + post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() + assert post['title'] == 'updated' + + + @pytest.mark.parametrize('path', ( + '/create', + '/1/update', + )) + def test_create_update_validate(client, auth, path): + auth.login() + response = client.post(path, data={'title': '', 'body': ''}) + assert b'Title is required.' in response.data + +The ``delete`` view should redirect to the index URL and the post should +no longer exist in the database. + +.. code-block:: python + :caption: ``tests/test_blog.py`` + + def test_delete(client, auth, app): + auth.login() + response = client.post('/1/delete') + assert response.headers['Location'] == 'http://localhost/' + + with app.app_context(): + db = get_db() + post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() + assert post is None + + +Running the Tests +----------------- + +Some extra configuration, which is not required but makes running +tests with coverage less verbose, can be added to the project's +``setup.cfg`` file. + +.. code-block:: none + :caption: ``setup.cfg`` + + [tool:pytest] + testpaths = tests + + [coverage:run] + branch = True + source = + flaskr + +To run the tests, use the ``pytest`` command. It will find and run all +the test functions you've written. + +.. code-block:: none + + pytest + + ========================= test session starts ========================== + platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 + rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg + collected 23 items + + tests/test_auth.py ........ [ 34%] + tests/test_blog.py ............ [ 86%] + tests/test_db.py .. [ 95%] + tests/test_factory.py .. [100%] + + ====================== 24 passed in 0.64 seconds ======================= + +If any tests fail, pytest will show the error that was raised. You can +run ``pytest -v`` to get a list of each test function rather than dots. + +To measure the code coverage of your tests, use the ``coverage`` command +to run pytest instead of running it directly. + +.. code-block:: none + + coverage run -m pytest + +You can either view a simple coverage report in the terminal: + +.. code-block:: none + + coverage report + + Name Stmts Miss Branch BrPart Cover + ------------------------------------------------------ + flaskr/__init__.py 21 0 2 0 100% + flaskr/auth.py 54 0 22 0 100% + flaskr/blog.py 54 0 16 0 100% + flaskr/db.py 24 0 4 0 100% + ------------------------------------------------------ + TOTAL 153 0 44 0 100% + +An HTML report allows you to see which lines were covered in each file: + +.. code-block:: none + + coverage html + +This generates files in the ``htmlcov`` directory. Open +``htmlcov/index.html`` in your browser to see the report. + +Continue to :doc:`deploy`. diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 1b09fcb8bb..c9c6a7cacb 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -1,118 +1,301 @@ -.. _tutorial-views: - -Step 6: The View Functions -========================== - -Now that the database connections are working, you can start writing the -view functions. You will need four of them; Show Entries, Add New Entry, -Login and Logout. Add the following code snipets to :file:`flaskr.py`. - -Show Entries ------------- - -This view shows all the entries stored in the database. It listens on the -root of the application and will select title and text from the database. -The one with the highest id (the newest entry) will be on top. The rows -returned from the cursor look a bit like dictionaries because we are using -the :class:`sqlite3.Row` row factory. - -The view function will pass the entries to the :file:`show_entries.html` -template and return the rendered one:: - - @app.route('/') - def show_entries(): - db = get_db() - cur = db.execute('select title, text from entries order by id desc') - entries = cur.fetchall() - return render_template('show_entries.html', entries=entries) - -Add New Entry -------------- - -This view lets the user add new entries if they are logged in. This only -responds to ``POST`` requests; the actual form is shown on the -`show_entries` page. If everything worked out well, it will -:func:`~flask.flash` an information message to the next request and -redirect back to the `show_entries` page:: - - @app.route('/add', methods=['POST']) - def add_entry(): - if not session.get('logged_in'): - abort(401) - db = get_db() - db.execute('insert into entries (title, text) values (?, ?)', - [request.form['title'], request.form['text']]) - db.commit() - flash('New entry was successfully posted') - return redirect(url_for('show_entries')) - -Note that this view checks that the user is logged in (that is, if the -`logged_in` key is present in the session and ``True``). - -.. admonition:: Security Note - - Be sure to use question marks when building SQL statements, as done in the - example above. Otherwise, your app will be vulnerable to SQL injection when - you use string formatting to build SQL statements. - See :ref:`sqlite3` for more. - -Login and Logout ----------------- - -These functions are used to sign the user in and out. Login checks the -username and password against the ones from the configuration and sets the -`logged_in` key for the session. If the user logged in successfully, that -key is set to ``True``, and the user is redirected back to the `show_entries` -page. In addition, a message is flashed that informs the user that he or -she was logged in successfully. If an error occurred, the template is -notified about that, and the user is asked again:: - - @app.route('/login', methods=['GET', 'POST']) +.. currentmodule:: flask + +Blueprints and Views +==================== + +A view function is the code you write to respond to requests to your +application. Flask uses patterns to match the incoming request URL to +the view that should handle it. The view returns data that Flask turns +into an outgoing response. Flask can also go the other direction and +generate a URL to a view based on its name and arguments. + + +Create a Blueprint +------------------ + +A :class:`Blueprint` is a way to organize a group of related views and +other code. Rather than registering views and other code directly with +an application, they are registered with a blueprint. Then the blueprint +is registered with the application when it is available in the factory +function. + +Flaskr will have two blueprints, one for authentication functions and +one for the blog posts functions. The code for each blueprint will go +in a separate module. Since the blog needs to know about authentication, +you'll write the authentication one first. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + import functools + + from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for + ) + from werkzeug.security import check_password_hash, generate_password_hash + + from flaskr.db import get_db + + bp = Blueprint('auth', __name__, url_prefix='/auth') + +This creates a :class:`Blueprint` named ``'auth'``. Like the application +object, the blueprint needs to know where it's defined, so ``__name__`` +is passed as the second argument. The ``url_prefix`` will be prepended +to all the URLs associated with the blueprint. + +Import and register the blueprint from the factory using +:meth:`app.register_blueprint() `. Place the +new code at the end of the factory function before returning the app. + +.. code-block:: python + :caption: ``flaskr/__init__.py`` + + def create_app(): + app = ... + # existing code omitted + + from . import auth + app.register_blueprint(auth.bp) + + return app + +The authentication blueprint will have views to register new users and +to log in and log out. + + +The First View: Register +------------------------ + +When the user visits the ``/auth/register`` URL, the ``register`` view +will return `HTML`_ with a form for them to fill out. When they submit +the form, it will validate their input and either show the form again +with an error message or create the new user and go to the login page. + +.. _HTML: https://developer.mozilla.org/docs/Web/HTML + +For now you will just write the view code. On the next page, you'll +write templates to generate the HTML form. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.route('/register', methods=('GET', 'POST')) + def register(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + elif db.execute( + 'SELECT id FROM user WHERE username = ?', (username,) + ).fetchone() is not None: + error = 'User {} is already registered.'.format(username) + + if error is None: + db.execute( + 'INSERT INTO user (username, password) VALUES (?, ?)', + (username, generate_password_hash(password)) + ) + db.commit() + return redirect(url_for('auth.login')) + + flash(error) + + return render_template('auth/register.html') + +Here's what the ``register`` view function is doing: + +#. :meth:`@bp.route ` associates the URL ``/register`` + with the ``register`` view function. When Flask receives a request + to ``/auth/register``, it will call the ``register`` view and use + the return value as the response. + +#. If the user submitted the form, + :attr:`request.method ` will be ``'POST'``. In this + case, start validating the input. + +#. :attr:`request.form ` is a special type of + :class:`dict` mapping submitted form keys and values. The user will + input their ``username`` and ``password``. + +#. Validate that ``username`` and ``password`` are not empty. + +#. Validate that ``username`` is not already registered by querying the + database and checking if a result is returned. + :meth:`db.execute ` takes a SQL query + with ``?`` placeholders for any user input, and a tuple of values + to replace the placeholders with. The database library will take + care of escaping the values so you are not vulnerable to a + *SQL injection attack*. + + :meth:`~sqlite3.Cursor.fetchone` returns one row from the query. + If the query returned no results, it returns ``None``. Later, + :meth:`~sqlite3.Cursor.fetchall` is used, which returns a list of + all results. + +#. If validation succeeds, insert the new user data into the database. + For security, passwords should never be stored in the database + directly. Instead, + :func:`~werkzeug.security.generate_password_hash` is used to + securely hash the password, and that hash is stored. Since this + query modifies data, :meth:`db.commit() ` + needs to be called afterwards to save the changes. + +#. After storing the user, they are redirected to the login page. + :func:`url_for` generates the URL for the login view based on its + name. This is preferable to writing the URL directly as it allows + you to change the URL later without changing all code that links to + it. :func:`redirect` generates a redirect response to the generated + URL. + +#. If validation fails, the error is shown to the user. :func:`flash` + stores messages that can be retrieved when rendering the template. + +#. When the user initially navigates to ``auth/register``, or + there was an validation error, an HTML page with the registration + form should be shown. :func:`render_template` will render a template + containing the HTML, which you'll write in the next step of the + tutorial. + + +Login +----- + +This view follows the same pattern as the ``register`` view above. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.route('/login', methods=('GET', 'POST')) def login(): - error = None if request.method == 'POST': - if request.form['username'] != app.config['USERNAME']: - error = 'Invalid username' - elif request.form['password'] != app.config['PASSWORD']: - error = 'Invalid password' - else: - session['logged_in'] = True - flash('You were logged in') - return redirect(url_for('show_entries')) - return render_template('login.html', error=error) - -The `logout` function, on the other hand, removes that key from the session -again. There is a neat trick here: if you use the :meth:`~dict.pop` method -of the dict and pass a second parameter to it (the default), the method -will delete the key from the dictionary if present or do nothing when that -key is not in there. This is helpful because now it is not necessary to -check if the user was logged in. - -:: - - @app.route('/logout') + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html') + +There are a few differences from the ``register`` view: + +#. The user is queried first and stored in a variable for later use. + +#. :func:`~werkzeug.security.check_password_hash` hashes the submitted + password in the same way as the stored hash and securely compares + them. If they match, the password is valid. + +#. :data:`session` is a :class:`dict` that stores data across requests. + When validation succeeds, the user's ``id`` is stored in a new + session. The data is stored in a *cookie* that is sent to the + browser, and the browser then sends it back with subsequent requests. + Flask securely *signs* the data so that it can't be tampered with. + +Now that the user's ``id`` is stored in the :data:`session`, it will be +available on subsequent requests. At the beginning of each request, if +a user is logged in their information should be loaded and made +available to other views. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.before_app_request + def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + +:meth:`bp.before_app_request() ` registers +a function that runs before the view function, no matter what URL is +requested. ``load_logged_in_user`` checks if a user id is stored in the +:data:`session` and gets that user's data from the database, storing it +on :data:`g.user `, which lasts for the length of the request. If +there is no user id, or if the id doesn't exist, ``g.user`` will be +``None``. + + +Logout +------ + +To log out, you need to remove the user id from the :data:`session`. +Then ``load_logged_in_user`` won't load a user on subsequent requests. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + @bp.route('/logout') def logout(): - session.pop('logged_in', None) - flash('You were logged out') - return redirect(url_for('show_entries')) + session.clear() + return redirect(url_for('index')) + + +Require Authentication in Other Views +------------------------------------- + +Creating, editing, and deleting blog posts will require a user to be +logged in. A *decorator* can be used to check this for each view it's +applied to. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + def login_required(view): + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) -.. admonition:: Security Note + return wrapped_view - Passwords should never be stored in plain text in a production - system. This tutorial uses plain text passwords for simplicity. If you - plan to release a project based off this tutorial out into the world, - passwords should be both `hashed and salted`_ before being stored in a - database or file. +This decorator returns a new view function that wraps the original view +it's applied to. The new function checks if a user is loaded and +redirects to the login page otherwise. If a user is loaded the original +view is called and continues normally. You'll use this decorator when +writing the blog views. - Fortunately, there are Flask extensions for the purpose of - hashing passwords and verifying passwords against hashes, so adding - this functionality is fairly straight forward. There are also - many general python libraries that can be used for hashing. +Endpoints and URLs +------------------ - You can find a list of recommended Flask extensions - `here `_ +The :func:`url_for` function generates the URL to a view based on a name +and arguments. The name associated with a view is also called the +*endpoint*, and by default it's the same as the name of the view +function. +For example, the ``hello()`` view that was added to the app +factory earlier in the tutorial has the name ``'hello'`` and can be +linked to with ``url_for('hello')``. If it took an argument, which +you'll see later, it would be linked to using +``url_for('hello', who='World')``. -Continue with :ref:`tutorial-templates`. +When using a blueprint, the name of the blueprint is prepended to the +name of the function, so the endpoint for the ``login`` function you +wrote above is ``'auth.login'`` because you added it to the ``'auth'`` +blueprint. -.. _hashed and salted: https://blog.codinghorror.com/youre-probably-storing-passwords-incorrectly/ +Continue to :doc:`templates`. diff --git a/examples/blueprintexample/blueprintexample.py b/examples/blueprintexample/blueprintexample.py deleted file mode 100644 index 6ca0dd1357..0000000000 --- a/examples/blueprintexample/blueprintexample.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Blueprint Example -~~~~~~~~~~~~~~~~~ - -:copyright: © 2010 by the Pallets team. -:license: BSD, see LICENSE for more details. -""" - -from flask import Flask -from simple_page.simple_page import simple_page - -app = Flask(__name__) -app.register_blueprint(simple_page) -# Blueprint can be registered many times -app.register_blueprint(simple_page, url_prefix='/pages') - -if __name__=='__main__': - app.run() diff --git a/examples/blueprintexample/simple_page/__init__.py b/examples/blueprintexample/simple_page/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/blueprintexample/simple_page/simple_page.py b/examples/blueprintexample/simple_page/simple_page.py deleted file mode 100644 index cb82cc372c..0000000000 --- a/examples/blueprintexample/simple_page/simple_page.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Blueprint, render_template, abort -from jinja2 import TemplateNotFound - -simple_page = Blueprint('simple_page', __name__, - template_folder='templates') - -@simple_page.route('/', defaults={'page': 'index'}) -@simple_page.route('/') -def show(page): - try: - return render_template('pages/%s.html' % page) - except TemplateNotFound: - abort(404) diff --git a/examples/blueprintexample/simple_page/templates/pages/hello.html b/examples/blueprintexample/simple_page/templates/pages/hello.html deleted file mode 100644 index 7e4a624d79..0000000000 --- a/examples/blueprintexample/simple_page/templates/pages/hello.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "pages/layout.html" %} - -{% block body %} - Hello -{% endblock %} diff --git a/examples/blueprintexample/simple_page/templates/pages/index.html b/examples/blueprintexample/simple_page/templates/pages/index.html deleted file mode 100644 index b8d92da42d..0000000000 --- a/examples/blueprintexample/simple_page/templates/pages/index.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "pages/layout.html" %} - -{% block body %} - Blueprint example page -{% endblock %} diff --git a/examples/blueprintexample/simple_page/templates/pages/layout.html b/examples/blueprintexample/simple_page/templates/pages/layout.html deleted file mode 100644 index f312d44be8..0000000000 --- a/examples/blueprintexample/simple_page/templates/pages/layout.html +++ /dev/null @@ -1,20 +0,0 @@ - -Simple Page Blueprint -
-

This is blueprint example

-

- A simple page blueprint is registered under / and /pages - you can access it using this URLs: -

-

- Also you can register the same blueprint under another path -

- - {% block body %}{% endblock %} -
diff --git a/examples/blueprintexample/simple_page/templates/pages/world.html b/examples/blueprintexample/simple_page/templates/pages/world.html deleted file mode 100644 index 9fa2880a8c..0000000000 --- a/examples/blueprintexample/simple_page/templates/pages/world.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "pages/layout.html" %} -{% block body %} - World -{% endblock %} diff --git a/examples/blueprintexample/test_blueprintexample.py b/examples/blueprintexample/test_blueprintexample.py deleted file mode 100644 index 44df7762ad..0000000000 --- a/examples/blueprintexample/test_blueprintexample.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Blueprint Example Tests -~~~~~~~~~~~~~~~~~~~~~~~ - -:copyright: © 2010 by the Pallets team. -:license: BSD, see LICENSE for more details. -""" - -import pytest - -import blueprintexample - - -@pytest.fixture -def client(): - return blueprintexample.app.test_client() - - -def test_urls(client): - r = client.get('/') - assert r.status_code == 200 - - r = client.get('/hello') - assert r.status_code == 200 - - r = client.get('/world') - assert r.status_code == 200 - - # second blueprint instance - r = client.get('/pages/hello') - assert r.status_code == 200 - - r = client.get('/pages/world') - assert r.status_code == 200 diff --git a/examples/flaskr/.gitignore b/examples/flaskr/.gitignore deleted file mode 100644 index 8d567f84e1..0000000000 --- a/examples/flaskr/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -flaskr.db -.eggs/ diff --git a/examples/flaskr/README b/examples/flaskr/README deleted file mode 100644 index ab668d67f9..0000000000 --- a/examples/flaskr/README +++ /dev/null @@ -1,40 +0,0 @@ - / Flaskr / - - a minimal blog application - - - ~ What is Flaskr? - - A sqlite powered thumble blog application - - ~ How do I use it? - - 1. edit the configuration in the factory.py file or - export a FLASKR_SETTINGS environment variable - pointing to a configuration file or pass in a - dictionary with config values using the create_app - function. - - 2. install the app from the root of the project directory - - pip install --editable . - - 3. instruct flask to use the right application - - export FLASK_APP="flaskr.factory:create_app()" - - 4. initialize the database with this command: - - flask initdb - - 5. now you can run flaskr: - - flask run - - the application will greet you on - http://localhost:5000/ - - ~ Is it tested? - - You betcha. Run `python setup.py test` to see - the tests pass. diff --git a/examples/flaskr/flaskr/__init__.py b/examples/flaskr/flaskr/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/flaskr/flaskr/blueprints/__init__.py b/examples/flaskr/flaskr/blueprints/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/flaskr/flaskr/blueprints/flaskr.py b/examples/flaskr/flaskr/blueprints/flaskr.py deleted file mode 100644 index e42bee629d..0000000000 --- a/examples/flaskr/flaskr/blueprints/flaskr.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flaskr - ~~~~~~ - - A microblog example application written as Flask tutorial with - Flask and sqlite3. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -from sqlite3 import dbapi2 as sqlite3 -from flask import Blueprint, request, session, g, redirect, url_for, abort, \ - render_template, flash, current_app - - -# create our blueprint :) -bp = Blueprint('flaskr', __name__) - - -def connect_db(): - """Connects to the specific database.""" - rv = sqlite3.connect(current_app.config['DATABASE']) - rv.row_factory = sqlite3.Row - return rv - - -def init_db(): - """Initializes the database.""" - db = get_db() - with current_app.open_resource('schema.sql', mode='r') as f: - db.cursor().executescript(f.read()) - db.commit() - - -def get_db(): - """Opens a new database connection if there is none yet for the - current application context. - """ - if not hasattr(g, 'sqlite_db'): - g.sqlite_db = connect_db() - return g.sqlite_db - - -@bp.route('/') -def show_entries(): - db = get_db() - cur = db.execute('select title, text from entries order by id desc') - entries = cur.fetchall() - return render_template('show_entries.html', entries=entries) - - -@bp.route('/add', methods=['POST']) -def add_entry(): - if not session.get('logged_in'): - abort(401) - db = get_db() - db.execute('insert into entries (title, text) values (?, ?)', - [request.form['title'], request.form['text']]) - db.commit() - flash('New entry was successfully posted') - return redirect(url_for('flaskr.show_entries')) - - -@bp.route('/login', methods=['GET', 'POST']) -def login(): - error = None - if request.method == 'POST': - if request.form['username'] != current_app.config['USERNAME']: - error = 'Invalid username' - elif request.form['password'] != current_app.config['PASSWORD']: - error = 'Invalid password' - else: - session['logged_in'] = True - flash('You were logged in') - return redirect(url_for('flaskr.show_entries')) - return render_template('login.html', error=error) - - -@bp.route('/logout') -def logout(): - session.pop('logged_in', None) - flash('You were logged out') - return redirect(url_for('flaskr.show_entries')) diff --git a/examples/flaskr/flaskr/factory.py b/examples/flaskr/flaskr/factory.py deleted file mode 100644 index b504f64a99..0000000000 --- a/examples/flaskr/flaskr/factory.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flaskr - ~~~~~~ - - A microblog example application written as Flask tutorial with - Flask and sqlite3. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -import os -from flask import Flask, g -from werkzeug.utils import find_modules, import_string -from flaskr.blueprints.flaskr import init_db - - -def create_app(config=None): - app = Flask('flaskr') - - app.config.update(dict( - DATABASE=os.path.join(app.root_path, 'flaskr.db'), - DEBUG=True, - SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/', - USERNAME='admin', - PASSWORD='default' - )) - app.config.update(config or {}) - app.config.from_envvar('FLASKR_SETTINGS', silent=True) - - register_blueprints(app) - register_cli(app) - register_teardowns(app) - - return app - - -def register_blueprints(app): - """Register all blueprint modules - - Reference: Armin Ronacher, "Flask for Fun and for Profit" PyBay 2016. - """ - for name in find_modules('flaskr.blueprints'): - mod = import_string(name) - if hasattr(mod, 'bp'): - app.register_blueprint(mod.bp) - return None - - -def register_cli(app): - @app.cli.command('initdb') - def initdb_command(): - """Creates the database tables.""" - init_db() - print('Initialized the database.') - - -def register_teardowns(app): - @app.teardown_appcontext - def close_db(error): - """Closes the database again at the end of the request.""" - if hasattr(g, 'sqlite_db'): - g.sqlite_db.close() diff --git a/examples/flaskr/flaskr/schema.sql b/examples/flaskr/flaskr/schema.sql deleted file mode 100644 index 25b2cadd1d..0000000000 --- a/examples/flaskr/flaskr/schema.sql +++ /dev/null @@ -1,6 +0,0 @@ -drop table if exists entries; -create table entries ( - id integer primary key autoincrement, - title text not null, - 'text' text not null -); diff --git a/examples/flaskr/flaskr/static/style.css b/examples/flaskr/flaskr/static/style.css deleted file mode 100644 index 4f3b71d8ae..0000000000 --- a/examples/flaskr/flaskr/static/style.css +++ /dev/null @@ -1,18 +0,0 @@ -body { font-family: sans-serif; background: #eee; } -a, h1, h2 { color: #377BA8; } -h1, h2 { font-family: 'Georgia', serif; margin: 0; } -h1 { border-bottom: 2px solid #eee; } -h2 { font-size: 1.2em; } - -.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; - padding: 0.8em; background: white; } -.entries { list-style: none; margin: 0; padding: 0; } -.entries li { margin: 0.8em 1.2em; } -.entries li h2 { margin-left: -1em; } -.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } -.add-entry dl { font-weight: bold; } -.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; - margin-bottom: 1em; background: #fafafa; } -.flash { background: #CEE5F5; padding: 0.5em; - border: 1px solid #AACBE2; } -.error { background: #F0D6D6; padding: 0.5em; } diff --git a/examples/flaskr/flaskr/templates/layout.html b/examples/flaskr/flaskr/templates/layout.html deleted file mode 100644 index 862a9f4a21..0000000000 --- a/examples/flaskr/flaskr/templates/layout.html +++ /dev/null @@ -1,17 +0,0 @@ - -Flaskr - -
-

Flaskr

-
- {% if not session.logged_in %} - log in - {% else %} - log out - {% endif %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block body %}{% endblock %} -
diff --git a/examples/flaskr/flaskr/templates/login.html b/examples/flaskr/flaskr/templates/login.html deleted file mode 100644 index 505d2f66e1..0000000000 --- a/examples/flaskr/flaskr/templates/login.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -

Login

- {% if error %}

Error: {{ error }}{% endif %} -

-
-
Username: -
-
Password: -
-
-
-
-{% endblock %} diff --git a/examples/flaskr/flaskr/templates/show_entries.html b/examples/flaskr/flaskr/templates/show_entries.html deleted file mode 100644 index cf8fbb8619..0000000000 --- a/examples/flaskr/flaskr/templates/show_entries.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "layout.html" %} -{% block body %} - {% if session.logged_in %} -
-
-
Title: -
-
Text: -
-
-
-
- {% endif %} -
    - {% for entry in entries %} -
  • {{ entry.title }}

    {{ entry.text|safe }}
  • - {% else %} -
  • Unbelievable. No entries here so far
  • - {% endfor %} -
-{% endblock %} diff --git a/examples/flaskr/setup.cfg b/examples/flaskr/setup.cfg deleted file mode 100644 index 9af7e6f11b..0000000000 --- a/examples/flaskr/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[aliases] -test=pytest \ No newline at end of file diff --git a/examples/flaskr/setup.py b/examples/flaskr/setup.py deleted file mode 100644 index f8995a0741..0000000000 --- a/examples/flaskr/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flaskr Tests - ~~~~~~~~~~~~ - - Tests the Flaskr application. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -from setuptools import setup, find_packages - -setup( - name='flaskr', - packages=find_packages(), - include_package_data=True, - install_requires=[ - 'flask', - ], - setup_requires=[ - 'pytest-runner', - ], - tests_require=[ - 'pytest', - ], -) diff --git a/examples/flaskr/tests/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py deleted file mode 100644 index 6e7618d59b..0000000000 --- a/examples/flaskr/tests/test_flaskr.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flaskr Tests - ~~~~~~~~~~~~ - - Tests the Flaskr application. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -import os -import tempfile -import pytest -from flaskr.factory import create_app -from flaskr.blueprints.flaskr import init_db - - -@pytest.fixture -def app(): - db_fd, db_path = tempfile.mkstemp() - config = { - 'DATABASE': db_path, - 'TESTING': True, - } - app = create_app(config=config) - - with app.app_context(): - init_db() - yield app - - os.close(db_fd) - os.unlink(db_path) - - -@pytest.fixture -def client(app): - return app.test_client() - - -def login(client, username, password): - return client.post('/login', data=dict( - username=username, - password=password - ), follow_redirects=True) - - -def logout(client): - return client.get('/logout', follow_redirects=True) - - -def test_empty_db(client): - """Start with a blank database.""" - rv = client.get('/') - assert b'No entries here so far' in rv.data - - -def test_login_logout(client, app): - """Make sure login and logout works""" - rv = login(client, app.config['USERNAME'], - app.config['PASSWORD']) - assert b'You were logged in' in rv.data - rv = logout(client) - assert b'You were logged out' in rv.data - rv = login(client,app.config['USERNAME'] + 'x', - app.config['PASSWORD']) - assert b'Invalid username' in rv.data - rv = login(client, app.config['USERNAME'], - app.config['PASSWORD'] + 'x') - assert b'Invalid password' in rv.data - - -def test_messages(client, app): - """Test that messages work""" - login(client, app.config['USERNAME'], - app.config['PASSWORD']) - rv = client.post('/add', data=dict( - title='', - text='HTML allowed here' - ), follow_redirects=True) - assert b'No entries here so far' not in rv.data - assert b'<Hello>' in rv.data - assert b'HTML allowed here' in rv.data diff --git a/examples/jqueryexample/jqueryexample.py b/examples/jqueryexample/jqueryexample.py deleted file mode 100644 index 561e53757f..0000000000 --- a/examples/jqueryexample/jqueryexample.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jQuery Example - ~~~~~~~~~~~~~~ - - A simple application that shows how Flask and jQuery get along. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -from flask import Flask, jsonify, render_template, request -app = Flask(__name__) - - -@app.route('/_add_numbers') -def add_numbers(): - """Add two numbers server side, ridiculous but well...""" - 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') - -if __name__ == '__main__': - app.run() diff --git a/examples/jqueryexample/templates/index.html b/examples/jqueryexample/templates/index.html deleted file mode 100644 index b6118cf4d6..0000000000 --- a/examples/jqueryexample/templates/index.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "layout.html" %} -{% block body %} - -

jQuery Example

-

- + - = - ? -

calculate server side -{% endblock %} diff --git a/examples/jqueryexample/templates/layout.html b/examples/jqueryexample/templates/layout.html deleted file mode 100644 index 8be7606e16..0000000000 --- a/examples/jqueryexample/templates/layout.html +++ /dev/null @@ -1,8 +0,0 @@ - -jQuery Example - - -{% block body %}{% endblock %} diff --git a/examples/minitwit/.gitignore b/examples/minitwit/.gitignore deleted file mode 100644 index c3accd8238..0000000000 --- a/examples/minitwit/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -minitwit.db -.eggs/ diff --git a/examples/minitwit/MANIFEST.in b/examples/minitwit/MANIFEST.in deleted file mode 100644 index 973d65863a..0000000000 --- a/examples/minitwit/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -graft minitwit/templates -graft minitwit/static -include minitwit/schema.sql \ No newline at end of file diff --git a/examples/minitwit/README b/examples/minitwit/README deleted file mode 100644 index b9bc5ea2df..0000000000 --- a/examples/minitwit/README +++ /dev/null @@ -1,39 +0,0 @@ - - / MiniTwit / - - because writing todo lists is not fun - - - ~ What is MiniTwit? - - A SQLite and Flask powered twitter clone - - ~ How do I use it? - - 1. edit the configuration in the minitwit.py file or - export an MINITWIT_SETTINGS environment variable - pointing to a configuration file. - - 2. install the app from the root of the project directory - - pip install --editable . - - 3. tell flask about the right application: - - export FLASK_APP=minitwit - - 4. fire up a shell and run this: - - flask initdb - - 5. now you can run minitwit: - - flask run - - the application will greet you on - http://localhost:5000/ - - ~ Is it tested? - - You betcha. Run the `python setup.py test` file to - see the tests pass. diff --git a/examples/minitwit/minitwit/__init__.py b/examples/minitwit/minitwit/__init__.py deleted file mode 100644 index 96c81aec84..0000000000 --- a/examples/minitwit/minitwit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .minitwit import app diff --git a/examples/minitwit/minitwit/minitwit.py b/examples/minitwit/minitwit/minitwit.py deleted file mode 100644 index 2fe002e29e..0000000000 --- a/examples/minitwit/minitwit/minitwit.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*- coding: utf-8 -*- -""" - MiniTwit - ~~~~~~~~ - - A microblogging application written with Flask and sqlite3. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -import time -from sqlite3 import dbapi2 as sqlite3 -from hashlib import md5 -from datetime import datetime -from flask import Flask, request, session, url_for, redirect, \ - render_template, abort, g, flash, _app_ctx_stack -from werkzeug import check_password_hash, generate_password_hash - - -# configuration -DATABASE = '/tmp/minitwit.db' -PER_PAGE = 30 -DEBUG = True -SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' - -# create our little application :) -app = Flask('minitwit') -app.config.from_object(__name__) -app.config.from_envvar('MINITWIT_SETTINGS', silent=True) - - -def get_db(): - """Opens a new database connection if there is none yet for the - current application context. - """ - top = _app_ctx_stack.top - if not hasattr(top, 'sqlite_db'): - top.sqlite_db = sqlite3.connect(app.config['DATABASE']) - top.sqlite_db.row_factory = sqlite3.Row - return top.sqlite_db - - -@app.teardown_appcontext -def close_database(exception): - """Closes the database again at the end of the request.""" - top = _app_ctx_stack.top - if hasattr(top, 'sqlite_db'): - top.sqlite_db.close() - - -def init_db(): - """Initializes the database.""" - db = get_db() - with app.open_resource('schema.sql', mode='r') as f: - db.cursor().executescript(f.read()) - db.commit() - - -@app.cli.command('initdb') -def initdb_command(): - """Creates the database tables.""" - init_db() - print('Initialized the database.') - - -def query_db(query, args=(), one=False): - """Queries the database and returns a list of dictionaries.""" - cur = get_db().execute(query, args) - rv = cur.fetchall() - return (rv[0] if rv else None) if one else rv - - -def get_user_id(username): - """Convenience method to look up the id for a username.""" - rv = query_db('select user_id from user where username = ?', - [username], one=True) - return rv[0] if rv else None - - -def format_datetime(timestamp): - """Format a timestamp for display.""" - return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') - - -def gravatar_url(email, size=80): - """Return the gravatar image for the given email address.""" - return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ - (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) - - -@app.before_request -def before_request(): - g.user = None - if 'user_id' in session: - g.user = query_db('select * from user where user_id = ?', - [session['user_id']], one=True) - - -@app.route('/') -def timeline(): - """Shows a users timeline or if no user is logged in it will - redirect to the public timeline. This timeline shows the user's - messages as well as all the messages of followed users. - """ - if not g.user: - return redirect(url_for('public_timeline')) - return render_template('timeline.html', messages=query_db(''' - select message.*, user.* from message, user - where message.author_id = user.user_id and ( - user.user_id = ? or - user.user_id in (select whom_id from follower - where who_id = ?)) - order by message.pub_date desc limit ?''', - [session['user_id'], session['user_id'], PER_PAGE])) - - -@app.route('/public') -def public_timeline(): - """Displays the latest messages of all users.""" - return render_template('timeline.html', messages=query_db(''' - select message.*, user.* from message, user - where message.author_id = user.user_id - order by message.pub_date desc limit ?''', [PER_PAGE])) - - -@app.route('/') -def user_timeline(username): - """Display's a users tweets.""" - profile_user = query_db('select * from user where username = ?', - [username], one=True) - if profile_user is None: - abort(404) - followed = False - if g.user: - followed = query_db('''select 1 from follower where - follower.who_id = ? and follower.whom_id = ?''', - [session['user_id'], profile_user['user_id']], - one=True) is not None - return render_template('timeline.html', messages=query_db(''' - select message.*, user.* from message, user where - user.user_id = message.author_id and user.user_id = ? - order by message.pub_date desc limit ?''', - [profile_user['user_id'], PER_PAGE]), followed=followed, - profile_user=profile_user) - - -@app.route('//follow') -def follow_user(username): - """Adds the current user as follower of the given user.""" - if not g.user: - abort(401) - whom_id = get_user_id(username) - if whom_id is None: - abort(404) - db = get_db() - db.execute('insert into follower (who_id, whom_id) values (?, ?)', - [session['user_id'], whom_id]) - db.commit() - flash('You are now following "%s"' % username) - return redirect(url_for('user_timeline', username=username)) - - -@app.route('//unfollow') -def unfollow_user(username): - """Removes the current user as follower of the given user.""" - if not g.user: - abort(401) - whom_id = get_user_id(username) - if whom_id is None: - abort(404) - db = get_db() - db.execute('delete from follower where who_id=? and whom_id=?', - [session['user_id'], whom_id]) - db.commit() - flash('You are no longer following "%s"' % username) - return redirect(url_for('user_timeline', username=username)) - - -@app.route('/add_message', methods=['POST']) -def add_message(): - """Registers a new message for the user.""" - if 'user_id' not in session: - abort(401) - if request.form['text']: - db = get_db() - db.execute('''insert into message (author_id, text, pub_date) - values (?, ?, ?)''', (session['user_id'], request.form['text'], - int(time.time()))) - db.commit() - flash('Your message was recorded') - return redirect(url_for('timeline')) - - -@app.route('/login', methods=['GET', 'POST']) -def login(): - """Logs the user in.""" - if g.user: - return redirect(url_for('timeline')) - error = None - if request.method == 'POST': - user = query_db('''select * from user where - username = ?''', [request.form['username']], one=True) - if user is None: - error = 'Invalid username' - elif not check_password_hash(user['pw_hash'], - request.form['password']): - error = 'Invalid password' - else: - flash('You were logged in') - session['user_id'] = user['user_id'] - return redirect(url_for('timeline')) - return render_template('login.html', error=error) - - -@app.route('/register', methods=['GET', 'POST']) -def register(): - """Registers the user.""" - if g.user: - return redirect(url_for('timeline')) - error = None - if request.method == 'POST': - if not request.form['username']: - error = 'You have to enter a username' - elif not request.form['email'] or \ - '@' not in request.form['email']: - error = 'You have to enter a valid email address' - elif not request.form['password']: - error = 'You have to enter a password' - elif request.form['password'] != request.form['password2']: - error = 'The two passwords do not match' - elif get_user_id(request.form['username']) is not None: - error = 'The username is already taken' - else: - db = get_db() - db.execute('''insert into user ( - username, email, pw_hash) values (?, ?, ?)''', - [request.form['username'], request.form['email'], - generate_password_hash(request.form['password'])]) - db.commit() - flash('You were successfully registered and can login now') - return redirect(url_for('login')) - return render_template('register.html', error=error) - - -@app.route('/logout') -def logout(): - """Logs the user out.""" - flash('You were logged out') - session.pop('user_id', None) - return redirect(url_for('public_timeline')) - - -# add some filters to jinja -app.jinja_env.filters['datetimeformat'] = format_datetime -app.jinja_env.filters['gravatar'] = gravatar_url diff --git a/examples/minitwit/minitwit/schema.sql b/examples/minitwit/minitwit/schema.sql deleted file mode 100644 index b272adc894..0000000000 --- a/examples/minitwit/minitwit/schema.sql +++ /dev/null @@ -1,21 +0,0 @@ -drop table if exists user; -create table user ( - user_id integer primary key autoincrement, - username text not null, - email text not null, - pw_hash text not null -); - -drop table if exists follower; -create table follower ( - who_id integer, - whom_id integer -); - -drop table if exists message; -create table message ( - message_id integer primary key autoincrement, - author_id integer not null, - text text not null, - pub_date integer -); diff --git a/examples/minitwit/minitwit/static/style.css b/examples/minitwit/minitwit/static/style.css deleted file mode 100644 index ebbed8c9c9..0000000000 --- a/examples/minitwit/minitwit/static/style.css +++ /dev/null @@ -1,178 +0,0 @@ -body { - background: #CAECE9; - font-family: 'Trebuchet MS', sans-serif; - font-size: 14px; -} - -a { - color: #26776F; -} - -a:hover { - color: #333; -} - -input[type="text"], -input[type="password"] { - background: white; - border: 1px solid #BFE6E2; - padding: 2px; - font-family: 'Trebuchet MS', sans-serif; - font-size: 14px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - color: #105751; -} - -input[type="submit"] { - background: #105751; - border: 1px solid #073B36; - padding: 1px 3px; - font-family: 'Trebuchet MS', sans-serif; - font-size: 14px; - font-weight: bold; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - color: white; -} - -div.page { - background: white; - border: 1px solid #6ECCC4; - width: 700px; - margin: 30px auto; -} - -div.page h1 { - background: #6ECCC4; - margin: 0; - padding: 10px 14px; - color: white; - letter-spacing: 1px; - text-shadow: 0 0 3px #24776F; - font-weight: normal; -} - -div.page div.navigation { - background: #DEE9E8; - padding: 4px 10px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #eee; - color: #888; - font-size: 12px; - letter-spacing: 0.5px; -} - -div.page div.navigation a { - color: #444; - font-weight: bold; -} - -div.page h2 { - margin: 0 0 15px 0; - color: #105751; - text-shadow: 0 1px 2px #ccc; -} - -div.page div.body { - padding: 10px; -} - -div.page div.footer { - background: #eee; - color: #888; - padding: 5px 10px; - font-size: 12px; -} - -div.page div.followstatus { - border: 1px solid #ccc; - background: #E3EBEA; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - padding: 3px; - font-size: 13px; -} - -div.page ul.messages { - list-style: none; - margin: 0; - padding: 0; -} - -div.page ul.messages li { - margin: 10px 0; - padding: 5px; - background: #F0FAF9; - border: 1px solid #DBF3F1; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - min-height: 48px; -} - -div.page ul.messages p { - margin: 0; -} - -div.page ul.messages li img { - float: left; - padding: 0 10px 0 0; -} - -div.page ul.messages li small { - font-size: 0.9em; - color: #888; -} - -div.page div.twitbox { - margin: 10px 0; - padding: 5px; - background: #F0FAF9; - border: 1px solid #94E2DA; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; -} - -div.page div.twitbox h3 { - margin: 0; - font-size: 1em; - color: #2C7E76; -} - -div.page div.twitbox p { - margin: 0; -} - -div.page div.twitbox input[type="text"] { - width: 585px; -} - -div.page div.twitbox input[type="submit"] { - width: 70px; - margin-left: 5px; -} - -ul.flashes { - list-style: none; - margin: 10px 10px 0 10px; - padding: 0; -} - -ul.flashes li { - background: #B9F3ED; - border: 1px solid #81CEC6; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - padding: 4px; - font-size: 13px; -} - -div.error { - margin: 10px 0; - background: #FAE4E4; - border: 1px solid #DD6F6F; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - padding: 4px; - font-size: 13px; -} diff --git a/examples/minitwit/minitwit/templates/layout.html b/examples/minitwit/minitwit/templates/layout.html deleted file mode 100644 index 5a43df61dc..0000000000 --- a/examples/minitwit/minitwit/templates/layout.html +++ /dev/null @@ -1,32 +0,0 @@ - -{% block title %}Welcome{% endblock %} | MiniTwit - -

-

MiniTwit

- - {% with flashes = get_flashed_messages() %} - {% if flashes %} -
    - {% for message in flashes %} -
  • {{ message }} - {% endfor %} -
- {% endif %} - {% endwith %} -
- {% block body %}{% endblock %} -
- -
diff --git a/examples/minitwit/minitwit/templates/login.html b/examples/minitwit/minitwit/templates/login.html deleted file mode 100644 index f15bf1098d..0000000000 --- a/examples/minitwit/minitwit/templates/login.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Sign In{% endblock %} -{% block body %} -

Sign In

- {% if error %}
Error: {{ error }}
{% endif %} -
-
-
Username: -
-
Password: -
-
-
-
-{% endblock %} - diff --git a/examples/minitwit/minitwit/templates/register.html b/examples/minitwit/minitwit/templates/register.html deleted file mode 100644 index f28cd9f0c8..0000000000 --- a/examples/minitwit/minitwit/templates/register.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Sign Up{% endblock %} -{% block body %} -

Sign Up

- {% if error %}
Error: {{ error }}
{% endif %} -
-
-
Username: -
-
E-Mail: -
-
Password: -
-
Password (repeat): -
-
-
-
-{% endblock %} diff --git a/examples/minitwit/minitwit/templates/timeline.html b/examples/minitwit/minitwit/templates/timeline.html deleted file mode 100644 index bf655634e5..0000000000 --- a/examples/minitwit/minitwit/templates/timeline.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "layout.html" %} -{% block title %} - {% if request.endpoint == 'public_timeline' %} - Public Timeline - {% elif request.endpoint == 'user_timeline' %} - {{ profile_user.username }}'s Timeline - {% else %} - My Timeline - {% endif %} -{% endblock %} -{% block body %} -

{{ self.title() }}

- {% if g.user %} - {% if request.endpoint == 'user_timeline' %} -
- {% if g.user.user_id == profile_user.user_id %} - This is you! - {% elif followed %} - You are currently following this user. - Unfollow user. - {% else %} - You are not yet following this user. - . - {% endif %} -
- {% elif request.endpoint == 'timeline' %} -
-

What's on your mind {{ g.user.username }}?

-
-

-

-
- {% endif %} - {% endif %} -
    - {% for message in messages %} -
  • - {{ message.username }} - {{ message.text }} - — {{ message.pub_date|datetimeformat }} - {% else %} -

  • There's no message so far. - {% endfor %} -
-{% endblock %} diff --git a/examples/minitwit/setup.cfg b/examples/minitwit/setup.cfg deleted file mode 100644 index b7e478982c..0000000000 --- a/examples/minitwit/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[aliases] -test=pytest diff --git a/examples/minitwit/setup.py b/examples/minitwit/setup.py deleted file mode 100644 index 1e5802167f..0000000000 --- a/examples/minitwit/setup.py +++ /dev/null @@ -1,16 +0,0 @@ -from setuptools import setup - -setup( - name='minitwit', - packages=['minitwit'], - include_package_data=True, - install_requires=[ - 'flask', - ], - setup_requires=[ - 'pytest-runner', - ], - tests_require=[ - 'pytest', - ], -) \ No newline at end of file diff --git a/examples/minitwit/tests/test_minitwit.py b/examples/minitwit/tests/test_minitwit.py deleted file mode 100644 index 3decc6dac4..0000000000 --- a/examples/minitwit/tests/test_minitwit.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -""" - MiniTwit Tests - ~~~~~~~~~~~~~~ - - Tests the MiniTwit application. - - :copyright: © 2010 by the Pallets team. - :license: BSD, see LICENSE for more details. -""" - -import os -import tempfile -import pytest -from minitwit import minitwit - - -@pytest.fixture -def client(): - db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp() - client = minitwit.app.test_client() - with minitwit.app.app_context(): - minitwit.init_db() - - yield client - - os.close(db_fd) - os.unlink(minitwit.app.config['DATABASE']) - - -def register(client, username, password, password2=None, email=None): - """Helper function to register a user""" - if password2 is None: - password2 = password - if email is None: - email = username + '@example.com' - return client.post('/register', data={ - 'username': username, - 'password': password, - 'password2': password2, - 'email': email, - }, follow_redirects=True) - - -def login(client, username, password): - """Helper function to login""" - return client.post('/login', data={ - 'username': username, - 'password': password - }, follow_redirects=True) - - -def register_and_login(client, username, password): - """Registers and logs in in one go""" - register(client, username, password) - return login(client, username, password) - - -def logout(client): - """Helper function to logout""" - return client.get('/logout', follow_redirects=True) - - -def add_message(client, text): - """Records a message""" - rv = client.post('/add_message', data={'text': text}, - follow_redirects=True) - if text: - assert b'Your message was recorded' in rv.data - return rv - - -def test_register(client): - """Make sure registering works""" - rv = register(client, 'user1', 'default') - assert b'You were successfully registered ' \ - b'and can login now' in rv.data - rv = register(client, 'user1', 'default') - assert b'The username is already taken' in rv.data - rv = register(client, '', 'default') - assert b'You have to enter a username' in rv.data - rv = register(client, 'meh', '') - assert b'You have to enter a password' in rv.data - rv = register(client, 'meh', 'x', 'y') - assert b'The two passwords do not match' in rv.data - rv = register(client, 'meh', 'foo', email='broken') - assert b'You have to enter a valid email address' in rv.data - - -def test_login_logout(client): - """Make sure logging in and logging out works""" - rv = register_and_login(client, 'user1', 'default') - assert b'You were logged in' in rv.data - rv = logout(client) - assert b'You were logged out' in rv.data - rv = login(client, 'user1', 'wrongpassword') - assert b'Invalid password' in rv.data - rv = login(client, 'user2', 'wrongpassword') - assert b'Invalid username' in rv.data - - -def test_message_recording(client): - """Check if adding messages works""" - register_and_login(client, 'foo', 'default') - add_message(client, 'test message 1') - add_message(client, '') - rv = client.get('/') - assert b'test message 1' in rv.data - assert b'<test message 2>' in rv.data - - -def test_timelines(client): - """Make sure that timelines work""" - register_and_login(client, 'foo', 'default') - add_message(client, 'the message by foo') - logout(client) - register_and_login(client, 'bar', 'default') - add_message(client, 'the message by bar') - rv = client.get('/public') - assert b'the message by foo' in rv.data - assert b'the message by bar' in rv.data - - # bar's timeline should just show bar's message - rv = client.get('/') - assert b'the message by foo' not in rv.data - assert b'the message by bar' in rv.data - - # now let's follow foo - rv = client.get('/foo/follow', follow_redirects=True) - assert b'You are now following "foo"' in rv.data - - # we should now see foo's message - rv = client.get('/') - assert b'the message by foo' in rv.data - assert b'the message by bar' in rv.data - - # but on the user's page we only want the user's message - rv = client.get('/bar') - assert b'the message by foo' not in rv.data - assert b'the message by bar' in rv.data - rv = client.get('/foo') - assert b'the message by foo' in rv.data - assert b'the message by bar' not in rv.data - - # now unfollow and check if that worked - rv = client.get('/foo/unfollow', follow_redirects=True) - assert b'You are no longer following "foo"' in rv.data - rv = client.get('/') - assert b'the message by foo' not in rv.data - assert b'the message by bar' in rv.data diff --git a/examples/patterns/largerapp/setup.py b/examples/patterns/largerapp/setup.py deleted file mode 100644 index eaf00f0718..0000000000 --- a/examples/patterns/largerapp/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from setuptools import setup - -setup( - name='yourapplication', - packages=['yourapplication'], - include_package_data=True, - install_requires=[ - 'flask', - ], -) diff --git a/examples/patterns/largerapp/tests/test_largerapp.py b/examples/patterns/largerapp/tests/test_largerapp.py deleted file mode 100644 index 32553d7c9e..0000000000 --- a/examples/patterns/largerapp/tests/test_largerapp.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Larger App Tests -~~~~~~~~~~~~~~~~ - -:copyright: © 2010 by the Pallets team. -:license: BSD, see LICENSE for more details. -""" - -from yourapplication import app -import pytest - -@pytest.fixture -def client(): - app.config['TESTING'] = True - client = app.test_client() - return client - -def test_index(client): - rv = client.get('/') - assert b"Hello World!" in rv.data diff --git a/examples/patterns/largerapp/yourapplication/__init__.py b/examples/patterns/largerapp/yourapplication/__init__.py deleted file mode 100644 index c2e05ddad4..0000000000 --- a/examples/patterns/largerapp/yourapplication/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -""" -yourapplication -~~~~~~~~~~~~~~~ - -:copyright: © 2010 by the Pallets team. -:license: BSD, see LICENSE for more details. -""" - -from flask import Flask -app = Flask('yourapplication') - -import yourapplication.views diff --git a/examples/patterns/largerapp/yourapplication/static/style.css b/examples/patterns/largerapp/yourapplication/static/style.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/patterns/largerapp/yourapplication/templates/index.html b/examples/patterns/largerapp/yourapplication/templates/index.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/patterns/largerapp/yourapplication/templates/layout.html b/examples/patterns/largerapp/yourapplication/templates/layout.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/patterns/largerapp/yourapplication/templates/login.html b/examples/patterns/largerapp/yourapplication/templates/login.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/patterns/largerapp/yourapplication/views.py b/examples/patterns/largerapp/yourapplication/views.py deleted file mode 100644 index 5337eab78a..0000000000 --- a/examples/patterns/largerapp/yourapplication/views.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -""" -yourapplication.views -~~~~~~~~~~~~~~~~~~~~~ - -:copyright: © 2010 by the Pallets team. -:license: BSD, see LICENSE for more details. -""" - -from yourapplication import app - -@app.route('/') -def index(): - return 'Hello World!' diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore new file mode 100644 index 0000000000..85a35845ad --- /dev/null +++ b/examples/tutorial/.gitignore @@ -0,0 +1,14 @@ +venv/ +*.pyc +__pycache__/ +instance/ +.cache/ +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ +.idea/ +*.swp +*~ diff --git a/examples/tutorial/LICENSE b/examples/tutorial/LICENSE new file mode 100644 index 0000000000..8f9252f452 --- /dev/null +++ b/examples/tutorial/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2010 by the Pallets team. + +Some rights reserved. + +Redistribution and use in source and binary forms of the software as +well as documentation, with or without modification, are permitted +provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/examples/flaskr/MANIFEST.in b/examples/tutorial/MANIFEST.in similarity index 58% rename from examples/flaskr/MANIFEST.in rename to examples/tutorial/MANIFEST.in index efbd93df6a..a73511ed2a 100644 --- a/examples/flaskr/MANIFEST.in +++ b/examples/tutorial/MANIFEST.in @@ -1,3 +1,6 @@ -graft flaskr/templates -graft flaskr/static +include LICENSE include flaskr/schema.sql +graft flaskr/static +graft flaskr/templates +graft tests +global-exclude *.pyc diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst new file mode 100644 index 0000000000..e0c44b61fd --- /dev/null +++ b/examples/tutorial/README.rst @@ -0,0 +1,76 @@ +Flaskr +====== + +The basic blog app built in the Flask `tutorial`_. + +.. _tutorial: http://flask.pocoo.org/docs/tutorial/ + + +Install +------- + +**Be sure to use the same version of the code as the version of the docs +you're reading.** You probably want the latest tagged version, but the +default Git version is the master branch. :: + + # clone the repository + git clone https://github.com/pallets/flask + cd flask + # checkout the correct version + git tag # shows the tagged versions + git checkout latest-tag-found-above + cd examples/tutorial + +Create a virtualenv and activate it:: + + python3 -m venv venv + . venv/bin/activate + +Or on Windows cmd:: + + py -3 -m venv venv + venv\Scripts\activate.bat + +Install Flaskr:: + + pip install -e . + +Or if you are using the master branch, install Flask from source before +installing Flaskr:: + + pip install -e ../.. + pip install -e . + + +Run +--- + +:: + + export FLASK_APP=flaskr + export FLASK_ENV=development + flask run + +Or on Windows cmd:: + + set FLASK_APP=flaskr + set FLASK_ENV=development + flask run + +Open http://127.0.0.1:5000 in a browser. + + +Test +---- + +:: + + pip install pytest + pytest + +Run with coverage report:: + + pip install pytest coverage + coverage run -m pytest + coverage report + coverage html # open htmlcov/index.html in a browser diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py new file mode 100644 index 0000000000..05316607aa --- /dev/null +++ b/examples/tutorial/flaskr/__init__.py @@ -0,0 +1,48 @@ +import os + +from flask import Flask + + +def create_app(test_config=None): + """Create and configure an instance of the Flask application.""" + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + # a default secret that should be overridden by instance config + SECRET_KEY='dev', + # store the database in the instance folder + DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), + ) + + if test_config is None: + # load the instance config, if it exists, when not testing + app.config.from_pyfile('config.py', silent=True) + else: + # load the test config if passed in + app.config.update(test_config) + + # ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + @app.route('/hello') + def hello(): + return 'Hello, World!' + + # register the database commands + from flaskr import db + db.init_app(app) + + # apply the blueprints to the app + from flaskr import auth, blog + app.register_blueprint(auth.bp) + app.register_blueprint(blog.bp) + + # make url_for('index') == url_for('blog.index') + # in another app, you might define a separate main index here with + # app.route, while giving the blog blueprint a url_prefix, but for + # the tutorial the blog will be the main index + app.add_url_rule('/', endpoint='index') + + return app diff --git a/examples/tutorial/flaskr/auth.py b/examples/tutorial/flaskr/auth.py new file mode 100644 index 0000000000..d86095bf81 --- /dev/null +++ b/examples/tutorial/flaskr/auth.py @@ -0,0 +1,108 @@ +import functools + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for +) +from werkzeug.security import check_password_hash, generate_password_hash + +from flaskr.db import get_db + +bp = Blueprint('auth', __name__, url_prefix='/auth') + + +def login_required(view): + """View decorator that redirects anonymous users to the login page.""" + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) + + return wrapped_view + + +@bp.before_app_request +def load_logged_in_user(): + """If a user id is stored in the session, load the user object from + the database into ``g.user``.""" + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + + +@bp.route('/register', methods=('GET', 'POST')) +def register(): + """Register a new user. + + Validates that the username is not already taken. Hashes the + password for security. + """ + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + elif db.execute( + 'SELECT id FROM user WHERE username = ?', (username,) + ).fetchone() is not None: + error = 'User {} is already registered.'.format(username) + + if error is None: + # the name is available, store it in the database and go to + # the login page + db.execute( + 'INSERT INTO user (username, password) VALUES (?, ?)', + (username, generate_password_hash(password)) + ) + db.commit() + return redirect(url_for('auth.login')) + + flash(error) + + return render_template('auth/register.html') + + +@bp.route('/login', methods=('GET', 'POST')) +def login(): + """Log in a registered user by adding the user id to the session.""" + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + # store the user id in a new session and return to the index + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html') + + +@bp.route('/logout') +def logout(): + """Clear the current session, including the stored user id.""" + session.clear() + return redirect(url_for('index')) diff --git a/examples/tutorial/flaskr/blog.py b/examples/tutorial/flaskr/blog.py new file mode 100644 index 0000000000..784b1d8c56 --- /dev/null +++ b/examples/tutorial/flaskr/blog.py @@ -0,0 +1,119 @@ +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for +) +from werkzeug.exceptions import abort + +from flaskr.auth import login_required +from flaskr.db import get_db + +bp = Blueprint('blog', __name__) + + +@bp.route('/') +def index(): + """Show all the posts, most recent first.""" + db = get_db() + posts = db.execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' ORDER BY created DESC' + ).fetchall() + return render_template('blog/index.html', posts=posts) + + +def get_post(id, check_author=True): + """Get a post and its author by id. + + Checks that the id exists and optionally that the current user is + the author. + + :param id: id of post to get + :param check_author: require the current user to be the author + :return: the post with author information + :raise 404: if a post with the given id doesn't exist + :raise 403: if the current user isn't the author + """ + post = get_db().execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' WHERE p.id = ?', + (id,) + ).fetchone() + + if post is None: + abort(404, "Post id {0} doesn't exist.".format(id)) + + if check_author and post['author_id'] != g.user['id']: + abort(403) + + return post + + +@bp.route('/create', methods=('GET', 'POST')) +@login_required +def create(): + """Create a new post for the current user.""" + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'INSERT INTO post (title, body, author_id)' + ' VALUES (?, ?, ?)', + (title, body, g.user['id']) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/create.html') + + +@bp.route('//update', methods=('GET', 'POST')) +@login_required +def update(id): + """Update a post if the current user is the author.""" + post = get_post(id) + + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'UPDATE post SET title = ?, body = ? WHERE id = ?', + (title, body, id) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/update.html', post=post) + + +@bp.route('//delete', methods=('POST',)) +@login_required +def delete(id): + """Delete a post. + + Ensures that the post exists and that the logged in user is the + author of the post. + """ + get_post(id) + db = get_db() + db.execute('DELETE FROM post WHERE id = ?', (id,)) + db.commit() + return redirect(url_for('blog.index')) diff --git a/examples/tutorial/flaskr/db.py b/examples/tutorial/flaskr/db.py new file mode 100644 index 0000000000..03bd3b3c98 --- /dev/null +++ b/examples/tutorial/flaskr/db.py @@ -0,0 +1,54 @@ +import sqlite3 + +import click +from flask import current_app, g +from flask.cli import with_appcontext + + +def get_db(): + """Connect to the application's configured database. The connection + is unique for each request and will be reused if this is called + again. + """ + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + +def close_db(e=None): + """If this request connected to the database, close the + connection. + """ + db = g.pop('db', None) + + if db is not None: + db.close() + + +def init_db(): + """Clear existing data and create new tables.""" + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + + +@click.command('init-db') +@with_appcontext +def init_db_command(): + """Clear existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + + +def init_app(app): + """Register database functions with the Flask app. This is called by + the application factory. + """ + app.teardown_appcontext(close_db) + app.cli.add_command(init_db_command) diff --git a/examples/tutorial/flaskr/schema.sql b/examples/tutorial/flaskr/schema.sql new file mode 100644 index 0000000000..dd4c86600a --- /dev/null +++ b/examples/tutorial/flaskr/schema.sql @@ -0,0 +1,20 @@ +-- Initialize the database. +-- Drop any existing data and create empty tables. + +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS post; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE post ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title TEXT NOT NULL, + body TEXT NOT NULL, + FOREIGN KEY (author_id) REFERENCES user (id) +); diff --git a/examples/tutorial/flaskr/static/style.css b/examples/tutorial/flaskr/static/style.css new file mode 100644 index 0000000000..2f1f4d0c3c --- /dev/null +++ b/examples/tutorial/flaskr/static/style.css @@ -0,0 +1,134 @@ +html { + font-family: sans-serif; + background: #eee; + padding: 1rem; +} + +body { + max-width: 960px; + margin: 0 auto; + background: white; +} + +h1, h2, h3, h4, h5, h6 { + font-family: serif; + color: #377ba8; + margin: 1rem 0; +} + +a { + color: #377ba8; +} + +hr { + border: none; + border-top: 1px solid lightgray; +} + +nav { + background: lightgray; + display: flex; + align-items: center; + padding: 0 0.5rem; +} + +nav h1 { + flex: auto; + margin: 0; +} + +nav h1 a { + text-decoration: none; + padding: 0.25rem 0.5rem; +} + +nav ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; +} + +nav ul li a, nav ul li span, header .action { + display: block; + padding: 0.5rem; +} + +.content { + padding: 0 1rem 1rem; +} + +.content > header { + border-bottom: 1px solid lightgray; + display: flex; + align-items: flex-end; +} + +.content > header h1 { + flex: auto; + margin: 1rem 0 0.25rem 0; +} + +.flash { + margin: 1em 0; + padding: 1em; + background: #cae6f6; + border: 1px solid #377ba8; +} + +.post > header { + display: flex; + align-items: flex-end; + font-size: 0.85em; +} + +.post > header > div:first-of-type { + flex: auto; +} + +.post > header h1 { + font-size: 1.5em; + margin-bottom: 0; +} + +.post .about { + color: slategray; + font-style: italic; +} + +.post .body { + white-space: pre-line; +} + +.content:last-child { + margin-bottom: 0; +} + +.content form { + margin: 1em 0; + display: flex; + flex-direction: column; +} + +.content label { + font-weight: bold; + margin-bottom: 0.5em; +} + +.content input, .content textarea { + margin-bottom: 1em; +} + +.content textarea { + min-height: 12em; + resize: vertical; +} + +input.danger { + color: #cc2f2e; +} + +input[type=submit] { + align-self: start; + min-width: 10em; +} diff --git a/examples/tutorial/flaskr/templates/auth/login.html b/examples/tutorial/flaskr/templates/auth/login.html new file mode 100644 index 0000000000..b326b5a6b8 --- /dev/null +++ b/examples/tutorial/flaskr/templates/auth/login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Log In{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/examples/tutorial/flaskr/templates/auth/register.html b/examples/tutorial/flaskr/templates/auth/register.html new file mode 100644 index 0000000000..4320e17e84 --- /dev/null +++ b/examples/tutorial/flaskr/templates/auth/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/examples/tutorial/flaskr/templates/base.html b/examples/tutorial/flaskr/templates/base.html new file mode 100644 index 0000000000..f09e92687c --- /dev/null +++ b/examples/tutorial/flaskr/templates/base.html @@ -0,0 +1,24 @@ + +{% block title %}{% endblock %} - Flaskr + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
diff --git a/examples/tutorial/flaskr/templates/blog/create.html b/examples/tutorial/flaskr/templates/blog/create.html new file mode 100644 index 0000000000..88e31e44bd --- /dev/null +++ b/examples/tutorial/flaskr/templates/blog/create.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}New Post{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/examples/tutorial/flaskr/templates/blog/index.html b/examples/tutorial/flaskr/templates/blog/index.html new file mode 100644 index 0000000000..3481b8e18d --- /dev/null +++ b/examples/tutorial/flaskr/templates/blog/index.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Posts{% endblock %}

+ {% if g.user %} + New + {% endif %} +{% endblock %} + +{% block content %} + {% for post in posts %} +
+
+
+

{{ post['title'] }}

+
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
+
+ {% if g.user['id'] == post['author_id'] %} + Edit + {% endif %} +
+

{{ post['body'] }}

+
+ {% if not loop.last %} +
+ {% endif %} + {% endfor %} +{% endblock %} diff --git a/examples/tutorial/flaskr/templates/blog/update.html b/examples/tutorial/flaskr/templates/blog/update.html new file mode 100644 index 0000000000..2c405e6303 --- /dev/null +++ b/examples/tutorial/flaskr/templates/blog/update.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+
+
+ +
+{% endblock %} diff --git a/examples/tutorial/setup.cfg b/examples/tutorial/setup.cfg new file mode 100644 index 0000000000..b0cc972d61 --- /dev/null +++ b/examples/tutorial/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +license_file = LICENSE + +[bdist_wheel] +universal = False + +[tool:pytest] +testpaths = tests + +[coverage:run] +branch = True +source = + flaskr diff --git a/examples/tutorial/setup.py b/examples/tutorial/setup.py new file mode 100644 index 0000000000..52a282a22b --- /dev/null +++ b/examples/tutorial/setup.py @@ -0,0 +1,23 @@ +import io + +from setuptools import find_packages, setup + +with io.open('README.rst', 'rt', encoding='utf8') as f: + readme = f.read() + +setup( + name='flaskr', + version='1.0.0', + url='http://flask.pocoo.org/docs/tutorial/', + license='BSD', + maintainer='Pallets team', + maintainer_email='contact@palletsprojects.com', + description='The basic blog app built in the Flask tutorial.', + long_description=readme, + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=[ + 'flask', + ], +) diff --git a/examples/tutorial/tests/conftest.py b/examples/tutorial/tests/conftest.py new file mode 100644 index 0000000000..143d692486 --- /dev/null +++ b/examples/tutorial/tests/conftest.py @@ -0,0 +1,64 @@ +import os +import tempfile + +import pytest +from flaskr import create_app +from flaskr.db import get_db, init_db + +# read in SQL for populating test data +with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: + _data_sql = f.read().decode('utf8') + + +@pytest.fixture +def app(): + """Create and configure a new app instance for each test.""" + # create a temporary file to isolate the database for each test + db_fd, db_path = tempfile.mkstemp() + # create the app with common test config + app = create_app({ + 'TESTING': True, + 'DATABASE': db_path, + }) + + # create the database and load test data + with app.app_context(): + init_db() + get_db().executescript(_data_sql) + + yield app + + # close and remove the temporary database + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture +def client(app): + """A test client for the app.""" + return app.test_client() + + +@pytest.fixture +def runner(app): + """A test runner for the app's Click commands.""" + return app.test_cli_runner() + + +class AuthActions(object): + def __init__(self, client): + self._client = client + + def login(self, username='test', password='test'): + return self._client.post( + '/auth/login', + data={'username': username, 'password': password} + ) + + def logout(self): + return self._client.get('/auth/logout') + + +@pytest.fixture +def auth(client): + return AuthActions(client) diff --git a/examples/tutorial/tests/data.sql b/examples/tutorial/tests/data.sql new file mode 100644 index 0000000000..9b68006510 --- /dev/null +++ b/examples/tutorial/tests/data.sql @@ -0,0 +1,8 @@ +INSERT INTO user (username, password) +VALUES + ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), + ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); + +INSERT INTO post (title, body, author_id, created) +VALUES + ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); diff --git a/examples/tutorial/tests/test_auth.py b/examples/tutorial/tests/test_auth.py new file mode 100644 index 0000000000..884bdf492a --- /dev/null +++ b/examples/tutorial/tests/test_auth.py @@ -0,0 +1,66 @@ +import pytest +from flask import g, session +from flaskr.db import get_db + + +def test_register(client, app): + # test that viewing the page renders without template errors + assert client.get('/auth/register').status_code == 200 + + # test that successful registration redirects to the login page + response = client.post( + '/auth/register', data={'username': 'a', 'password': 'a'} + ) + assert 'http://localhost/auth/login' == response.headers['Location'] + + # test that the user was inserted into the database + with app.app_context(): + assert get_db().execute( + "select * from user where username = 'a'", + ).fetchone() is not None + + +@pytest.mark.parametrize(('username', 'password', 'message'), ( + ('', '', b'Username is required.'), + ('a', '', b'Password is required.'), + ('test', 'test', b'already registered'), +)) +def test_register_validate_input(client, username, password, message): + response = client.post( + '/auth/register', + data={'username': username, 'password': password} + ) + assert message in response.data + + +def test_login(client, auth): + # test that viewing the page renders without template errors + assert client.get('/auth/login').status_code == 200 + + # test that successful login redirects to the index page + response = auth.login() + assert response.headers['Location'] == 'http://localhost/' + + # login request set the user_id in the session + # check that the user is loaded from the session + with client: + client.get('/') + assert session['user_id'] == 1 + assert g.user['username'] == 'test' + + +@pytest.mark.parametrize(('username', 'password', 'message'), ( + ('a', 'test', b'Incorrect username.'), + ('test', 'a', b'Incorrect password.'), +)) +def test_login_validate_input(auth, username, password, message): + response = auth.login(username, password) + assert message in response.data + + +def test_logout(client, auth): + auth.login() + + with client: + auth.logout() + assert 'user_id' not in session diff --git a/examples/tutorial/tests/test_blog.py b/examples/tutorial/tests/test_blog.py new file mode 100644 index 0000000000..11700458b4 --- /dev/null +++ b/examples/tutorial/tests/test_blog.py @@ -0,0 +1,92 @@ +import pytest +from flaskr.db import get_db + + +def test_index(client, auth): + response = client.get('/') + assert b"Log In" in response.data + assert b"Register" in response.data + + auth.login() + response = client.get('/') + assert b'test title' in response.data + assert b'by test on 2018-01-01' in response.data + assert b'test\nbody' in response.data + assert b'href="/1/update"' in response.data + + +@pytest.mark.parametrize('path', ( + '/create', + '/1/update', + '/1/delete', +)) +def test_login_required(client, path): + response = client.post(path) + assert response.headers['Location'] == 'http://localhost/auth/login' + + +def test_author_required(app, client, auth): + # change the post author to another user + with app.app_context(): + db = get_db() + db.execute('UPDATE post SET author_id = 2 WHERE id = 1') + db.commit() + + auth.login() + # current user can't modify other user's post + assert client.post('/1/update').status_code == 403 + assert client.post('/1/delete').status_code == 403 + # current user doesn't see edit link + assert b'href="/1/update"' not in client.get('/').data + + +@pytest.mark.parametrize('path', ( + '/2/update', + '/2/delete', +)) +def test_exists_required(client, auth, path): + auth.login() + assert client.post(path).status_code == 404 + + +def test_create(client, auth, app): + auth.login() + assert client.get('/create').status_code == 200 + client.post('/create', data={'title': 'created', 'body': ''}) + + with app.app_context(): + db = get_db() + count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] + assert count == 2 + + +def test_update(client, auth, app): + auth.login() + assert client.get('/1/update').status_code == 200 + client.post('/1/update', data={'title': 'updated', 'body': ''}) + + with app.app_context(): + db = get_db() + post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() + assert post['title'] == 'updated' + + +@pytest.mark.parametrize('path', ( + '/create', + '/1/update', +)) +def test_create_update_validate(client, auth, path): + auth.login() + response = client.post(path, data={'title': '', 'body': ''}) + assert b'Title is required.' in response.data + + +def test_delete(client, auth, app): + auth.login() + response = client.post('/1/delete') + assert response.headers['Location'] == 'http://localhost/' + + with app.app_context(): + db = get_db() + post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() + assert post is None diff --git a/examples/tutorial/tests/test_db.py b/examples/tutorial/tests/test_db.py new file mode 100644 index 0000000000..99c46d049d --- /dev/null +++ b/examples/tutorial/tests/test_db.py @@ -0,0 +1,28 @@ +import sqlite3 + +import pytest +from flaskr.db import get_db + + +def test_get_close_db(app): + with app.app_context(): + db = get_db() + assert db is get_db() + + with pytest.raises(sqlite3.ProgrammingError) as e: + db.execute('SELECT 1') + + assert 'closed' in str(e) + + +def test_init_db_command(runner, monkeypatch): + class Recorder(object): + called = False + + def fake_init_db(): + Recorder.called = True + + monkeypatch.setattr('flaskr.db.init_db', fake_init_db) + result = runner.invoke(args=['init-db']) + assert 'Initialized' in result.output + assert Recorder.called diff --git a/examples/tutorial/tests/test_factory.py b/examples/tutorial/tests/test_factory.py new file mode 100644 index 0000000000..b7afeae712 --- /dev/null +++ b/examples/tutorial/tests/test_factory.py @@ -0,0 +1,12 @@ +from flaskr import create_app + + +def test_config(): + """Test create_app without passing test config.""" + assert not create_app().testing + assert create_app({'TESTING': True}).testing + + +def test_hello(client): + response = client.get('/hello') + assert response.data == b'Hello, World!' diff --git a/tox.ini b/tox.ini index d74c87589d..cbc72adef5 100644 --- a/tox.ini +++ b/tox.ini @@ -31,9 +31,7 @@ deps = commands = # the examples need to be installed to test successfully - pip install -e examples/flaskr -q - pip install -e examples/minitwit -q - pip install -e examples/patterns/largerapp -q + pip install -e examples/tutorial -q # pytest-cov doesn't seem to play nice with -p coverage run -p -m pytest tests examples