Steve Piercy
2017-06-11 2c0e3e334955574383fa73eaf932931199e13a8a
commit | author | age
640d77 1 .. _wiki2_defining_the_domain_model:
SP 2
701193 3 =========================
CM 4 Defining the Domain Model
5 =========================
e26700 6
b29848 7 The first change we'll make to our stock cookiecutter-generated application will
d6243a 8 be to define a wiki page :term:`domain model`.
e26700 9
CM 10 .. note::
11
574ba1 12   There is nothing special about the filename ``user.py`` or ``page.py`` except
MM 13   that they are Python modules.  A project may have many models throughout its
14   codebase in arbitrarily named modules.  Modules implementing models often
15   have ``model`` in their names or they may live in a Python subpackage of
16   your application package named ``models`` (as we've done in this tutorial),
17   but this is only a convention and not a requirement.
e26700 18
893465 19
a115c6 20 Declaring dependencies in our ``setup.py`` file
MM 21 ===============================================
22
23 The models code in our application will depend on a package which is not a
24 dependency of the original "tutorial" application.  The original "tutorial"
b29848 25 application was generated by the cookiecutter; it doesn't know about our
15fb09 26 custom application requirements.
a115c6 27
b29848 28 We need to add a dependency, the `bcrypt <https://pypi.python.org/pypi/bcrypt>`_ package, to our ``tutorial``
a115c6 29 package's ``setup.py`` file by assigning this dependency to the ``requires``
MM 30 parameter in the ``setup()`` function.
31
32 Open ``tutorial/setup.py`` and edit it to look like the following:
33
34 .. literalinclude:: src/models/setup.py
35    :linenos:
36    :emphasize-lines: 12
37    :language: python
38
39 Only the highlighted line needs to be added.
b29848 40
SP 41 .. note::
42
446fae 43     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.
a115c6 44
MM 45
9ff535 46 Running ``pip install -e .``
23c2d7 47 ============================
MM 48
9ff535 49 Since a new software dependency was added, you will need to run ``pip install
SP 50 -e .`` again inside the root of the ``tutorial`` package to obtain and register
51 the newly added dependency distribution.
23c2d7 52
MM 53 Make sure your current working directory is the root of the project (the
54 directory in which ``setup.py`` lives) and execute the following command.
55
56 On UNIX:
57
58 .. code-block:: bash
59
b29848 60     $ $VENV/bin/pip install -e .
23c2d7 61
MM 62 On Windows:
63
a651b3 64 .. code-block:: doscon
23c2d7 65
b29848 66     c:\tutorial> %VENV%\Scripts\pip install -e .
23c2d7 67
MM 68 Success executing this command will end with a line to the console something
b29848 69 like the following.
23c2d7 70
b29848 71 .. code-block:: text
SP 72
73     Successfully installed bcrypt-3.1.2 cffi-1.9.1 pycparser-2.17 tutorial
23c2d7 74
MM 75
574ba1 76 Remove ``mymodel.py``
b29848 77 =====================
574ba1 78
15fb09 79 Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is
SP 80 only a sample and we're not going to use it.
574ba1 81
MM 82
83 Add ``user.py``
b29848 84 ===============
574ba1 85
MM 86 Create a new file ``tutorial/models/user.py`` with the following contents:
87
88 .. literalinclude:: src/models/tutorial/models/user.py
893465 89    :linenos:
KS 90    :language: py
197808 91
574ba1 92 This is a very basic model for a user who can authenticate with our wiki.
893465 93
15fb09 94 We discussed briefly in the previous chapter that our models will inherit from
SP 95 an SQLAlchemy :func:`sqlalchemy.ext.declarative.declarative_base`. This will
96 attach the model to our schema.
5edd54 97
574ba1 98 As you can see, our ``User`` class has a class-level attribute
15fb09 99 ``__tablename__`` which equals the string ``users``. Our ``User`` class will
SP 100 also have class-level attributes named ``id``, ``name``, ``password_hash``,
101 and ``role`` (all instances of :class:`sqlalchemy.schema.Column`). These will
102 map to columns in the ``users`` table. The ``id`` attribute will be the primary
103 key in the table. The ``name`` attribute will be a text column, each value of
104 which needs to be unique within the column. The ``password_hash`` is a nullable
b29848 105 text attribute that will contain a securely hashed password. Finally, the
15fb09 106 ``role`` text attribute will hold the role of the user.
e26700 107
15fb09 108 There are two helper methods that will help us later when using the user
SP 109 objects. The first is ``set_password`` which will take a raw password and
b29848 110 transform it using ``bcrypt`` into an irreversible representation, a process known
15fb09 111 as "hashing". The second method, ``check_password``, will allow us to compare
SP 112 the hashed value of the submitted password against the hashed value of the
113 password stored in the user's record in the database. If the two hashed values
114 match, then the submitted password is valid, and we can authenticate the user.
115
116 We hash passwords so that it is impossible to decrypt them and use them to
117 authenticate in the application. If we stored passwords foolishly in clear
118 text, then anyone with access to the database could retrieve any password to
119 authenticate as any user.
574ba1 120
MM 121
122 Add ``page.py``
b29848 123 ===============
574ba1 124
MM 125 Create a new file ``tutorial/models/page.py`` with the following contents:
126
127 .. literalinclude:: src/models/tutorial/models/page.py
0aed1c 128    :linenos:
574ba1 129    :language: py
0aed1c 130
574ba1 131 As you can see, our ``Page`` class is very similar to the ``User`` defined
2dc061 132 above, except with attributes focused on storing information about a wiki page,
SP 133 including ``id``, ``name``, and ``data``. The only new construct introduced
134 here is the ``creator_id`` column, which is a foreign key referencing the
135 ``users`` table. Foreign keys are very useful at the schema-level, but since we
136 want to relate ``User`` objects with ``Page`` objects, we also define a
137 ``creator`` attribute as an ORM-level mapping between the two tables.
138 SQLAlchemy will automatically populate this value using the foreign key
139 referencing the user. Since the foreign key has ``nullable=False``, we are
140 guaranteed that an instance of ``page`` will have a corresponding
141 ``page.creator``, which will be a ``User`` instance.
e26700 142
bfa499 143
SP 144 Edit ``models/__init__.py``
b29848 145 ===========================
bfa499 146
SP 147 Since we are using a package for our models, we also need to update our
574ba1 148 ``__init__.py`` file to ensure that the models are attached to the metadata.
bfa499 149
d1cb34 150 Open the ``tutorial/models/__init__.py`` file and edit it to look like
bfa499 151 the following:
SP 152
153 .. literalinclude:: src/models/tutorial/models/__init__.py
154    :linenos:
155    :language: py
2c0e3e 156    :emphasize-lines: 8,9
bfa499 157
9ff535 158 Here we align our imports with the names of the models, ``Page`` and ``User``.
bfa499 159
SP 160
161 Edit ``scripts/initializedb.py``
b29848 162 ================================
0aed1c 163
b3e608 164 We haven't looked at the details of this file yet, but within the ``scripts``
dd6232 165 directory of your ``tutorial`` package is a file named ``initializedb.py``.
SP 166 Code in this file is executed whenever we run the ``initialize_tutorial_db``
b29848 167 command, as we did in the installation step of this tutorial.
SP 168
169 .. note::
170
171     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.
0aed1c 172
dd6232 173 Since we've changed our model, we need to make changes to our
SP 174 ``initializedb.py`` script.  In particular, we'll replace our import of
2dc061 175 ``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end
SP 176 of the script to create two ``User`` objects (``basic`` and ``editor``) as well
177 as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``.
e26700 178
2dc061 179 Open ``tutorial/scripts/initializedb.py`` and edit it to look like the
SP 180 following:
e26700 181
c884ee 182 .. literalinclude:: src/models/tutorial/scripts/initializedb.py
e26700 183    :linenos:
CM 184    :language: python
574ba1 185    :emphasize-lines: 18,44-57
c4b64f 186
d6243a 187 Only the highlighted lines need to be changed.
e26700 188
bfa499 189
1204f0 190 Installing the project and re-initializing the database
b29848 191 =======================================================
af1a96 192
2dc061 193 Because our model has changed, and in order to reinitialize the database, we
SP 194 need to rerun the ``initialize_tutorial_db`` command to pick up the changes
195 we've made to both the models.py file and to the initializedb.py file. See
dd6232 196 :ref:`initialize_db_wiki2` for instructions.
5edd54 197
9ff535 198 Success will look something like this:
5edd54 199
9ff535 200 .. code-block:: bash
SP 201
b29848 202     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
SP 203     2016-12-20 02:51:11,195 INFO  [sqlalchemy.engine.base.Engine:1236][MainThread] ()
204     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
205     2016-12-20 02:51:11,195 INFO  [sqlalchemy.engine.base.Engine:1236][MainThread] ()
206     2016-12-20 02:51:11,196 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("pages")
207     2016-12-20 02:51:11,196 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
208     2016-12-20 02:51:11,196 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("users")
209     2016-12-20 02:51:11,197 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
210     2016-12-20 02:51:11,197 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread]
211     CREATE TABLE users (
212             id INTEGER NOT NULL,
213             name TEXT NOT NULL,
214             role TEXT NOT NULL,
215             password_hash TEXT,
216             CONSTRAINT pk_users PRIMARY KEY (id),
217             CONSTRAINT uq_users_name UNIQUE (name)
218     )
5edd54 219
CM 220
b29848 221     2016-12-20 02:51:11,197 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
SP 222     2016-12-20 02:51:11,198 INFO  [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
223     2016-12-20 02:51:11,199 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread]
224     CREATE TABLE pages (
225             id INTEGER NOT NULL,
226             name TEXT NOT NULL,
227             data TEXT NOT NULL,
228             creator_id INTEGER NOT NULL,
229             CONSTRAINT pk_pages PRIMARY KEY (id),
230             CONSTRAINT uq_pages_name UNIQUE (name),
231             CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id)
232     )
e6e4f6 233
MM 234
b29848 235     2016-12-20 02:51:11,199 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
SP 236     2016-12-20 02:51:11,200 INFO  [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
237     2016-12-20 02:51:11,755 INFO  [sqlalchemy.engine.base.Engine:679][MainThread] BEGIN (implicit)
238     2016-12-20 02:51:11,755 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
239     2016-12-20 02:51:11,755 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ('editor', 'editor', '$2b$12$ds7h2Zb7.l6TEFup5h8f4ekA9GRfEpE1yQGDRvT9PConw73kKuupG')
240     2016-12-20 02:51:11,756 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
241     2016-12-20 02:51:11,756 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ('basic', 'basic', '$2b$12$KgruXP5Vv7rikr6dGB3TF.flGXYpiE0Li9K583EVomjY.SYmQOsyi')
242     2016-12-20 02:51:11,757 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
243     2016-12-20 02:51:11,757 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ('FrontPage', 'This is the front page', 1)
244     2016-12-20 02:51:11,757 INFO  [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
9ff535 245
6772a2 246
1204f0 247 View the application in a browser
b29848 248 =================================
e26700 249
6ed41a 250 We can't.  At this point, our system is in a "non-runnable" state; we'll need
CM 251 to change view-related files in the next chapter to be able to start the
2dc061 252 application successfully.  If you try to start the application (see
SP 253 :ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on
254 your console that ends with this exception:
e26700 255
CM 256 .. code-block:: text
257
258    ImportError: cannot import name MyModel
39f8a0 259
CM 260 This will also happen if you attempt to run the tests.