view doc/tutorial.rst @ 68:61667621e19d draft

Work around bug in py_compile.
author David Barts <n5jrn@me.com>
date Mon, 15 Jul 2019 00:15:25 -0700
parents 682cd33e564c
children
line wrap: on
line source

********
Tutorial
********

.. highlight:: none

==============================================
First Step: Create a Directory for Your Webapp
==============================================

TinCan is a lot like a web server; it serves a directory of files. There is
one catch, though: that directory must contain a ``WEB-INF`` subdirectory.
It's OK if ``WEB-INF`` ends up being empty (as it can, for a really simple
webapp). If ``WEB-INF`` is missing, TinCan will conclude the directory
doesn't contain a webapp and will refuse to serve it. (This is a deliberate
feature, added to prevent serving directories that don't contain webapps.)

So the first thing we must do is create a directory to hold our new webapp.
Let's call it ``demo``::
    $ mkdir demo demo/WEB-INF

===================
Adding Some Content
===================
Use your favorite editor to create a file called ``hello.pspx``, and insert
the following content into it::

    <!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <h1>Hello, World</h1>
        <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
      </body>
    </html>

Save the file to disk, and start up a test server::

    $ launch demo

You should see something like the following::

    adding route: /hello.pspx (GET)
    Bottle v0.12.16 server starting up (using WSGIRefServer())...
    Listening on http://localhost:8080/
    Hit Ctrl-C to quit.

If you already have a server listening on port 8080, this naturally won't work.
Use the ``--port`` option to tell ``launch`` to listen on another port, e.g.::

    $ launch --port=8000

When you have a working test server, point your browser at
``http://localhost:8080/hello.pspx``
and you should see something like the following:

.. image:: hello1.png

What just happened?

#. TinCan found the ``hello.pspx`` file.

#. In that file, it found no special header directives. It also found no ``hello.py`` file.

#. So TinCan fell back to its default behavior, and created an instance of the ``tincan.Page`` class (the base class of all code-behind logic), and associated the template code in ``hello.pspx`` with it.

#. The ``${page.request.environ['PATH_INFO']}`` expression references objects found in that standard page object to return the part of the HTTP request path that is being interpreted as a route within the webapp itself.

==============================
Adding Some Content of Our Own
==============================

Use Control-C to force the test server to quit, then add two more files. First,
a template, ``hello2.pspx``::

    <!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <h1>Hello Again, World</h1>
        <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
        <p>The current time on the server is ${time}.</p>
      </body>
    </html>

Next, some code-behind in ``hello2.py``::

    import time
    import tincan

    class Hello2(tincan.Page):
        def handle(self):
            self.time = time.ctime()

This time, when you launch the test server, you should notice a second route
being created::

    $ ./launch demo
    adding route: /hello.pspx (GET)
    adding route: /hello2.pspx (GET)
    Bottle v0.12.16 server starting up (using WSGIRefServer())...
    Listening on http://localhost:8080/
    Hit Ctrl-C to quit.

When you visit the new page, you should see something like this:

.. image:: hello2.png

What new happened this time?

#. You created a code-behind file with the same name as its associated template (only the extensions differ).

#. Because the file names match, TinCan deduced that the two files were related, one containing the code-behind for the associated template.

#. TinCan looked in the code-behind file for a something subclassing ``tincan.Page``, and created an instance of that class, complete with the standard ``request`` and ``response`` instance variables.

#. It then called the ``handle(self)`` method of that class, which defined yet another instance variable.

#. Because that instance variable *did not* start with an underscore, TinCan considered it to be exportable, and exported it as a template variable.

Suppose you had written the following code-behind instead::

    from time import ctime
    from tincan import Page

    class Hello2(Page):
        def handle(self):
            self.time = ctime()

Every class is a subclass of itself, so what stops TinCan from getting all confused now that there are two identifiers in this module, ``Page`` and ``Hello2``, both of which are subclasses of ``tincan.Page``? For that matter, what if you had defined your own subclass of ``tincan.Page`` and subclassed it further, e.g.::

    from time import ctime
    from mymodule import MyPage

    class Hello2(MyPage):
        def handle(self):
            self.time = ctime()

The answer is that the code would still have worked (you might want to try the first example above just to prove it). When deciding what class to use, TinCan looks for *the deepest subclass of tincan.Page it can find* in the code-behind. So in a code-behind file that contains both a reference to ``tincan.Page`` itself, and a subclass of ``tincan.Page``, the subclass will always "win." Likewise, a subclass of a subclass will always "win" over a direct subclass of the parent class.

A code-behind file need not share the same file name as its associated
template. If you use the ``#python`` header directive, you can tell TinCan
exactly which code-behind file to use. For example::

    #python other.py
    <!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <h1>Hello Again, World</h1>
        <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
        <p>The current time on the server is ${time}.</p>
      </body>
    </html>

What happens if you edit ``hello2.pspx`` to look like the above but *do not* rename ``hello2.py`` to ``other.py``? What happens if you make ``hello2.pspx`` run with ``hello.py`` (and vice versa)? Try it and see!

\(Note that it is generally not a good idea to gratuitously name things inconsistently, as it makes it hard for other people — or even you, at a later time — to easily figure out what is going on.\)

===========
Error Pages
===========

If you tried the exercises in the paragraph immediately above, some of them let you see what happens when something goes wrong in TinCan. What if that standard error page is not to your liking, and you want to display something customized? TinCan lets you do that. Create a file called ``500.pspx``::

    #errors 500
    <!DOCTYPE html>
    <html>
      <head>
        <title>${error.status_line}</title>
      </head>
      <body>
        <h1>${error.status_line}</h1>
        <p>How embarrassing! It seems there was an internal error serving
        <kbd>${request.url}</kbd></p>
      </body>
    </html>

Now when you visit a page that doesn't work due to server-side errors (such as a template referencing an undefined variable), you'll see something like:

.. image:: 500.png

Error pages work a little differently from normal pages. They are based on subclasses of ``tincan.ErrorPage``. You get two standard template variables "for free:" ``error``, containing an instance of a ``bottle.HTTPError`` object pertaining to the error, and ``request``, a ``bottle.HTTPRequest`` object pertaining to the request that triggered the error.

It is not as common for error pages to have code-behind as it is for normal, non-error pages, but it is possible and allowed. As alluded to above, the class to subclass is ``tincan.ErrorPage``; such classes have a ``handle()`` method which may define instance variables which get exported to the associated template using the same basic rules as for normal pages.

If an error page itself causes an error, TinCan (the underlying Bottle framework, actually) will ignore it and  display a fallback error page. This is to avoid triggering an infinite loop.

====================================
Index Pages and Server-Side Forwards
====================================

Remove any ``#python`` header directives you added to ``hello.pspx`` and ``hello2.pspx`` during your previous experiments and create ``index.pspx``::

    #forward hello.pspx

That's it, just one line! When you launch the modified webapp, you should notice a couple new routes being created::

    $ launch demo
    adding route: /hello.pspx (GET)
    adding route: /hello2.pspx (GET)
    adding route: /index.pspx (GET)
    adding route: / (GET)
    Bottle v0.12.16 server starting up (using WSGIRefServer())...
    Listening on http://localhost:8080/
    Hit Ctrl-C to quit.

And if you navigate to ``http://localhost:8080/``, you should see our old friend the first page we created in this tutorial.

Just like how a web server recognizes ``index.html`` as special, and routes requests for its containing directory to it, TinCan recognizes ``index.pspx`` as special.

``#forward`` creates what is known as a *server-side forward*. Unlike with an HTTP forward, all the action happens on the server side. The client never sees any intermediate 3xx response and never has to make a second HTTP request to resolve the URL. Any TinCan template file containing a ``#forward`` header directive will have *everything else in it* ignored, and otherwise act as if it were an exact copy of the route referenced in the ``#forward`` header.

It is permitted for a page to ``#forward`` to another ``#forward`` page (but you probably should think twice before doing this). ``#forward`` loops are not permitted and attempts to create them will be rejected and cause an error message at launch time.

=====================================
Form Data and Requests Other Than GET
=====================================

As a final example, here's a sample page, ``name.pspx``, that processes form data via a POST request, and which uses some `Chameleon TAL <https://chameleon.readthedocs.io/en/latest/reference.html#basics-tal>`_ to render and style the response page, call it::

    #rem A page that accepts both GET and POST requests
    #methods GET POST
    <!DOCTYPE html>
    <html>
      <head>
        <title>Form Test</title>
      </head>
      <body>
        <h1>Form Test</h1>
        <h2>Enter Your Name Below</h2>
        <form method="post">
          <input type="text" name="name"/>
          <input type="submit" value="Submit"/>
        </form>
        <if tal:condition="message is not None" tal:omit-tag="True">
          <h2 tal:content="message.subject"></h2>
          <p tal:attributes="style message.style" tal:content="message.body"></p>
        </if>
      </body>
    </html>

Here's the code-behind for that page::

    from tincan import Page
    from jsdict import JSDict

    class Name(Page):
        def handle(self):
            self._error_message = JSDict({"subject": "Error", "style": "color: red;"})
            if "name" not in self.request.forms:
                if self.request.method == "GET":
                    self.message = None
                else:
                    self.message = self.error("This should not happen!")
            else:
                name = self.request.forms["name"]
                if name.strip() == "":
                    self.message = self.error("Please enter your name above.")
                else:
                    self.message = JSDict({
                        "subject": "Hello, {0}".format(name.split()[0]),
                        "style": None,
                        "body": "Pleased to meet you!"
                    })

        def error(self, message):
            self._error_message.body = message
            return self._error_message

This example requires you to create a third file, in ``WEB-INF/lib/jsdict.py``::

    class JSDict(dict):
        def __getattr__(self, name):
            return self[name]

        def __setattr__(self, name, value):
            self[name] = value

        def __delattr__(self, name):
            del self[name]

``WEB-INF/lib`` is the directory where webapp-specific library routines live. It gets added to ``sys.path`` automatically when your webapp is launched.

This example introduces two new header directives, ``#rem`` and ``#methods``. The former introduces a remark (comment); the latter specifies the request methods that this page will respond to. Not specifying a ``#methods`` line is equivalent to specifying ``#methods GET``. When you launch the webapp after adding this new page, you shound notice TinCan announce that it is creating a route that supports both GET and POST requests for it::

    $ launch demo
    adding route: /hello.pspx (GET)
    adding route: /hello2.pspx (GET)
    adding route: /index.pspx (GET)
    adding route: / (GET)
    adding route: /name.pspx (GET,POST)
    Bottle v0.12.16 server starting up (using WSGIRefServer())...
    Listening on http://localhost:8080/
    Hit Ctrl-C to quit.