| 1 | .. _tutorial-videostore:
|
|---|
| 2 |
|
|---|
| 3 | ########################################
|
|---|
| 4 | Creating a Movie Database Application
|
|---|
| 5 | ########################################
|
|---|
| 6 |
|
|---|
| 7 | :Release: |version|
|
|---|
| 8 | :Date: |today|
|
|---|
| 9 |
|
|---|
| 10 | In this tutorial we will create a fully functional movie database application
|
|---|
| 11 | with Camelot. We assume Camelot is properly :ref:`installed <doc-install>`.
|
|---|
| 12 |
|
|---|
| 13 | Starting a New Project
|
|---|
| 14 | ======================
|
|---|
| 15 |
|
|---|
| 16 | We begin with the creation of a new project. Typing the following command in
|
|---|
| 17 | your favorite command prompt (or shell) creates one::
|
|---|
| 18 |
|
|---|
| 19 | python PTC\camelot\bin\camelot_admin.py startproject videostore
|
|---|
| 20 |
|
|---|
| 21 | Under linux, you may have to adjust the folder separator. This tutorial has
|
|---|
| 22 | been written under the Windows XP operating system. The pictures also reflect
|
|---|
| 23 | that operating system.
|
|---|
| 24 |
|
|---|
| 25 | `PTC` is the path to Camelot main directory. The folder :file:`videostore`
|
|---|
| 26 | should appear in your the directory you are working in. We will be working the
|
|---|
| 27 | Python modules created and put inside this directory.
|
|---|
| 28 |
|
|---|
| 29 | Main Window and Views
|
|---|
| 30 | =====================
|
|---|
| 31 |
|
|---|
| 32 | :option:`camelot_admin.py` created some modules for us. Let's focus on the
|
|---|
| 33 | one called :file:`main.py` which contains the entry point of your Camelot
|
|---|
| 34 | application. If you launch it::
|
|---|
| 35 |
|
|---|
| 36 | python videostore\main.py
|
|---|
| 37 |
|
|---|
| 38 | your `PyQt <http://www.riverbankcomputing.co.uk/software/pyqt/intro>`_
|
|---|
| 39 | :abbr:`Graphical User Interface <GUI>` should look like the one we show in the
|
|---|
| 40 | picture below:
|
|---|
| 41 |
|
|---|
| 42 | .. image:: ../_static/picture1.png
|
|---|
| 43 |
|
|---|
| 44 | The application has menus, a toolbar, a left navigation pane, and a central
|
|---|
| 45 | area on which nothing is currently displayed.
|
|---|
| 46 |
|
|---|
| 47 | The navigation pane has its first button selected. Select any other button by
|
|---|
| 48 | clicking on it, and see the nagivation tree fill itself with new entries.
|
|---|
| 49 | These are `entities`, and we will talk about them later. (Generally speaking,
|
|---|
| 50 | an `entity` represents a single table in a database.)
|
|---|
| 51 |
|
|---|
| 52 | .. image:: ../_static/picture2.png
|
|---|
| 53 |
|
|---|
| 54 | .. note::
|
|---|
| 55 |
|
|---|
| 56 | Camelot uses `sections` to group `models`. Each button in the navigation
|
|---|
| 57 | pane represents a `section`, and each entry of the navigation tree is part
|
|---|
| 58 | of this section.
|
|---|
| 59 |
|
|---|
| 60 | Notice that the application disables most of the menus and the toolbar
|
|---|
| 61 | buttons. When we click on an entity, more options become available.
|
|---|
| 62 | So let's click on the entity ``Persons`` of the section ``Relations``.
|
|---|
| 63 |
|
|---|
| 64 | A child window appears in the previously empty area and is maximized by
|
|---|
| 65 | default: this is the table view of the entity.
|
|---|
| 66 |
|
|---|
| 67 | .. image:: ../_static/picture3.png
|
|---|
| 68 |
|
|---|
| 69 | Each row is a record with some fields that we can edit (others might not be
|
|---|
| 70 | editable). Let's now add a new row by clicking on the new icon (next to the
|
|---|
| 71 | trash bin icon; which removes a row).
|
|---|
| 72 |
|
|---|
| 73 | .. image:: ../_static/picture4.png
|
|---|
| 74 |
|
|---|
| 75 | We now see a form view with additional fields. Forms are not maximized by
|
|---|
| 76 | default. Forms label **required** fields in bold.
|
|---|
| 77 |
|
|---|
| 78 | .. image:: ../_static/picture5.png
|
|---|
| 79 |
|
|---|
| 80 | Fill in a first and last name, and close the form. Camelot will automatically
|
|---|
| 81 | validate and echo the changes to the database. We can reopen the form by
|
|---|
| 82 | clicking on the blue icon in the first column of each row of the table. Notice
|
|---|
| 83 | also that there is now an entry in our table.
|
|---|
| 84 |
|
|---|
| 85 | .. image:: ../_static/picture6.png
|
|---|
| 86 |
|
|---|
| 87 | That's it for basic usages of the interface. Next we will write code for our
|
|---|
| 88 | database model.
|
|---|
| 89 |
|
|---|
| 90 |
|
|---|
| 91 | Creating the Movie Model
|
|---|
| 92 | ========================
|
|---|
| 93 |
|
|---|
| 94 | Let's first take a look at the :file:`settings.py` in our project directory.
|
|---|
| 95 | There is an attribute, ``ENGINE``, an anonymous function, which returns a
|
|---|
| 96 | :abbr:`Uniform Resource Identifier URI`. That's the database your Camelot
|
|---|
| 97 | application will be connecting too. Camelot provides a default ``sqlite`` URI
|
|---|
| 98 | scheme. But you can set your own.
|
|---|
| 99 |
|
|---|
| 100 | If you set a database file that does not exist it will be created in the
|
|---|
| 101 | directory from which the application is *launched*.
|
|---|
| 102 |
|
|---|
| 103 | Now we can look at :file:`model.py`. Camelot has already imported some classes
|
|---|
| 104 | for us. They are used to create our entities. Let's say we want a movie entity
|
|---|
| 105 | with a ``title``, a short ``description``, a ``release date``, and a
|
|---|
| 106 | ``genre``.
|
|---|
| 107 |
|
|---|
| 108 | The aforementioned specifications translate into the following Python code,
|
|---|
| 109 | that we add to our model.py module::
|
|---|
| 110 |
|
|---|
| 111 | class Movie(Entity):
|
|---|
| 112 | using_options(tablename='movie')
|
|---|
| 113 |
|
|---|
| 114 | title = Field(Unicode(60), required=True)
|
|---|
| 115 | short_description = Field(Unicode(512))
|
|---|
| 116 | release_date = Field(Date)
|
|---|
| 117 | genre = Field(Unicode(15))
|
|---|
| 118 |
|
|---|
| 119 | ``Movie`` inherits ``Entity`` from the `Elixir <http://elixir.ematia.de/trac/wiki>`_
|
|---|
| 120 | library. We use ``using_options()`` to name the table ourselves. Elixir would
|
|---|
| 121 | have used the location of our module to generate a name in the form
|
|---|
| 122 | *package_model_entity*, as described `in Elixir documentation
|
|---|
| 123 | <http://elixir.ematia.de/apidocs/elixir.options.html>`_.
|
|---|
| 124 |
|
|---|
| 125 | Our entity holds four fields.
|
|---|
| 126 |
|
|---|
| 127 | ::
|
|---|
| 128 |
|
|---|
| 129 | title = Field(Unicode(60), required=True)
|
|---|
| 130 |
|
|---|
| 131 | ``title`` holds up to 60 unicode characters, and is required:
|
|---|
| 132 |
|
|---|
| 133 | ::
|
|---|
| 134 |
|
|---|
| 135 | short_description = Field(Unicode(512))
|
|---|
| 136 |
|
|---|
| 137 | ``short_description`` can hold up to 512 characters:
|
|---|
| 138 |
|
|---|
| 139 | ::
|
|---|
| 140 |
|
|---|
| 141 | release_date = Field(Date)
|
|---|
| 142 | genre = Field(Unicode(15))
|
|---|
| 143 |
|
|---|
| 144 | ``release_date`` holds a date, and ``genre`` up to 15 unicode characters:
|
|---|
| 145 |
|
|---|
| 146 | For more information about defining fields, refer to
|
|---|
| 147 | `this page <http://elixir.ematia.de/apidocs/elixir.fields.html>`_. The
|
|---|
| 148 | different `SQLAlchemy <http://www.sqlalchemy.org>`_ types used by Elixir
|
|---|
| 149 | are described `here <http://www.sqlalchemy.org/docs/04/types.html>`_.
|
|---|
| 150 | Finally, Camelot fields are documented in the API.
|
|---|
| 151 |
|
|---|
| 152 | Let's now create an ``EntityAdmin`` subclass
|
|---|
| 153 |
|
|---|
| 154 |
|
|---|
| 155 | The EntityAdmin Subclass
|
|---|
| 156 | ========================
|
|---|
| 157 |
|
|---|
| 158 | We have to tell Camelot about our entities, so they show up in the :abbr:`GUI`.
|
|---|
| 159 | This is one of the purposes of ``EntityAdmin`` subclasses. After adding the
|
|---|
| 160 | ``EntityAdmin`` subclass, our ``Movie`` class now looks like this::
|
|---|
| 161 |
|
|---|
| 162 | class Movie(Entity):
|
|---|
| 163 | using_options(tablename='movie')
|
|---|
| 164 |
|
|---|
| 165 | title = Field(Unicode(60), required=True)
|
|---|
| 166 | short_description = Field(Unicode(512))
|
|---|
| 167 | release_date = Field(Date)
|
|---|
| 168 | genre = Field(Unicode(15))
|
|---|
| 169 |
|
|---|
| 170 | class Admin(EntityAdmin):
|
|---|
| 171 | verbose_name = 'Movie'
|
|---|
| 172 | list_display = ['title', 'short_description', 'release_date', 'genre']
|
|---|
| 173 |
|
|---|
| 174 | def __unicode__(self):
|
|---|
| 175 | return self.title or 'untitled movie'
|
|---|
| 176 |
|
|---|
| 177 | We made ``Admin`` an inner class to strengthen the link between it and the
|
|---|
| 178 | ``Entity`` subclass. Camelot does not force us. ``Admin`` holds three
|
|---|
| 179 | attributes.
|
|---|
| 180 |
|
|---|
| 181 | ``verbose_name`` will be the label used in navigation trees.
|
|---|
| 182 |
|
|---|
| 183 | The last attribute is interesting; it holds a list containing the fields we
|
|---|
| 184 | have defined above. As the name suggests, ``list_display`` tells Camelot to
|
|---|
| 185 | only show the fields specified in the list. ``list_display`` does not affect
|
|---|
| 186 | forms.
|
|---|
| 187 |
|
|---|
| 188 | In our case we want to display four fields: ``title``, ``short_description``,
|
|---|
| 189 | ``release_date``, and ``genre`` (that is, all of them.)
|
|---|
| 190 |
|
|---|
| 191 | We also add a ``__unicode__()`` method that will return either the title of the
|
|---|
| 192 | movie entity or ``'untitled movie'`` if title is empty. This is a good
|
|---|
| 193 | programming practice.
|
|---|
| 194 |
|
|---|
| 195 | Let's move onto the last piece of the puzzle.
|
|---|
| 196 |
|
|---|
| 197 | Configuring the Application
|
|---|
| 198 | ===========================
|
|---|
| 199 |
|
|---|
| 200 | We are now working with :file:`application_admin.py`. One of
|
|---|
| 201 | the tasks of :file:`application_admin.py` is to specify the sections in
|
|---|
| 202 | the left pane of the main window.
|
|---|
| 203 |
|
|---|
| 204 | Camelot defined a class, ``MyApplicationAdmin``, for us. This class is a
|
|---|
| 205 | subclass of ``ApplicationAdmin``, which is used to control the overall look
|
|---|
| 206 | and feel of every Camelot application.
|
|---|
| 207 |
|
|---|
| 208 | To change sections in the left pane of the main window, simply overwrite the
|
|---|
| 209 | ``get_sections`` method, to return a list of the desired sections. By default
|
|---|
| 210 | this method contains::
|
|---|
| 211 |
|
|---|
| 212 | def get_sections(self):
|
|---|
| 213 | from camelot.model.memento import Memento
|
|---|
| 214 | from camelot.model.authentication import Person, Organization
|
|---|
| 215 | from camelot.model.i18n import Translation
|
|---|
| 216 | return [Section('relation',
|
|---|
| 217 | Icon('tango/24x24/apps/system-users.png'),
|
|---|
| 218 | items = [Person, Organization]),
|
|---|
| 219 | Section('configuration',
|
|---|
| 220 | Icon('tango/24x24/categories/preferences-system.png'),
|
|---|
| 221 | items = [Memento, Translation])
|
|---|
| 222 | ]
|
|---|
| 223 |
|
|---|
| 224 | which will display two buttons in the navigation pane, labelled ``'Relations'``
|
|---|
| 225 | and ``'Configurations'``, with the specified icon next to each label. And yes,
|
|---|
| 226 | the order matters.
|
|---|
| 227 |
|
|---|
| 228 | We need to add a new section for our ``Movie`` entity, this is done by
|
|---|
| 229 | extending the list of sections returned by the ``get_sections`` method with a
|
|---|
| 230 | Movie section::
|
|---|
| 231 |
|
|---|
| 232 | Section('movies',
|
|---|
| 233 | Icon('tango/24x24/mimetypes/x-office-presentation.png'),
|
|---|
| 234 | items = [Movie])
|
|---|
| 235 |
|
|---|
| 236 | The constructor of a section object takes the name of the section, the icon to
|
|---|
| 237 | be used and the items in the section. The items is a list of the entities for
|
|---|
| 238 | which a table view should shown.
|
|---|
| 239 |
|
|---|
| 240 | Camelot comes with the `Tango <http://tango.freedesktop.org/Tango_Icon_Library>`_
|
|---|
| 241 | icon collection; we use a suitable icon for our movie section.
|
|---|
| 242 |
|
|---|
| 243 | The resulting method now becomes::
|
|---|
| 244 |
|
|---|
| 245 | def get_sections(self):
|
|---|
| 246 | from camelot.model.memento import Memento
|
|---|
| 247 | from camelot.model.authentication import Person, Organization
|
|---|
| 248 | from camelot.model.i18n import Translation
|
|---|
| 249 | from example.model import Movie
|
|---|
| 250 | return [Section('movies',
|
|---|
| 251 | Icon('tango/24x24/mimetypes/x-office-presentation.png'),
|
|---|
| 252 | items = [Movie]),
|
|---|
| 253 | Section('relation',
|
|---|
| 254 | Icon('tango/24x24/apps/system-users.png'),
|
|---|
| 255 | items = [Person, Organization]),
|
|---|
| 256 | Section('configuration',
|
|---|
| 257 | Icon('tango/24x24/categories/preferences-system.png'),
|
|---|
| 258 | items = [Memento, Translation])
|
|---|
| 259 | ]
|
|---|
| 260 |
|
|---|
| 261 | We can now try our application.
|
|---|
| 262 |
|
|---|
| 263 | We see a new button the navigation pane labelled `'Movies'`. Clicking on it
|
|---|
| 264 | fills the navigation tree with the only entity in the movies's section.
|
|---|
| 265 | Clicking on this tree entry opens the table view. And if we click on the blue
|
|---|
| 266 | folder of each record, a form view appears as shown below.
|
|---|
| 267 |
|
|---|
| 268 | .. image:: ../_static/picture7.png
|
|---|
| 269 |
|
|---|
| 270 | That's it for the basics of defining an entity and setting it for display in
|
|---|
| 271 | Camelot. Next we look at relationships between entities.
|
|---|
| 272 |
|
|---|
| 273 | Relationships
|
|---|
| 274 | =============
|
|---|
| 275 |
|
|---|
| 276 | We will be using Elixir's special fields ``ManyToOne`` and ``OneToMany`` to
|
|---|
| 277 | specify relationships between entities. But first we need a ``Director``
|
|---|
| 278 | entity. We define it as follows::
|
|---|
| 279 |
|
|---|
| 280 | class Director(Entity):
|
|---|
| 281 | using_options(tablename='director')
|
|---|
| 282 |
|
|---|
| 283 | name = Field(Unicode(60))
|
|---|
| 284 | movies = OneToMany('Movie')
|
|---|
| 285 |
|
|---|
| 286 | Once again, we name the table ourselves. What's new here is ``OneToMany``.
|
|---|
| 287 |
|
|---|
| 288 | In Elixir, ``OneToMany`` is a relationship; it takes as parameter the related
|
|---|
| 289 | class's name. Behind the scenes, Elixir creates a director id column in the
|
|---|
| 290 | table represented by the entity ``Movie`` and set a foreign key constraint on
|
|---|
| 291 | this column.
|
|---|
| 292 |
|
|---|
| 293 | Elixir requires that we add an inverse relationship ``ManyToOne`` to our
|
|---|
| 294 | ``Movie`` entity. It ends up looking as follows::
|
|---|
| 295 |
|
|---|
| 296 | class Movie(Entity):
|
|---|
| 297 | using_options(tablename='movie')
|
|---|
| 298 |
|
|---|
| 299 | title = Field(Unicode(60), required=True)
|
|---|
| 300 | short_description = Field(Unicode(512))
|
|---|
| 301 | release_date = Field(Date)
|
|---|
| 302 | genre = Field(Unicode(15))
|
|---|
| 303 | director = ManyToOne('Director')
|
|---|
| 304 |
|
|---|
| 305 | class Admin(EntityAdmin):
|
|---|
| 306 | verbose_name = 'Movie'
|
|---|
| 307 | list_display = ['title',
|
|---|
| 308 | 'short_description',
|
|---|
| 309 | 'release_date',
|
|---|
| 310 | 'genre',
|
|---|
| 311 | 'director']
|
|---|
| 312 |
|
|---|
| 313 | def __unicode__(self):
|
|---|
| 314 | return self.title or 'untitled movie'
|
|---|
| 315 |
|
|---|
| 316 | We also inserted ``'director'`` in ``list_display``.
|
|---|
| 317 |
|
|---|
| 318 | Our ``Director`` entity needs an administration class, which will adds the
|
|---|
| 319 | entity to the section ``'movies'``. We will also add ``__unicode__()`` method
|
|---|
| 320 | as suggested above. The entity now looks as follows::
|
|---|
| 321 |
|
|---|
| 322 | class Director(Entity):
|
|---|
| 323 | using_options(tablename='director')
|
|---|
| 324 |
|
|---|
| 325 | name = Field(Unicode(60))
|
|---|
| 326 | movies = OneToMany('Movie')
|
|---|
| 327 |
|
|---|
| 328 | class Admin(EntityAdmin):
|
|---|
| 329 | verbose_name = 'Director'
|
|---|
| 330 | list_display = ['name']
|
|---|
| 331 |
|
|---|
| 332 | def __unicode__(self):
|
|---|
| 333 | return self.name or 'unknown director'
|
|---|
| 334 |
|
|---|
| 335 | For completeness the two entities are once again listed below::
|
|---|
| 336 |
|
|---|
| 337 | class Movie(Entity):
|
|---|
| 338 | using_options(tablename='movie')
|
|---|
| 339 |
|
|---|
| 340 | title = Field(Unicode(60), required=True)
|
|---|
| 341 | short_description = Field(Unicode(512))
|
|---|
| 342 | release_date = Field(Date)
|
|---|
| 343 | genre = Field(Unicode(15))
|
|---|
| 344 | director = ManyToOne('Director')
|
|---|
| 345 |
|
|---|
| 346 | class Admin(EntityAdmin):
|
|---|
| 347 | verbose_name = 'Movie'
|
|---|
| 348 | list_display = ['title',
|
|---|
| 349 | 'short_description',
|
|---|
| 350 | 'release_date',
|
|---|
| 351 | 'genre',
|
|---|
| 352 | 'director']
|
|---|
| 353 |
|
|---|
| 354 | def __unicode__(self):
|
|---|
| 355 | return self.title or 'untitled movie'
|
|---|
| 356 |
|
|---|
| 357 |
|
|---|
| 358 | class Director(Entity):
|
|---|
| 359 | using_options(tablename='director')
|
|---|
| 360 |
|
|---|
| 361 | name = Field(Unicode(60))
|
|---|
| 362 | movies = OneToMany('Movie')
|
|---|
| 363 |
|
|---|
| 364 | class Admin(EntityAdmin):
|
|---|
| 365 | verbose_name = 'Director'
|
|---|
| 366 | list_display = ['name']
|
|---|
| 367 |
|
|---|
| 368 | def __unicode__(self):
|
|---|
| 369 | return self.name or 'unknown director'
|
|---|
| 370 |
|
|---|
| 371 | The last step is to fix :file:`application_admin.py` by adding the following
|
|---|
| 372 | lines to the Director entity to the Movie section::
|
|---|
| 373 |
|
|---|
| 374 | Section('movies',
|
|---|
| 375 | Icon('tango/24x24/mimetypes/x-office-presentation.png'),
|
|---|
| 376 | items = [Movie, Director])
|
|---|
| 377 |
|
|---|
| 378 | This takes care of the relationship between our two entities. Below is the new
|
|---|
| 379 | look of our video store application.
|
|---|
| 380 |
|
|---|
| 381 | .. image:: ../_static/picture8.png
|
|---|
| 382 |
|
|---|
| 383 | We have just learned the basics of Camelot, and have a nice movie database
|
|---|
| 384 | application we can play with. In another tutorial, we will learn more advanced
|
|---|
| 385 | features of Camelot.
|
|---|