summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2/definingmodels.rst
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tutorials/wiki2/definingmodels.rst')
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst420
1 files changed, 325 insertions, 95 deletions
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index e30af12b2..9159027c4 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -1,126 +1,356 @@
+.. _wiki2_defining_the_domain_model:
+
=========================
Defining the Domain Model
=========================
-The first change we'll make to our stock ``pcreate``-generated application will
-be to define a :term:`domain model` constructor representing a wiki page.
-We'll do this inside our ``models.py`` file.
+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.
-Making Edits to ``models.py``
------------------------------
+We need to add a dependency, the `bcrypt <https://pypi.org/project/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: 13
+ :language: python
+
+Only the highlighted line needs to be added.
.. note::
- There is nothing special about the filename ``models.py``. A
- project may have many models throughout its codebase in arbitrarily-named
- files. Files implementing models often have ``model`` in their filenames
- (or they may live in a Python subpackage of your application package named
- ``models``) , but this is only by convention.
+ 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
+
+ %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.4 cffi-1.11.5 pycparser-2.18 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.
-Open ``tutorial/tutorial/models.py`` file and edit it to look like the
-following:
-.. literalinclude:: src/models/tutorial/models.py
- :linenos:
- :language: py
- :emphasize-lines: 20-22,25
+Edit ``models/__init__.py``
+===========================
-(The highlighted lines are the ones that need to be changed.)
+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.
-The first thing we've done is remove the stock ``MyModel`` class
-from the generated ``models.py`` file. The ``MyModel`` class is only a
-sample and we're not going to use it.
+Open the ``tutorial/models/__init__.py`` file and edit it to look like
+the following:
-Then, we added a ``Page`` class. Because this is a SQLAlchemy application,
-this class inherits from an instance of
-:func:`sqlalchemy.ext.declarative.declarative_base`.
+.. literalinclude:: src/models/tutorial/models/__init__.py
+ :linenos:
+ :language: py
+ :emphasize-lines: 8,9
-.. literalinclude:: src/models/tutorial/models.py
- :pyobject: Page
- :linenos:
- :language: python
+Here we align our imports with the names of the models, ``Page`` and ``User``.
-As you can see, our ``Page`` class has a class level attribute
-``__tablename__`` which equals the string ``'pages'``. This means that
-SQLAlchemy will store our wiki data in a SQL table named ``pages``. Our
-``Page`` class will also have class-level attributes named ``id``, ``name`` and
-``data`` (all instances of :class:`sqlalchemy.schema.Column`).
-These will map to columns in the ``pages`` table.
-The ``id`` attribute will be the primary key in the table.
-The ``name`` attribute will be a text attribute, each value of
-which needs to be unique within the column. The ``data`` attribute is a text
-attribute that will hold the body of each page.
-Changing ``scripts/initializedb.py``
-------------------------------------
+.. _wiki2_migrate_database_alembic:
+
+Migrate the database with Alembic
+=================================
+
+Now that we have written our models, we need to modify the database schema to reflect the changes to our code. Let's generate a new revision, then upgrade the database to the latest revision (head).
+
+On Unix:
+
+.. code-block:: bash
+
+ $VENV/bin/alembic -c development.ini revision --autogenerate \
+ -m "use new models Page and User"
+ $VENV/bin/alembic -c development.ini upgrade head
+
+On Windows:
+
+.. code-block:: doscon
+
+ %VENV%\Scripts\alembic -c development.ini revision \
+ --autogenerate -m "use new models Page and User"
+ %VENV%\Scripts\alembic -c development.ini upgrade head
+
+Success executing these commands will generate output similar to the following.
+
+.. code-block:: text
+
+ 2018-06-29 01:28:42,407 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:28:42,407 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:28:42,408 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:28:42,408 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:28:42,409 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-29 01:28:42,409 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,410 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-29 01:28:42,410 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,411 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT name FROM sqlite_master WHERE type='table' ORDER BY name
+ 2018-06-29 01:28:42,412 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,413 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("models")
+ 2018-06-29 01:28:42,413 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA foreign_key_list("models")
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,415 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_list("models")
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_info("my_index")
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_list("models")
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_info("my_index")
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ Generating /<somepath>/tutorial/tutorial/alembic/versions/20180629_23e9f8eb6c28.py ... done
+
+.. code-block:: text
+
+ 2018-06-29 01:29:37,957 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,963 INFO [sqlalchemy.engine.base.Engine:1151][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)
+ )
+
+
+ 2018-06-29 01:29:37,963 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,966 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,968 INFO [sqlalchemy.engine.base.Engine:1151][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 fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id),
+ CONSTRAINT uq_pages_name UNIQUE (name)
+ )
+
+
+ 2018-06-29 01:29:37,968 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ DROP INDEX my_index
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ DROP TABLE models
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,971 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] UPDATE alembic_version SET version_num='23e9f8eb6c28' WHERE alembic_version.version_num = 'b6b22ae3e628'
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+
+
+.. _wiki2_alembic_overview:
+
+Alembic overview
+----------------
+
+Let's briefly discuss our configuration for Alembic.
+
+In the alchemy cookiecutter's ``development.ini`` file, the setting for ``script_location`` configures Alembic to look for the migration script in the directory ``tutorial/alembic``.
+By default Alembic stores the migration files one level deeper in ``tutorial/alembic/versions``.
+These files are generated by Alembic, then executed when we run upgrade or downgrade migrations.
+The setting ``file_template`` provides the format for each migration's file name.
+We've configured the ``file_template`` setting to make it somewhat easy to find migrations by file name.
+
+At this point in this tutorial, we have two migration files.
+Examine them to see what Alembic will do when you upgrade or downgrade the database to a specific revision.
+Notice the revision identifiers and how they relate to one another in a chained sequence.
+
+.. seealso:: For further information, see the `Alembic documentation <http://alembic.zzzcomputing.com/en/latest/>`_.
+
+
+Edit ``scripts/initialize_db.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).
+directory of your ``tutorial`` package is a file named ``initialize_db.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 one of
-``Page`` and we'll change the very end of the script to create a ``Page``
-rather than a ``MyModel`` and add it to our ``DBSession``.
+Since we've changed our model, we need to make changes to our
+``initialize_db.py`` script. In particular, we'll replace our import of
+``MyModel`` with those of ``User`` and ``Page``. We'll also change the 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/tutorial/scripts/initializedb.py`` and edit it to look like the
+Open ``tutorial/scripts/initialize_db.py`` and edit it to look like the
following:
-.. literalinclude:: src/models/tutorial/scripts/initializedb.py
- :linenos:
- :language: python
- :emphasize-lines: 14,36
-
-(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::
-
- 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread]
- PRAGMA table_info("pages")
- 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2011-11-27 01:22:45,277 INFO [sqlalchemy.engine.base.Engine][MainThread]
- CREATE TABLE pages (
- id INTEGER NOT NULL,
- name TEXT,
- data TEXT,
- PRIMARY KEY (id),
- UNIQUE (name)
- )
-
-
- 2011-11-27 01:22:45,278 INFO [sqlalchemy.engine.base.Engine][MainThread] ()
- 2011-11-27 01:22:45,397 INFO [sqlalchemy.engine.base.Engine][MainThread]
- COMMIT
- 2011-11-27 01:22:45,400 INFO [sqlalchemy.engine.base.Engine][MainThread]
- BEGIN (implicit)
- 2011-11-27 01:22:45,401 INFO [sqlalchemy.engine.base.Engine][MainThread]
- INSERT INTO pages (name, data) VALUES (?, ?)
- 2011-11-27 01:22:45,401 INFO [sqlalchemy.engine.base.Engine][MainThread]
- ('FrontPage', 'This is the front page')
- 2011-11-27 01:22:45,402 INFO [sqlalchemy.engine.base.Engine][MainThread]
- COMMIT
-
-Viewing the Application in a Browser
-------------------------------------
+.. literalinclude:: src/models/tutorial/scripts/initialize_db.py
+ :linenos:
+ :language: python
+ :emphasize-lines: 11-24
+
+Only the highlighted lines need to be changed.
+
+
+Populating the database
+=======================
+
+Because our model has changed, and to repopulate the database, we
+need to rerun the ``initialize_tutorial_db`` command to pick up the changes
+we've made to the initialize_db.py file. See :ref:`initialize_db_wiki2` for instructions.
+
+Success will look something like this:
+
+.. code-block:: text
+
+ 2018-06-29 01:30:39,326 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:30:39,326 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:30:39,327 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:30:39,327 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:30:39,328 INFO [sqlalchemy.engine.base.Engine:682][MainThread] BEGIN (implicit)
+ 2018-06-29 01:30:39,329 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,329 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('editor', 'editor', '$2b$12$PlaJSN7goVbyx8OFs8yAju9n5gHGdI6PZ2QRJGM2jDCiEU4ItUNxy')
+ 2018-06-29 01:30:39,330 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,330 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('basic', 'basic', '$2b$12$MvXdM8jlkbjEyPZ6uXzRg.yatZZK8jCwfPaM7kFkmVJiJjRoCCvmW')
+ 2018-06-29 01:30:39,331 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,331 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('FrontPage', 'This is the front page', 1)
+ 2018-06-29 01:30:39,332 INFO [sqlalchemy.engine.base.Engine:722][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:
+application successfully. If you try to start the application (see
+:ref:`wiki2-start-the-application`) and visit http://localhost:6543, you'll wind up with a Python traceback on
+your console that ends with this exception:
.. code-block:: text
- ImportError: cannot import name MyModel
+ AttributeError: module 'tutorial.models' has no attribute 'MyModel'
This will also happen if you attempt to run the tests.