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. |