.. _wiki2_defining_the_domain_model:
|
|
=========================
|
Defining the Domain Model
|
=========================
|
|
The first change we'll make to our stock cookiecutter-generated application will
|
be to define a wiki page :term:`domain model`.
|
|
.. note::
|
|
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.
|
|
|
Declaring dependencies in our ``setup.py`` 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.python.org/pypi/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/setup.py
|
:linenos:
|
:emphasize-lines: 12
|
:language: python
|
|
Only the highlighted line needs to be added.
|
|
.. note::
|
|
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.
|
|
|
Running ``pip install -e .``
|
============================
|
|
Since a new software dependency was added, you will need to run ``pip install
|
-e .`` 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
|
|
$ $VENV/bin/pip install -e .
|
|
On Windows:
|
|
.. code-block:: doscon
|
|
c:\tutorial> %VENV%\Scripts\pip install -e .
|
|
Success executing this command will end with a line to the console something
|
like the following.
|
|
.. code-block:: text
|
|
Successfully installed bcrypt-3.1.2 cffi-1.9.1 pycparser-2.17 tutorial
|
|
|
Remove ``mymodel.py``
|
=====================
|
|
Let's 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
|
|
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.
|
|
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.
|
|
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, a process known
|
as "hashing". The second method, ``check_password``, will allow us to compare
|
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.
|
|
|
Add ``page.py``
|
===============
|
|
Create a new file ``tutorial/models/page.py`` with the following contents:
|
|
.. literalinclude:: src/models/tutorial/models/page.py
|
:linenos:
|
:language: py
|
|
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
|
``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
|
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 to ensure that the models are attached to the metadata.
|
|
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: 8,9
|
|
Here we align our imports with the names of the models, ``Page`` and ``User``.
|
|
|
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.
|
|
.. 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.
|
|
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 those of ``User`` and ``Page``. We'll also change the very end
|
of the script to create two ``User`` objects (``basic`` and ``editor``) as well
|
as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``.
|
|
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: 18,44-57
|
|
Only the highlighted lines need to be changed.
|
|
|
Installing the project and re-initializing the database
|
=======================================================
|
|
Because our model has changed, and in order to reinitialize the database, we
|
need to rerun the ``initialize_tutorial_db`` command to pick up the changes
|
we've made to both the models.py file and to the initializedb.py file. See
|
:ref:`initialize_db_wiki2` for instructions.
|
|
Success will look something like this:
|
|
.. code-block:: bash
|
|
2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
|
2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1236][MainThread] ()
|
2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
|
2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1236][MainThread] ()
|
2016-12-20 02:51:11,196 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("pages")
|
2016-12-20 02:51:11,196 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
|
2016-12-20 02:51:11,196 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("users")
|
2016-12-20 02:51:11,197 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
|
2016-12-20 02:51:11,197 INFO [sqlalchemy.engine.base.Engine:1140][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-12-20 02:51:11,197 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
|
2016-12-20 02:51:11,198 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
|
2016-12-20 02:51:11,199 INFO [sqlalchemy.engine.base.Engine:1140][MainThread]
|
CREATE TABLE pages (
|
id INTEGER NOT NULL,
|
name TEXT NOT NULL,
|
data TEXT 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-12-20 02:51:11,199 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
|
2016-12-20 02:51:11,200 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
|
2016-12-20 02:51:11,755 INFO [sqlalchemy.engine.base.Engine:679][MainThread] BEGIN (implicit)
|
2016-12-20 02:51:11,755 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
|
2016-12-20 02:51:11,755 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('editor', 'editor', '$2b$12$ds7h2Zb7.l6TEFup5h8f4ekA9GRfEpE1yQGDRvT9PConw73kKuupG')
|
2016-12-20 02:51:11,756 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
|
2016-12-20 02:51:11,756 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('basic', 'basic', '$2b$12$KgruXP5Vv7rikr6dGB3TF.flGXYpiE0Li9K583EVomjY.SYmQOsyi')
|
2016-12-20 02:51:11,757 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
|
2016-12-20 02:51:11,757 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('FrontPage', 'This is the front page', 1)
|
2016-12-20 02:51:11,757 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
|
|
|
View the application in a browser
|
=================================
|
|
We can't. At this point, our system is in a "non-runnable" state; we'll need
|
to change view-related files in the next chapter to be able to start the
|
application successfully. If you try to start the application (see
|
:ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on
|
your console that ends with this exception:
|
|
.. code-block:: text
|
|
ImportError: cannot import name MyModel
|
|
This will also happen if you attempt to run the tests.
|