Steve Piercy
2017-06-11 2c0e3e334955574383fa73eaf932931199e13a8a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
.. _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.