Skip to content

Commit

Permalink
Merge pull request #3747 from Pylons/wiki2-sqla-2.0
Browse files Browse the repository at this point in the history
upgrade the wiki2 tutorial with the new cookiecutter updates
  • Loading branch information
mmerickel committed Feb 8, 2024
2 parents 53eb7e7 + 222386e commit 03e2e43
Show file tree
Hide file tree
Showing 133 changed files with 1,842 additions and 1,340 deletions.
8 changes: 4 additions & 4 deletions docs/tutorials/wiki2/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ Insert the highlighted line.
In the same file, now edit the ``edit_page`` view function:

.. literalinclude:: src/authentication/tutorial/views/default.py
:lines: 44-59
:lines: 49-68
:lineno-match:
:emphasize-lines: 5-7
:emphasize-lines: 9-11
:language: python

Only the highlighted lines need to be changed.
Expand All @@ -144,9 +144,9 @@ If the user either is not logged in or the user is not the page's creator
In the same file, now edit the ``add_page`` view function:

.. literalinclude:: src/authentication/tutorial/views/default.py
:lines: 61-
:lines: 70-
:lineno-match:
:emphasize-lines: 3-5,13
:emphasize-lines: 3-5,15
:language: python

Only the highlighted lines need to be changed.
Expand Down
16 changes: 8 additions & 8 deletions docs/tutorials/wiki2/authorization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Open the file ``tutorial/routes.py`` and edit the following lines:

.. literalinclude:: src/authorization/tutorial/routes.py
:linenos:
:emphasize-lines: 1-11,18-
:emphasize-lines: 1-11,19-
:language: python

The highlighted lines need to be edited or added.
Expand All @@ -101,7 +101,7 @@ the principals of either ``role:editor`` or ``role:basic`` to have the
``create`` permission:

.. literalinclude:: src/authorization/tutorial/routes.py
:lines: 31-39
:lines: 35-43
:lineno-match:
:emphasize-lines: 5-9
:language: python
Expand All @@ -110,7 +110,7 @@ The ``NewPage`` is loaded as the :term:`context` of the ``add_page`` route by
declaring a ``factory`` on the route:

.. literalinclude:: src/authorization/tutorial/routes.py
:lines: 19-20
:lines: 20-21
:lineno-match:
:emphasize-lines: 1-2
:language: python
Expand All @@ -119,7 +119,7 @@ The ``PageResource`` class defines the :term:`ACL` for a ``Page``. It uses an
actual ``Page`` object to determine *who* can do *what* to the page.

.. literalinclude:: src/authorization/tutorial/routes.py
:lines: 48-
:lines: 54-
:lineno-match:
:emphasize-lines: 5-10
:language: python
Expand All @@ -128,7 +128,7 @@ The ``PageResource`` is loaded as the :term:`context` of the ``view_page`` and
``edit_page`` routes by declaring a ``factory`` on the routes:

.. literalinclude:: src/authorization/tutorial/routes.py
:lines: 18-22
:lines: 19-23
:lineno-match:
:emphasize-lines: 1,4-5
:language: python
Expand Down Expand Up @@ -157,7 +157,7 @@ Edit the ``view_page`` view to declare the ``view`` permission, and remove the
explicit checks within the view:

.. literalinclude:: src/authorization/tutorial/views/default.py
:lines: 18-23
:lines: 19-24
:lineno-match:
:emphasize-lines: 1-2,4
:language: python
Expand All @@ -171,15 +171,15 @@ the view logic.
Edit the ``edit_page`` view to declare the ``edit`` permission:

.. literalinclude:: src/authorization/tutorial/views/default.py
:lines: 38-42
:lines: 41-45
:lineno-match:
:emphasize-lines: 1-2,4
:language: python

Edit the ``add_page`` view to declare the ``create`` permission:

.. literalinclude:: src/authorization/tutorial/views/default.py
:lines: 52-56
:lines: 55-59
:lineno-match:
:emphasize-lines: 1-2,4
:language: python
Expand Down
25 changes: 5 additions & 20 deletions docs/tutorials/wiki2/basiclayout.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,29 +218,14 @@ following:
:linenos:
:language: py

``meta.py`` contains imports and support code for defining the models. We
create a dictionary ``NAMING_CONVENTION`` as well for consistent naming of
support objects like indices and constraints.
``meta.py`` contains imports and support code for defining the models.

.. literalinclude:: src/basiclayout/tutorial/models/meta.py
:end-before: metadata
:linenos:
:language: py

Next we create a ``metadata`` object from the class
:class:`sqlalchemy.schema.MetaData`, using ``NAMING_CONVENTION`` as the value
for the ``naming_convention`` argument.
The core goal of ``meta.py`` is to define a declarative base class (``Base``) that links all of our models together into a shared :class:`sqlalchemy.schema.MetaData`.

A ``MetaData`` object represents the table and other schema definitions for a
single database. We also need to create a declarative ``Base`` object to use as
a base class for our models. Our models will inherit from this ``Base``, which
will attach the tables to the ``metadata`` we created, and define our
application's database schema.
We create the :class:`sqlalchemy.schema.MetaData` with a ``naming_convention`` to support properly naming objects when generating migrations with ``alembic``.

.. literalinclude:: src/basiclayout/tutorial/models/meta.py
:lines: 15-16
:lineno-match:
:language: py
Any object inheriting from the new ``Base`` will be attached to ``metadata`` and
are able to reference eachother in relationships by name.

Next open ``tutorial/models/mymodel.py``, which should already contain the
following:
Expand Down
82 changes: 43 additions & 39 deletions docs/tutorials/wiki2/definingmodels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,21 @@ be to define a wiki page :term:`domain model`.
but this is only a convention and not a requirement.


Declaring dependencies in our ``setup.py`` file
===============================================
Declaring dependencies in our ``pyproject.toml`` file
=====================================================

The models code in our application will depend on a package which is not a
dependency of the original "tutorial" application. The original "tutorial"
application was generated by the cookiecutter; it doesn't know about our
custom application requirements.

We need to add a dependency, the `bcrypt <https://pypi.org/project/bcrypt/>`_ package, to our ``tutorial``
package's ``setup.py`` file by assigning this dependency to the ``requires``
parameter in the ``setup()`` function.
We need to add a dependency, the `bcrypt <https://pypi.org/project/bcrypt/>`_ package, to our ``tutorial`` package's ``pyproject.toml`` file.
Dependencies are defined via the ``dependencies`` key in the ``[project]`` section.

Open ``tutorial/setup.py`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages:
Open ``pyproject.toml`` and edit it to look like the following by adding ``bcrypt`` and sorting the packages:

.. literalinclude:: src/models/setup.py
:lines: 11-24
.. literalinclude:: src/models/pyproject.toml
:lines: 20-33
:linenos:
:lineno-match:
:emphasize-lines: 3
Expand All @@ -56,7 +55,7 @@ Since a new software dependency was added, you will need to run ``pip install
the newly added dependency distribution.

Make sure your current working directory is the root of the project (the
directory in which ``setup.py`` lives) and execute the following command.
directory in which ``pyproject.toml`` lives) and execute the following command.

On Unix:

Expand All @@ -75,7 +74,7 @@ like the following.

.. code-block:: text
Successfully installed bcrypt-3.2.0 cffi-1.14.4 pycparser-2.20 tutorial
Successfully installed bcrypt-4.1.2 tutorial-0.0
Remove ``mymodel.py``
Expand All @@ -96,19 +95,24 @@ Create a new file ``tutorial/models/user.py`` with the following contents:

This is a very basic model for a user who can authenticate with our wiki.

We discussed briefly in the previous chapter that our models will inherit from
an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will
attach the model to our schema.
We discussed briefly in the previous chapter that our models will inherit from an SQLAlchemy :func:`sqlalchemy.orm.DeclarativeBase`.
This will attach the model to our schema.

As you can see, our ``User`` class has a class-level attribute
``__tablename__`` which equals the string ``users``. Our ``User`` class will
also have class-level attributes named ``id``, ``name``, ``password_hash``,
and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will
map to columns in the ``users`` table. The ``id`` attribute will be the primary
key in the table. The ``name`` attribute will be a text column, each value of
which needs to be unique within the column. The ``password_hash`` is a nullable
text attribute that will contain a securely hashed password. Finally, the
``role`` text attribute will hold the role of the user.
As you can see, our ``User`` class has a class-level attribute ``__tablename__`` which equals the string ``users``.
Our ``User`` class will also have class-level attributes named ``id``, ``name``, ``password_hash``, and ``role``.
These attributes will map to columns in the ``users`` table.
The ``id`` attribute will be the primary key in the table.
The ``name`` attribute will be a text column, each value of which needs to be unique within the column.
The ``password_hash`` is a nullable text attribute that will contain a securely hashed password.
Finally, the ``role`` text attribute will hold the role of the user.

.. note::

Read more about how SQLAlchemy defines columns in the ORM at :ref:`sqla:orm_declarative_table_config_toplevel`.
Every column has a base schema inferred from the ``Mapped[...]`` type annotation.
This can define the Python type, for which SQLAlchemy maintains default database type mappings.
It also defines whether the attribute is nullable.
Any further schema attributes like primary keys, unique keys, or overrides to the database types, can be done by also defining the ``mapped_column``.

There are two helper methods that will help us later when using the user
objects. The first is ``set_password`` which will take a raw password and
Expand All @@ -118,10 +122,11 @@ the hashed value of the submitted password against the hashed value of the
password stored in the user's record in the database. If the two hashed values
match, then the submitted password is valid, and we can authenticate the user.

We hash passwords so that it is impossible to decrypt them and use them to
authenticate in the application. If we stored passwords foolishly in clear
text, then anyone with access to the database could retrieve any password to
authenticate as any user.
We hash passwords so that it is impossible to decipher them and use them to authenticate in the application.
If we stored passwords foolishly in clear text and we lost control of the database, all of our users could be compromised.

Notice that we configured a ``created_pages`` relationship, which will make more sense after you create the ``Page`` object in the next section.
This relationship is the reverse side of the ``creator`` one-to-many relationship on the ``Page`` and allows a user object to access a list of all pages in which the user is the creator.


Add ``page.py``
Expand All @@ -141,7 +146,7 @@ here is the ``creator_id`` column, which is a foreign key referencing the
want to relate ``User`` objects with ``Page`` objects, we also define a
``creator`` attribute as an ORM-level mapping between the two tables.
SQLAlchemy will automatically populate this value using the foreign key
referencing the user. Since the foreign key has ``nullable=False``, we are
referencing the user. Since ``creator`` attribute / foreign key is not marked as optional, we are
guaranteed that an instance of ``page`` will have a corresponding
``page.creator``, which will be a ``User`` instance.

Expand Down Expand Up @@ -190,20 +195,19 @@ Success executing these commands will generate output similar to the following.

.. code-block:: text
2021-01-07 08:00:14,550 INFO [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl.
2021-01-07 08:00:14,551 INFO [alembic.runtime.migration:158][MainThread] Will assume non-transactional DDL.
2021-01-07 08:00:14,553 INFO [alembic.autogenerate.compare:134][MainThread] Detected added table 'users'
2021-01-07 08:00:14,553 INFO [alembic.autogenerate.compare:134][MainThread] Detected added table 'pages'
2021-01-07 08:00:14,558 INFO [alembic.autogenerate.compare:622][MainThread] Detected removed index 'my_index' on 'models'
2021-01-07 08:00:14,558 INFO [alembic.autogenerate.compare:176][MainThread] Detected removed table 'models'
Generating <somepath>/tutorial/tutorial/alembic/versions/20210107_bc9a3dead43a.py ... done
2024-02-04 13:29:23,664 INFO [alembic.runtime.migration:216][MainThread] Context impl SQLiteImpl.
2024-02-04 13:29:23,665 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL.
2024-02-04 13:29:23,667 INFO [alembic.autogenerate.compare:189][MainThread] Detected added table 'users'
2024-02-04 13:29:23,667 INFO [alembic.autogenerate.compare:189][MainThread] Detected added table 'pages'
2024-02-04 13:29:23,672 INFO [alembic.autogenerate.compare:672][MainThread] Detected removed index 'my_index' on 'models'
2024-02-04 13:29:23,672 INFO [alembic.autogenerate.compare:230][MainThread] Detected removed table 'models'
Generating <somepath>/tutorial/alembic/versions/20240204_07f9d6b626b2.py ... done
.. code-block:: text
2021-01-07 08:00:21,318 INFO [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl.
2021-01-07 08:00:21,318 INFO [alembic.runtime.migration:158][MainThread] Will assume non-transactional DDL.
2021-01-07 08:00:21,320 INFO [alembic.runtime.migration:517][MainThread] Running upgrade 90658c4a9673 -> bc9a3dead43a, use new models Page and User
2024-02-04 13:32:48,735 INFO [alembic.runtime.migration:216][MainThread] Context impl SQLiteImpl.
2024-02-04 13:32:48,735 INFO [alembic.runtime.migration:219][MainThread] Will assume non-transactional DDL.
2024-02-04 13:32:48,737 INFO [alembic.runtime.migration:622][MainThread] Running upgrade 4b6614165904 -> 07f9d6b626b2, use new models Page and User
.. _wiki2_alembic_overview:

Expand Down Expand Up @@ -235,7 +239,7 @@ command, as we did in the installation step of this tutorial.

.. note::

The command is named ``initialize_tutorial_db`` because of the mapping defined in the ``[console_scripts]`` entry point of our project's ``setup.py`` file.
The command is named ``initialize_tutorial_db`` because of the mapping defined in the ``[project.scripts]`` section of our project's ``pyproject.toml`` file.

Since we've changed our model, we need to make changes to our
``initialize_db.py`` script. In particular, we'll replace our import of
Expand Down
19 changes: 9 additions & 10 deletions docs/tutorials/wiki2/definingviews.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ Remember in the previous chapter we added a new dependency of the ``bcrypt``
package. Again, the view code in our application will depend on a package which
is not a dependency of the original "tutorial" application.

We need to add a dependency on the ``docutils`` package to our ``tutorial``
package's ``setup.py`` file by assigning this dependency to the ``requires``
list.
We need to add a dependency on the ``docutils`` package, to our ``tutorial`` package's ``pyproject.toml`` file.
Dependencies are defined via the ``dependencies`` key in the ``[project]`` section.

Open ``tutorial/setup.py`` and edit it to look like the following:
Open ``pyproject.toml`` and edit it to look like the following:

.. literalinclude:: src/views/setup.py
:lines: 11-25
.. literalinclude:: src/views/pyproject.toml
:lines: 20-34
:lineno-match:
:emphasize-lines: 4
:language: python
Expand Down Expand Up @@ -186,7 +185,7 @@ The ``view_wiki`` view function
Following is the code for the ``view_wiki`` view function and its decorator:

.. literalinclude:: src/views/tutorial/views/default.py
:lines: 16-19
:lines: 17-20
:lineno-match:
:linenos:
:language: python
Expand All @@ -211,7 +210,7 @@ The ``view_page`` view function
Here is the code for the ``view_page`` view function and its decorator:

.. literalinclude:: src/views/tutorial/views/default.py
:lines: 21-41
:lines: 22-46
:lineno-match:
:linenos:
:language: python
Expand Down Expand Up @@ -264,7 +263,7 @@ The ``edit_page`` view function
Here is the code for the ``edit_page`` view function and its decorator:

.. literalinclude:: src/views/tutorial/views/default.py
:lines: 43-55
:lines: 48-64
:lineno-match:
:linenos:
:language: python
Expand Down Expand Up @@ -301,7 +300,7 @@ The ``add_page`` view function
Here is the code for the ``add_page`` view function and its decorator:

.. literalinclude:: src/views/tutorial/views/default.py
:lines: 57-
:lines: 66-
:lineno-match:
:linenos:
:language: python
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/wiki2/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Overall
We choose to use :term:`reStructuredText` markup in the wiki text. Translation
from reStructuredText to HTML is provided by the widely used ``docutils``
Python module. We will add this module to the dependency list in the project's
``setup.py`` file.
``pyproject.toml`` file.

Models
======
Expand Down

0 comments on commit 03e2e43

Please sign in to comment.