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
|
=========================
Defining the Domain Model
=========================
The first change we'll make to our stock ``pcreate``-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.
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
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 a 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 [1]_. 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. 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: 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 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 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 need to align our imports with the names of the models ``User``,
and ``Page``.
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 [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 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/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, in order to reinitialize the database, we need
to rerun the ``initialize_tutorial_db`` command to pick up the changes you'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::
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
---------------------------------
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.
.. _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.
|