{{ entry.title }}
{{ entry.body }}
Created {{entry.created}}
.. slideconf::
:autoslides: True
**********
Session 06
**********
.. image:: /_static/lj_entry.png
:width: 65%
:align: center
Interacting with Data
=====================
**Wherein we learn to display our data, and to create and edit it too!**
But First
---------
Last week we discussed the **model** part of the *MVC* application design
pattern.
.. rst-class:: build
.. container::
We set up a project using the `Pyramid`_ web framework and the `SQLAlchemy`_
library for persisting our data to a database.
We looked at how to define a simple model by investigating the demo model
created on our behalf.
And we went over, briefly, the way we can interact with this model at the
command line to make sure we've got it right.
Finally, we defined what attributes a learning journal entry would have,
and a pair of methods we think we will need to make the model complete.
.. _Pyramid: http://www.pylonsproject.org/projects/pyramid/about
.. _SQLAlchemy: http://docs.sqlalchemy.org/en/rel_0_9/
Our Data Model
--------------
Over the last week, your assignment was to create the new model.
.. rst-class:: build
.. container::
Did you get that done?
If not, what stopped you?
Let's take a few minutes here to answer questions about this task so you
are more comfortable.
Questions?
.. nextslide:: A Complete Example
I've added a working ``models.py`` file to our `class repository`_ in the
``resources/session06/`` folder.
Let's review how it works.
.. _class repository: https://github.com/UWPCE-PythonCert/training.python_web/tree/master/resources/session06
.. nextslide:: Demo Interaction
I've also made a few small changes to make the ``pshell`` command a bit more
helpful.
.. rst-class:: build
.. container::
In ``learning_journal/__init__.py`` I added the following function:
.. code-block:: python
def create_session(settings):
from sqlalchemy.orm import sessionmaker
engine = engine_from_config(settings, 'sqlalchemy.')
Session = sessionmaker(bind=engine)
return Session()
Then, in ``development.ini`` I added the following configuration:
.. code-block:: ini
[pshell]
create_session = learning_journal.create_session
entry = learning_journal.models.Entry
.. nextslide:: Using the new ``pshell``
Here's a demo interaction using ``pshell`` with these new features:
.. rst-class:: build
.. container::
First ``cd`` to your project code, fire up your project virtualenv and
start python:
.. code-block:: bash
$ cd projects/learning-journal/learning_journal
$ source ../ljenv/bin/activate
(ljenv)$ pshell development.ini
Python 3.5.0 (default, Sep 16 2015, 10:42:55)
...
Environment:
app The WSGI application.
...
Custom Variables:
create_session learning_journal.create_session
entry learning_journal.models.Entry
In [1]: session = create_session(registry.settings)
[demo]
The MVC Controller
==================
.. rst-class:: left
.. container::
Let's go back to thinking for a bit about the *Model-View-Controller*
pattern.
.. figure:: http://upload.wikimedia.org/wikipedia/commons/4/40/MVC_passive_view.png
:align: center
:width: 25%
By Alan Evangelista (Own work) [CC0], via Wikimedia Commons
.. rst-class:: build
.. container::
We talked last week (and today) about the *model*
Today, we'll dig into *controllers* and *views*
or as we will know them in Pyramid: *views* and *renderers*
HTTP Request/Response
---------------------
Internet software is driven by the HTTP Request/Response cycle.
.. rst-class:: build
.. container::
A *client* (perhaps a user with a web browser) makes a **request**
A *server* receives and handles that request and returns a **response**
The *client* receives the response and views it, perhaps making a new
**request**
And around and around it goes.
.. nextslide:: URLs
An HTTP request arrives at a server through the magic of a **URL**
.. code-block:: bash
http://uwpce-pythoncert.github.io/training.python_web/html/index.html
.. rst-class:: build
.. container::
Let's break that up into its constituent parts:
.. rst-class:: build
\http://:
This part is the *protocol*, it determines how the request will be sent
uwpce-pythoncert.github.io:
This is a *domain name*. It's the human-facing address for a server
somewhere.
/training.python_web/html/index.html:
This part is the *path*. It serves as a locator for a resource *on the
server*
.. nextslide:: Paths
In a static website (like our documentation) the *path* identifies a **physical
location** in the server's filesystem.
.. rst-class:: build
.. container::
Some directory on the server is the *home* for the web process, and the
*path* is looked up there.
Whatever resource (a file, an image, whatever) is located there is returned
to the user as a response.
If the path leads to a location that doesn't exist, the server responds
with a **404 Not Found** error.
In the golden days of yore, this was the only way content was served via
HTTP.
.. nextslide:: Paths in an MVC System
In todays world we have dynamic systems, server-side web frameworks like
Pyramid.
.. rst-class:: build
.. container::
The requests that you send to a server are handled by a software process
that assembles a response instead of looking up a physical location.
But we still have URLs, with *protocol*, *domain* and *path*.
What is the role for a path in a process that doesn't refer to a physical
file system?
Most web frameworks now call the *path* a **route**.
They provide a way of matching *routes* to the code that will be run to
handle requests.
Routes in Pyramid
-----------------
In Pyramid, routes are handled as *configuration* and are set up in the *main*
function in ``__init__.py``:
.. code-block:: python
# learning_journal/__init__.py
def main(global_config, **settings):
# ...
config.add_route('home', '/')
# ...
.. rst-class:: build
.. container::
Our code template created a sample route for us, using the ``add_route``
method of the ``Configurator`` class.
The ``add_route`` method has two required arguments: a *name* and a
*pattern*
In our sample route, the *name* is ``'home'``
In our sample route, the *pattern* is ``'/'``
.. nextslide::
When a request comes in to a Pyramid application, the framework looks at all
the *routes* that have been configured.
.. rst-class:: build
.. container::
One by one, in order, it tries to match the *path* of the incoming request
against the *pattern* of the route.
As soon as a *pattern* matches the *path* from the incoming request, that
route is used and no further matching is performed.
If no route is found that matches, then the request will automatically get
a **404 Not Found** error response.
In our sample app, we have one sample *route* named ``'home'``, with a
pattern of ``/``.
This means that any request that comes in for ``/`` will be matched to this
route, and any other request will be **404**.
.. nextslide:: Routes as API
In a very real sense, the *routes* defined in an application *are* the public
API.
.. rst-class:: build
.. container::
Any route that is present represents something the user can do.
Any route that is not present is something the user cannot do.
You can use the proper definition of routes to help conceptualize what your
app will do.
What routes might we want for a learning journal application?
What will our application do?
.. nextslide:: Defining our Routes
Let's add routes for our application.
.. rst-class:: build
.. container::
Open ``learning_journal/__init__.py``.
For our list page, the existing ``'home'`` route will do fine, leave it.
Add the following two routes:
.. code-block:: python
config.add_route('home', '/') # already there
config.add_route('detail', '/journal/{id:\d+}')
config.add_route('action', '/journal/{action}')
The ``'detail'`` route will serve a single journal entry, identified by an
``id``.
The ``action`` route will serve ``create`` and ``edit`` views, depending on
the ``action`` specified.
In both cases, we want to capture a portion of the matched path to use
information it provides.
.. nextslide:: Matching an ID
In a pattern, you can capture a ``path segment`` *replacement
marker*, a valid Python symbol surrounded by curly braces:
.. rst-class:: build
.. container::
::
/home/{foo}/
If you want to match a particular pattern, like digits only, add a
*regular expression*::
/journal/{id:\d+}
Matched path segments are captured in a ``matchdict``::
# pattern # actual url # matchdict
/journal/{id:\d+} /journal/27 {'id': '27'}
The ``matchdict`` is made available as an attribute of the *request object*
(more on that soon)
.. nextslide:: Connecting Routes to Views
In Pyramid, a *route* is connected by configuration to a *view*.
.. rst-class:: build
.. container::
In our app, a sample view has been created for us, in ``views.py``:
.. code-block:: python
@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
# ...
The order in which *routes* are configured *is important*, so that must be
done in ``__init__.py``.
The order in which views are connected to routes *is not important*, so the
*declarative* ``@view_config`` decorator can be used.
When ``config.scan`` is called, all files in our application are searched
for such *declarative configuration* and it is added.
The Pyramid View
----------------
Let's imagine that a *request* has come to our application for the path
``'/'``.
.. rst-class:: build
.. container::
The framework made a match of that path to a *route* with the pattern ``'/'``.
Configuration connected that route to a *view* in our application.
Now, the view that was connected will be *called*, which brings us to the
nature of *views*
.. rst-class:: centered
--A Pyramid view is a *callable* that takes *request* as an argument--
Remember what a *callable* is?
.. nextslide:: What the View Does
So, a *view* is a callable that takes the *request* as an argument.
.. rst-class:: build
.. container::
It can then use information from that request to build appropriate data,
perhaps using the application's *models*.
Then, it returns the data it assembled, passing it on to a `renderer`_.
Which *renderer* to use is determined, again, by configuration:
.. code-block:: python
@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
# ...
More about this in a moment.
The *view* stands at the intersection of *input data*, the application
*model* and *renderers* that offer rendering of the results.
It is the *Controller* in our MVC application.
.. _renderer: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html
.. nextslide:: Adding Stub Views
Add temporary views to our application in ``views.py`` (and comment out the
sample view):
.. code-block:: python
@view_config(route_name='home', renderer='string')
def index_page(request):
return 'list page'
@view_config(route_name='detail', renderer='string')
def view(request):
return 'detail page'
@view_config(route_name='action', match_param='action=create', renderer='string')
def create(request):
return 'create page'
@view_config(route_name='action', match_param='action=edit', renderer='string')
def update(request):
return 'edit page'
.. nextslide:: Testing Our Views
Now we can verify that our view configuration has worked.
.. rst-class:: build
.. container::
Make sure your virtualenv is properly activated, and start the web server:
.. code-block:: bash
(ljenv)$ pserve development.ini
Starting server in PID 84467.
serving on http://0.0.0.0:6543
Then try viewing some of the expected application urls:
.. rst-class:: build
* http://localhost:6543/
* http://localhost:6543/journal/1
* http://localhost:6543/journal/create
* http://localhost:6543/journal/edit
What happens if you visit a URL that *isn't* in our configuration?
.. nextslide:: Interacting With the Model
Now that we've got temporary views that work, we can fix them to get
information from our database
.. rst-class:: build
.. container::
We'll begin with the list view.
We need some code that will fetch all the journal entries we've written, in
reverse order, and hand that collection back for rendering.
.. code-block:: python
from .models import (
DBSession,
MyModel,
Entry, # <- Add this import
)
# and update this view function
def index_page(request):
entries = Entry.all()
return {'entries': entries}
.. nextslide:: Using the ``matchdict``
Next, we want to write the view for a single entry.
.. rst-class:: build
.. container::
We'll need to use the ``id`` value our route captures into the
``matchdict``.
Remember that the ``matchdict`` is an attribute of the request.
We'll get the ``id`` from there, and use it to get the correct entry.
.. code-block:: python
# add this import at the top
from pyramid.httpexceptions import HTTPNotFound
# and update this view function:
def view(request):
this_id = request.matchdict.get('id', -1)
entry = Entry.by_id(this_id)
if not entry:
return HTTPNotFound()
return {'entry': entry}
.. nextslide:: Testing Our Views
We can now verify that these views work correctly.
.. rst-class:: build
.. container::
Make sure your virtualenv is properly activated, and start the web server:
.. code-block:: bash
(ljenv)$ pserve development.ini
Starting server in PID 84467.
serving on http://0.0.0.0:6543
Then try viewing the list page and an entry page:
* http://localhost:6543
* http://localhost:6543/journal/1
What happens when you request an entry with an id that isn't in the
database?
* http://localhost:6543/journal/100
The MVC View
============
.. rst-class:: left
.. container::
Again, back to the *Model-View-Controller* pattern.
.. figure:: http://upload.wikimedia.org/wikipedia/commons/4/40/MVC_passive_view.png
:align: center
:width: 25%
By Alan Evangelista (Own work) [CC0], via Wikimedia Commons
.. rst-class:: build
.. container::
We've built a *model* and we've created some *controllers* that use it.
In Pyramid, we call *controllers* **views** and they are callables that
take *request* as an argument.
Let's turn to the last piece of the *MVC* patter, the *view*
Presenting Data
---------------
The job of the *view* in the *MVC* pattern is to present data in a format that
is readable to the user of the system.
.. rst-class:: build
.. container::
There are many ways to present data.
Some are readable by humans (tables, charts, graphs, HTML pages, text
files).
Some are more for machines (xml files, csv, json).
Which of these formats is the *right one* depends on your purpose.
What is the purpose of our learning journal?
Pyramid Renderers
-----------------
In Pyramid, the job of presenting data is performed by a *renderer*.
.. rst-class:: build
.. container::
So we can consider the Pyramid **renderer** to be the *view* in our *MVC*
app.
We've already seen how we can connect a *renderer* to a Pyramid *view* with
configuration.
In fact, we have already done so, using a built-in renderer called
``'string'``.
This renderer converts the return value of its *view* to a string and sends
that back to the client as an HTTP response.
But the result isn't so nice looking.
.. nextslide:: Template Renderers
The `built-in renderers` (``'string'``, ``'json'``, ``'jsonp'``) in Pyramid are
not the only ones available.
.. _built-in renderers: http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/renderers.html#built-in-renderers
.. rst-class:: build
.. container::
There are add-ons to Pyramid that support using various *template
languages* as renderers.
In fact, one of these was installed by default when you created this
project.
.. nextslide:: Configuring a Template Renderer
.. code-block:: python
# in setup.py
requires = [
# ...
'pyramid_chameleon',
# ...
]
# in learning_journal/__init__.py
def main(global_config, **settings):
# ...
config.include('pyramid_chameleon')
.. rst-class:: build
.. container::
The `pyramid_chameleon` package supports using the `chameleon` template
language.
The language is quite nice and powerful, but not so easy to learn.
Let's use a different one, *jinja2*
.. nextslide:: Changing Template Renderers
Change ``pyramid_chameleon`` to ``pyramid_jinja2`` in both of these files:
.. code-block:: python
# in setup.py
requires = [
# ...
'pyramid_jinja2',
# ...
]
# in learning_journal/__init__.py
def main(global_config, **settings):
# ...
config.include('pyramid_jinja2')
.. nextslide:: Picking up the Changes
We've changed the dependencies for our Pyramid project.
.. rst-class:: build
.. container::
As a result, we will need to re-install it so the new dependencies are also
installed:
.. code-block:: bash
(ljenv)$ python setup.py develop
...
Finished processing dependencies for learning-journal==0.0
(ljenv)$
Now, we can use *Jinja2* templates in our project.
Let's learn a bit about how `Jinja2 templates`_ work.
.. _Jinja2 templates: http://jinja.pocoo.org/docs/templates/
Jinja2 Template Basics
----------------------
We'll start with the absolute basics.
.. rst-class:: build
.. container::
Fire up an iPython interpreter, using your `ljenv` virtualenv:
.. code-block:: bash
(ljenv)$ ipython
...
In [1]:
Then import the ``Template`` class from the ``jinja2`` package:
.. code-block:: ipython
In [1]: from jinja2 import Template
.. nextslide:: Templates are Strings
A template is constructed with a simple string:
.. code-block:: ipython
In [2]: t1 = Template("Hello {{ name }}, how are you")
.. rst-class:: build
.. container::
Here, we've simply typed the string directly, but it is more common to
build a template from the contents of a *file*.
Notice that our string has some odd stuff in it: ``{{ name }}``.
This is called a *placeholder* and when the template is *rendered* it is
replaced.
.. nextslide:: Rendering a Template
Call the ``render`` method, providing *context*:
.. code-block:: ipython
In [3]: t1.render(name="Freddy")
Out[3]: 'Hello Freddy, how are you'
In [4]: t1.render(name="Gloria")
Out[4]: 'Hello Gloria, how are you'
.. rst-class:: build
.. container::
*Context* can either be keyword arguments, or a dictionary
Note the resemblance to something you've seen before:
.. code-block:: python
>>> "This is {owner}'s string".format(owner="Cris")
'This is Cris's string'
.. nextslide:: Dictionaries in Context
Dictionaries passed in as part of the *context* can be addressed with *either*
subscript or dotted notation:
.. code-block:: ipython
In [5]: person = {'first_name': 'Frank',
...: 'last_name': 'Herbert'}
In [6]: t2 = Template("{{ person.last_name }}, {{ person['first_name'] }}")
In [7]: t2.render(person=person)
Out[7]: 'Herbert, Frank'
.. rst-class:: build
* Jinja2 will try the *correct* way first (attr for dotted, item for
subscript).
* If nothing is found, it will try the opposite.
* If nothing is found, it will return an *undefined* object.
.. nextslide:: Objects in Context
The exact same is true of objects passed in as part of *context*:
.. rst-class:: build
.. container::
.. code-block:: ipython
In [8]: t3 = Template("{{ obj.x }} + {{ obj['y'] }} = Fun!")
In [9]: class Game(object):
...: x = 'babies'
...: y = 'bubbles'
...:
In [10]: bathtime = Game()
In [11]: t3.render(obj=bathtime)
Out[11]: 'babies + bubbles = Fun!'
This means your templates can be agnostic as to the nature of the
things found in *context*
.. nextslide:: Filtering values in Templates
You can apply `filters`_ to the data passed in *context* with the pipe ('|')
operator:
.. _filters: http://jinja.pocoo.org/docs/dev/templates/#filters
.. code-block:: ipython
In [12]: t4 = Template("shouted: {{ phrase|upper }}")
In [13]: t4.render(phrase="this is very important")
Out[13]: 'shouted: THIS IS VERY IMPORTANT'
.. rst-class:: build
.. container::
You can also chain filters together:
.. code-block:: ipython
In [14]: t5 = Template("confusing: {{ phrase|upper|reverse }}")
In [15]: t5.render(phrase="howdy doody")
Out[15]: 'confusing: YDOOD YDWOH'
.. nextslide:: Control Flow
Logical `control structures`_ are also available:
.. _control structures: http://jinja.pocoo.org/docs/dev/templates/#list-of-control-structures
.. rst-class:: build
.. container::
.. code-block:: ipython
In [16]: tmpl = """
....: {% for item in list %}{{ item}}, {% endfor %}
....: """
In [17]: t6 = Template(tmpl)
In [18]: t6.render(list=['a', 'b', 'c', 'd', 'e'])
Out[18]: '\na, b, c, d, e, '
Any control structure introduced in a template **must** be paired with an
explicit closing tag (``{% for %}...{% endfor %}``)
Remember, although template tags like ``{% for %}`` or ``{% if %}`` look a
lot like Python, *they are not*.
The syntax is specific and must be followed correctly.
.. nextslide:: Template Tests
There are a number of specialized *tests* available for use with the
``if...elif...else`` control structure:
.. code-block:: ipython
In [19]: tmpl = """
....: {% if phrase is upper %}
....: {{ phrase|lower }}
....: {% elif phrase is lower %}
....: {{ phrase|upper }}
....: {% else %}{{ phrase }}{% endif %}"""
In [20]: t7 = Template(tmpl)
In [21]: t7.render(phrase="FOO")
Out[21]: '\n\n foo\n'
In [22]: t7.render(phrase='bar')
Out[22]: '\n\n BAR\n'
In [23]: t7.render(phrase='This should print as-is')
Out[23]: '\nThis should print as-is'
.. nextslide:: Basic Expressions
Basic `Python-like expressions`_ are also supported:
.. _Python-like expressions: http://jinja.pocoo.org/docs/dev/templates/#expressions
.. code-block:: ipython
In [24]: tmpl = """
....: {% set sum = 0 %}
....: {% for val in values %}
....: {{ val }}: {{ sum + val }}
....: {% set sum = sum + val %}
....: {% endfor %}
....: """
In [25]: t8 = Template(tmpl)
In [26]: t8.render(values=range(1, 11))
Out[26]: '\n\n\n1: 1\n \n\n2: 3\n \n\n3: 6\n \n\n4: 10\n
\n\n5: 15\n \n\n6: 21\n \n\n7: 28\n \n\n8: 36\n
\n\n9: 45\n \n\n10: 55\n \n\n'
Our Templates
-------------
There's more that Jinja2 templates can do, but it will be easier to introduce
you to that in the context of a working template. So let's make some.
.. nextslide:: Detail Template
We have a Pyramid view that returns a single entry. Let's create a template to
show it.
.. rst-class:: build
.. container::
In ``learning_journal/templates`` create a new file ``detail.jinja2``:
.. code-block:: jinja
{{ entry.body }} Created {{entry.created}}{{ entry.title }}
This journal is empty
{% endif %} .. nextslide:: It's worth taking a look at a few specifics of this template. .. rst-class:: build .. container:: .. code-block:: jinja {% for entry in entries %} ... {% endfor %} Jinja2 templates are rendered with a *context*. A Pyramid *view* returns a dictionary, which is passed to the renderer as part of of that *context* This means we can access values we return from our *view* in the *renderer* using the names we assigned to them. .. nextslide:: It's worth taking a look at a few specifics of this template. .. code-block:: jinja {{ entry.title }} The *request* object is also placed in the context by Pyramid. Request has a method ``route_url`` that will create a URL for a named route. This allows you to include URLs in your template without needing to know exactly what they will be. This process is called *reversing*, since it's a bit like a reverse phone book lookup. .. nextslide:: Finally, you'll need to connect this new renderer to your listing view: .. code-block:: python @view_config(route_name='home', renderer='templates/list.jinja2') def index_page(request): # ... .. nextslide:: Try It Out We can now see our list page too. Let's try starting the server: .. rst-class:: build .. container:: .. code-block:: bash (ljenv)$ pserve development.ini Starting server in PID 90536. serving on http://0.0.0.0:6543 Then try viewing the home page of your journal: * http://localhost:6543/ Click on the link to an entry, it should work. .. nextslide:: Sharing Structure These views are reasonable, if quite plain. .. rst-class:: build .. container:: It'd be nice to put them into something that looks a bit more like a website. Jinja2 allows you to combine templates using something called `template inheritance`_. You can create a basic page structure, and then *inherit* that structure in other templates. In our class resources I've added a page template ``layout.jinja2``. Copy that page to your templates directory .. _template inheritance: http://jinja.pocoo.org/docs/dev/templates/#template-inheritance .. nextslide:: ``layout.jinja2`` .. code-block:: jinja