summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2/definingmodels.rst
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-02-18 02:39:43 -0600
committerMichael Merickel <michael@merickel.org>2016-02-18 02:39:43 -0600
commit5dc1c80046b7eb83fb7c51105bed0e73b2ab759c (patch)
treea66d8ee8f8282a72a5ada4fc5d903486caac6a85 /docs/tutorials/wiki2/definingmodels.rst
parent3eb1c354d320536ee470b79dcb930d20da93d97d (diff)
parent66fabb4ac707b5b4289db0094756f1a1af7269cc (diff)
downloadpyramid-5dc1c80046b7eb83fb7c51105bed0e73b2ab759c.tar.gz
pyramid-5dc1c80046b7eb83fb7c51105bed0e73b2ab759c.tar.bz2
pyramid-5dc1c80046b7eb83fb7c51105bed0e73b2ab759c.zip
Merge pull request #2334 from mmerickel/feature/alchemy-scaffold-update-tweaks
object-level security and tons of other small improvements to the wiki2 tutorial
Diffstat (limited to 'docs/tutorials/wiki2/definingmodels.rst')
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst245
1 files changed, 176 insertions, 69 deletions
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index b38177d04..41f36fa26 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -3,71 +3,148 @@ Defining the Domain Model
=========================
The first change we'll make to our stock ``pcreate``-generated application will
-be to define a :term:`domain model` constructor representing a wiki page.
-We'll do this inside our ``mymodel.py`` file.
+be to define a wiki page :term:`domain model`.
+.. note::
-Edit ``mymodel.py``
--------------------
+ There is nothing special about the filename ``user.py`` or ``page.py`` except
+ that they are Python modules. A project may have many models throughout its
+ codebase in arbitrarily named modules. Modules implementing models often
+ have ``model`` in their names or they may live in a Python subpackage of
+ your application package named ``models`` (as we've done in this tutorial),
+ but this is only a convention and not a requirement.
-.. note::
- There is nothing special about the filename ``mymodel.py``. A
- project may have many models throughout its codebase in arbitrarily named
- files. Files implementing models often have ``model`` in their filenames
- or they may live in a Python subpackage of your application package named
- ``models`` (as we've done in this tutorial), but this is only by convention.
+Declaring dependencies in our ``setup.py`` file
+===============================================
-Open the ``tutorial/tutorial/models/mymodel.py`` file and edit it to look like
-the following:
+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 ``pcreate`` command; it doesn't know
+about our custom application requirements.
+
+We need to add a dependency on the ``bcrypt`` package to our ``tutorial``
+package's ``setup.py`` file by assigning this dependency to the ``requires``
+parameter in the ``setup()`` function.
+
+Open ``tutorial/setup.py`` and edit it to look like the following:
-.. literalinclude:: src/models/tutorial/models/mymodel.py
+.. literalinclude:: src/models/setup.py
+ :linenos:
+ :emphasize-lines: 12
+ :language: python
+
+Only the highlighted line needs to be added.
+
+
+Running ``setup.py develop``
+============================
+
+Since a new software dependency was added, you will need to run ``python
+setup.py develop`` again inside the root of the ``tutorial`` package to obtain
+and register 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.
+
+On UNIX:
+
+.. code-block:: bash
+
+ $ cd tutorial
+ $ $VENV/bin/python setup.py develop
+
+On Windows:
+
+.. code-block:: text
+
+ c:\pyramidtut> cd tutorial
+ c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop
+
+Success executing this command will end with a line to the console something
+like this::
+
+ Finished processing dependencies for tutorial==0.0
+
+
+Remove ``mymodel.py``
+---------------------
+
+The first thing we'll do is delete the file ``tutorial/models/mymodel.py``.
+The ``MyModel`` class is only a sample and we're not going to use it.
+
+
+Add ``user.py``
+---------------
+
+Create a new file ``tutorial/models/user.py`` with the following contents:
+
+.. literalinclude:: src/models/tutorial/models/user.py
:linenos:
:language: py
- :emphasize-lines: 9-11,13,14
-The highlighted lines are the ones that need to be changed, as well as
-removing lines that reference ``Index``.
+This is a very basic model for a user who can authenticate with our wiki.
-The first thing we've done is remove the stock ``MyModel`` class
-from the generated ``models.py`` file. The ``MyModel`` class is only a
-sample and we're not going to use it.
+We discussed briefly in the previous chapter that our models will inherit
+from a SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This
+will attach the model to our schema.
-Then we added a ``Page`` class. Because this is an SQLAlchemy application,
-this class inherits from an instance of
-:func:`sqlalchemy.ext.declarative.declarative_base`.
+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 [1]_. Finally, the ``role`` text attribute
+will hold the role of the user.
-.. literalinclude:: src/models/tutorial/models/mymodel.py
- :pyobject: Page
+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 transform it using bcrypt_ into an irreversible representation. The
+``check_password`` method will allow us to compare input passwords to
+see if they resolve to the same hash signifying a match.
+
+
+Add ``page.py``
+---------------
+
+Create a new file ``tutorial/models/page.py`` with the following contents:
+
+.. literalinclude:: src/models/tutorial/models/page.py
:linenos:
- :language: python
+ :language: py
-As you can see, our ``Page`` class has a class level attribute
-``__tablename__`` which equals the string ``'pages'``. This means that
-SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our
-``Page`` class will also have class-level attributes named ``id``, ``name``,
-and ``data`` (all instances of :class:`sqlalchemy.schema.Column`). These will
-map to columns in the ``pages`` table. The ``id`` attribute will be the
-primary key in the table. The ``name`` attribute will be a text attribute,
-each value of which needs to be unique within the column. The ``data``
-attribute is a text attribute that will hold the body of each page.
+As you can see, our ``Page`` class is very similar to the ``User`` defined
+above except with attributes focused on storing information about a wiki
+page including ``id``, ``name``, and ``data``. The only new construct
+introduced here is the ``creator_id`` column which is a foreign key
+referencing the ``users`` table. Foreign keys are very useful at the
+schema-level but since we want to relate ``User`` objects with ``Page``
+objects we also define a the ``creator`` attribute which is 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 guaranteed that an instance of ``page`` will
+have a corresponding ``page.creator`` which will be a ``User`` instance.
Edit ``models/__init__.py``
---------------------------
Since we are using a package for our models, we also need to update our
-``__init__.py`` file.
+``__init__.py`` file to ensure that the models are attached to the metadata.
-Open the ``tutorial/tutorial/models/__init__.py`` file and edit it to look like
+Open the ``tutorial/models/__init__.py`` file and edit it to look like
the following:
.. literalinclude:: src/models/tutorial/models/__init__.py
:linenos:
:language: py
- :emphasize-lines: 4
+ :emphasize-lines: 8,9
-Here we need to align our import with the name of the model ``Page``.
+Here we need to align our imports with the names of the models ``User``,
+and ``Page``.
Edit ``scripts/initializedb.py``
@@ -76,25 +153,23 @@ Edit ``scripts/initializedb.py``
We haven't looked at the details of this file yet, but within the ``scripts``
directory of your ``tutorial`` package is a file named ``initializedb.py``.
Code in this file is executed whenever we run the ``initialize_tutorial_db``
-command, as we did in the installation step of this tutorial.
+command, as we did in the installation step of this tutorial [2]_.
Since we've changed our model, we need to make changes to our
``initializedb.py`` script. In particular, we'll replace our import of
-``MyModel`` with one of ``Page`` and we'll change the very end of the script
-to create a ``Page`` rather than a ``MyModel`` and add it to our
-``DBSession``.
+``MyModel`` with those of ``User`` and ``Page`` and we'll change the very end
+of the script to create two ``User`` objects (``basic`` and ``editor``) and a
+``Page`` rather than a ``MyModel`` and add them to our ``dbsession``.
-Open ``tutorial/tutorial/scripts/initializedb.py`` and edit it to look like
+Open ``tutorial/scripts/initializedb.py`` and edit it to look like
the following:
.. literalinclude:: src/models/tutorial/scripts/initializedb.py
:linenos:
:language: python
- :emphasize-lines: 16,31,41
+ :emphasize-lines: 18,44-57
-Only the highlighted lines need to be changed, as well as removing the lines
-referencing ``pyramid.scripts.common`` and ``options`` under the ``main``
-function.
+Only the highlighted lines need to be changed.
Installing the project and re-initializing the database
@@ -107,28 +182,49 @@ made to both the models.py file and to the initializedb.py file. See
Success will look something like this::
- 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2015-05-24 15:34:14,542 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
- 2015-05-24 15:34:14,543 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
- 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-24 15:34:14,544 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
- CREATE TABLE pages (
- id INTEGER NOT NULL,
- name TEXT,
- data TEXT,
- PRIMARY KEY (id),
- UNIQUE (name)
- )
-
-
- 2015-05-24 15:34:14,545 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
- 2015-05-24 15:34:14,546 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
- 2015-05-24 15:34:14,548 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
- 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data) VALUES (?, ?)
- 2015-05-24 15:34:14,549 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page')
- 2015-05-24 15:34:14,550 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2016-02-12 01:06:35,855 INFO [sqlalchemy.engine.base.Engine:1193][MainThread] ()
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users")
+ 2016-02-12 01:06:35,856 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE users (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ role TEXT NOT NULL,
+ password_hash TEXT,
+ CONSTRAINT pk_users PRIMARY KEY (id),
+ CONSTRAINT uq_users_name UNIQUE (name)
+ )
+
+
+ 2016-02-12 01:06:35,857 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-12 01:06:35,858 INFO [sqlalchemy.engine.base.Engine:1097][MainThread]
+ CREATE TABLE pages (
+ id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ data INTEGER NOT NULL,
+ creator_id INTEGER NOT NULL,
+ CONSTRAINT pk_pages PRIMARY KEY (id),
+ CONSTRAINT uq_pages_name UNIQUE (name),
+ CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id)
+ )
+
+
+ 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ()
+ 2016-02-12 01:06:35,859 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
+ 2016-02-12 01:06:36,383 INFO [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', '$2b$12$bSr5QR3wFs1LAnld7R94e.TXPj7DVoTxu2hA1kY6rm.Q3cAhD.AQO')
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2016-02-12 01:06:36,384 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', '$2b$12$.v0BQK2xWEQOnywbX2BFs.qzXo5Qf9oZohGWux/MOSj6Z.pVaY2Z6')
+ 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
+ 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1)
+ 2016-02-12 01:06:36,385 INFO [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
View the application in a browser
---------------------------------
@@ -144,3 +240,14 @@ up with a Python traceback on your console that ends with this exception:
ImportError: cannot import name MyModel
This will also happen if you attempt to run the tests.
+
+.. _bcrypt: https://pypi.python.org/pypi/bcrypt
+
+.. [1] We are using the bcrypt_ package from PyPI to hash our passwords
+ securely. There are other one-way hash algorithms for passwords if
+ bcrypt is an issue on your system. Just make sure that it's an
+ algorithm approved for storing passwords versus a generic one-way hash.
+
+.. [2] 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.