========================= 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. 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 ``pcreate`` command; it doesn't know about our custom application requirements. We need to add a dependency on the ``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. Running ``setup.py develop`` ============================ Since a new software dependency was added, you will need to run ``python setup.py develop`` 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 $ cd tutorial $ $VENV/bin/python setup.py develop On Windows: .. code-block:: text c:\pyramidtut> cd tutorial c:\pyramidtut\tutorial> %VENV%\Scripts\python setup.py develop Success executing this command will end with a line to the console something like this:: Finished processing dependencies for tutorial==0.0 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.