.. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .. % Copyright 2001 by Object Craft P/L, Melbourne, Australia. .. % LICENCE - see LICENCE file distributed with this software for details. .. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% .. _app-guide: ****************************** Guide to Building Applications ****************************** Roughly speaking, every page or document sent from a web server to a browser is the result of the same processing sequence. For our purposes a document is one page in an application. #. The browser connects to the server and requests a page. #. The server decodes the browser request and processes it. This can cause all manner of subsidiary application processing to occur. #. The server sends the result of processing back to the browser as the next page in the application. The essentially stateless nature of the web presents problems for the application developer who wishes to retain state between different pages in their application. From the point of view of the user, the state of the application at the server is represented by the page that they see in their browser window(s). When they enter values and press submit buttons on their page they (quite reasonably) expect the application on the web server to process their request and return the next page in the application. The point of view of the web application developer is very different. At the server side the application must be able to receive a request from one browser, process it and send the next application page back to that browser. Before seeing the next request from the same browser, another browser may request a different page from the application. The web application even has to deal with multiple browsers simultaneously accessing different pages in the application. All of this while maintaining the illusion for the end user that they are running their own copy of the application. Even when you only have a single machine running Apache, there are multiple Apache processes on that machine that are receiving and processing browser requests. This means that there is no guarantee that the same process that received the last request from a particular browser will receive the next request from that browser. If your application requires some state to be retained between pages, there has to be a way for that state to migrate between different Apache processes. If you are using more than one machine to process requests, then you need some way for the application state to move between machines. There are essentially three approaches to solving the state propagation problem. #. Deploy a stateless application. Only the most trivial applications do not require state to be retained across browser requests. #. Get the browser to store the application state. This can be done by embedding information in URL's and hidden fields in forms. When the browser requests the next page the application restores state by extracting it from the hidden field values sent back by the browser. Browser cookies can also be used to store some types of application state. #. Get the browser to store a session identifier in either URL's, hidden fields, or a cookie. When the browser requests the next page the application uses the browser supplied session identifier to locate the state in a server-side session database of some description. Assuming that your application is non-trivial, the second approach (state held by the browser) is the easiest to implement. If you are building a financial application, you will probably feel more comfortable with the third approach. This has the advantage of hiding implementation details of your application from prying eyes. All three approaches are supported (in one way or another) by Albatross. In all Albatross applications there are two important objects that determine how browser requests are processed; the application object, and the execution context object. The application is a potentially long lived object that provides generic services for multiple browser requests. A new execution context is created to process each browser request. One of the things that the application and execution context do is cooperate to provide session functionality. The execution context is the object where session data is created and manipulated, but the application is usually responsible to loading and saving the session. This allows the application to maintain a long lived connection with a server if necessary. .. _app-model: Albatross Application Model =========================== In the Albatross world view explained in the previous section, all web applications follow the same processing sequence to handle browser requests. When processing a browser request to generate a response the processing (in most cases) flows according to figure :ref:`fig-dataflow`. .. _fig-dataflow: .. figure:: .build/figures/dataflow.* :align: center Request Processing Dataflow The processing steps are: #. Capture the browser request in a :class:`Request` object. #. Pass the :class:`Request` object to the :meth:`run` method of the application object. #. Application locates the Python code for processing the browser request. #. Page processing code runs one or more Albatross templates. #. Templates contain either standard Albatross tags or application defined extension tags. #. As tags are converted to HTML a stream of content fragments is sent to the execution context. #. When the execution context content is flushed all of the fragments are joined together. #. Joined flushed content is sent to the :class:`Request` object :meth:`write_content` method. #. Application response is returned to the browser. In the Albatross code the processing is driven by the :meth:`run` method of the :class:`Application` class in the :mod:`app` module. It is instructive to look at the *exact* code from Albatross that implements the processing sequence. .. code-block:: python ctx = self.create_context() ctx.set_request(req) self.load_session(ctx) self.load_page(ctx) if self.validate_request(ctx): self.merge_request(ctx) self.process_request(ctx) self.display_response(ctx) self.save_session(ctx) ctx.flush_content() The code is contained within a ``try``/``except`` block to allow the application to trap and handle exceptions. The :class:`Application` class assumes very little about the implementation of each of these steps. The detail of the processing stages is defined by a collection of mixin classes. A combination of the :class:`Application` class and a selection of the mixin classes is used to construct your application class and execution context classes. There are a number of prefabricated applications and execution contexts, see chapter :ref:`pack-overview`. This mix and match approach to building the application and execution context classes provides a great deal of flexibility. Albatross is an application toolkit, not a deployment platform. You are encouraged to look at the code and develop your own mixin classes to suit your deployment requirements. One of the primary goals of Albatross is to keep the toolkit line count low. This reduces the amount of time you need to spend before you can make your own custom extensions to the toolkit. In the previous chapter we talked about the importance of separating the presentation layer of the application from the implementation as shown in figure :ref:`fig-presimp`. Albatross HTML templates provide a fairly powerful tool for achieving that separation. .. _fig-presimp: .. figure:: .build/figures/twolayer.* :align: center Separation of Presentation/Implementation The presentation layer consists of a collection of template files that contain the logic required to display data from the objects contained in the implementation layer. In Albatross, the line between the two layers is the execution context. To make objects available to the presentation layer the application places references to those objects into the local or global namespace of the execution context. The local namespace is populated in application code like this: .. code-block:: python ctx.locals.mbox = Mbox(ctx.locals.username, ctx.locals.passwd) ctx.locals.msg = ctx.locals.mbox[int(ctx.locals.msgnum) - 1] To execute Python expressions inside the template files, the execution context uses the following Python code from the :class:`NamespaceMixin` class: .. code-block:: python def eval_expr(self, expr): self.locals.__ctx__ = self try: return eval(expr, self.__globals, self.locals.__dict__) finally: del self.locals.__ctx__ Whenever application code calls the :meth:`run_template` or :meth:`run_template_once` methods of the :class:`NamespaceMixin` class Albatross sets the global namespace (via :meth:`set_globals`) for expression evaluation (in :attr:`self.__globals`) to the globals of the function that called :meth:`run_template` or :meth:`run_template_once`. Not many applications are output only, most accept browser input. The Albatross application object merges the browser request into the execution context in the :meth:`merge_request` method. Referring back to the application processing sequence also note that the application object displays the result of processing the browser request via the execution context. With this in mind figure :ref:`fig-presimp` becomes figure :ref:`fig-presimpexec`. .. _fig-presimpexec: .. figure:: .build/figures/twolayerctx.* :align: center Presentation/Implementation and Execution Context The only thing missing is the application glue that processes the browser requests, places application objects into the execution context, and directs the execution of template files. The application model built into Albatross is intended to facilitate the use of a model-view-controller like approach (see figure :ref:`fig-mvc`) to constructing your application. There are many excellent descriptions of the model-view-controller design pattern which can be found by searching for "model view controller" on ``_. .. _fig-mvc: .. figure:: .build/figures/mvc.* :align: center Albatross model-view-controller The *user* invokes application functions via the *controller* through the *view*. The *controller* contains logic to direct the application functionality contained within the *model*. All of the real application functionality is in the *model*, not the *controller*. Changes to the application *model* are then propagated to the *view* via the *controller*. In Albatross terms, the implementation layer is the *model* and the presentation layer is the *view*. The application glue plays the role of the *controller*. By divorcing all application logic from the *view* and *controller* you are able to construct unit test suites for your application functionality using the Python :mod:`unittest` module. Albatross uses an approach inspired by the traditional model-view-controller design pattern. So now we can draw the final version of the diagram which shows how Albatross applications process browser requests in figure :ref:`fig-appmodel`. .. _fig-appmodel: .. figure:: .build/figures/albmvc.* :align: center Albatross Application Model As you can see the execution context is central to all of the Albatross processing. It is worth revisiting the application processing sequence set out in :ref:`app-model` at the start of this section to see how the application draws all of the elements together. During step four (page processing) the Albatross application object will call on your application code to process the browser request. There are a number of different ways in that your application code can be "attached" to the Albatross application object. The :class:`PageObjectMixin` application mixin class requires that you implement application functionality in "page objects" and define methods that can be called by the toolkit. As an example, here is the page object for the ``'login'`` page of the ``popview`` sample application. .. code-block:: python class LoginPage: name = 'login' def page_process(self, ctx): if ctx.req_equals('login'): if ctx.locals.username and ctx.locals.passwd: try: ctx.open_mbox() ctx.add_session_vars('username', 'passwd') except poplib.error_proto: return ctx.set_page('list') def page_display(self, ctx): ctx.run_template('login.html') When the toolkit needs to process the browser request it calls the :meth:`page_process` method of the current page object. As you can see, the code determines which request was made by the browser, instantiates the application objects required to service the browser request, then directs the context to move to a new page via the :meth:`set_page` method. When Albatross is ready to display the browser response it calls the :meth:`page_display` method of the current page object. In the code above, :meth:`set_page` is only called if the mailbox is opened successfully. This means that a failed login will result in the login page being displayed again. Note that when you change pages the object that generates the HTML will be a different object to that that processed the browser request. To let you get a foothold on the toolkit application functionality we will work through another variant of the ``form`` application. .. _tug-form4: Using Albatross Input Tags (Again) ================================== In the previous chapter we demonstrated the use of Albatross input tags to transfer values from the execution context into HTML ```` tags, and from the browser request back into the execution context. In this section we present the same process using an Albatross application object. The sample program from this section is supplied in the ``samples/form4`` directory and can be installed in your web server ``cgi-bin`` directory by running the following commands. .. code-block:: sh cd samples/form4 python install.py The ``form.html`` template file used by the application follows. .. literalinclude:: ../samples/form4/form.html :language: albatross The most important new features in the template file are the use of the ```` tag, and the ``list`` attribute in the ```` tag. Most execution contexts created by application objects inherit from the :class:`NameRecorderMixin`. The :class:`NameRecorderMixin` records the name, type and multiple value disposition of each input tag in a form in a cryptographically signed hidden field named ``__albform__``. This mechanism prevents clients from being able to merge arbitrary data into the local namespace, as well as providing additional information to make the merging process more reliable. The recording process requires that all ```` tags be enclosed by an ```` tag. When the resulting form is submitted, the contents of the ``__albform__`` field controls merging of form fields into ``ctx.locals``. Only fields tracked by ``__albform__`` will be merged. Consequently, if submission occurs via a GET request without an ``__albform__`` field (for example, as a result of the user following an ````), the application must explicitly request relevent fields be merged via the ``merge_vars(...)`` method. [#]_ Any input field with the ``list`` attribute will always receive a list value from a POST browser request regardless of how many values (including none) were sent by the browser. An exception will be raised if you specify multiple input tags with the same name in a form and do not include the ``list`` attribute. The input tag types ``radio``, ``image``, and ``submit`` can only have a single value, even if multiple inputs of the same name appear in a form, and the ``list`` attribute should not be specified on these. The ``form.py`` program is show below. .. literalinclude:: ../samples/form4/form.py :language: python You can run the program by pointing your browser at ``_. Notice that the browser request is automatically merged into the local namespace and then extracted by the template when generating the HTML response. The program uses the :class:`SimpleApp` application class. :class:`SimpleApp` uses an object to define each page served by the application. Each of the page objects must be registered with the application via the :meth:`register_page` method. When the application enters a new page :class:`SimpleApp` calls the :meth:`page_enter` method of the page object to allow the application to initialise execution context values. In the above program the :meth:`page_enter` method initialises all values used by the HTML form to ``None``, initialises the variable *num* to ``0`` and places it into the session. As shown in the application processing sequence in the :ref:`app-model` section, the first step in handling a browser request is to create an execution context. The :class:`SimpleApp` class uses instances of the :class:`SimpleAppContext` class which inherits from :class:`HiddenFieldSessionMixin`. The :class:`HiddenFieldSessionMixin` class stores session data in a hidden field named ``__albstate__`` at the end of each form. When an Albatross application needs to display the result of processing a request it calls the :meth:`page_display` method of the current page. In the above program this method increments *num* and then runs the ``form.html`` template. It is important to note that any changes to session values after executing a template will be lost as the session state is saved in the HTML produced by the template. It is worth explaining again that the program does not perform any request merging --- this is all done automatically by the Albatross application and execution context objects. .. _app-popview1: The Popview Application ======================= In this section we will develop a simple application that allows a user to log onto a POP server to view the contents of their mailbox. Python provides the :mod:`poplib` module which provides a nice interface to the POP3 protocol. The complete sample program is contained in the ``samples/popview1`` directory. Use the ``install.py`` script to install the sample. .. code-block:: sh cd samples/popview1 python install.py First of all let's create the *model* components of the application. This consists of some classes to simplify access to a user mailbox. Create a module called ``popviewlib.py`` and start with the :class:`Mbox` class. .. code-block:: python import string import poplib pophost = 'pop' class Mbox: def __init__(self, name, passwd): self.mbox = poplib.POP3(pophost) self.mbox.user(name) self.mbox.pass_(passwd) def __getitem__(self, i): try: return Msg(self.mbox, i + 1) except poplib.error_proto: raise IndexError The important feature of our :class:`Mbox` class is that it implements the Python sequence protocol to retrieve messages. This allows us to iterate over the mailbox using the ```` tag in templates. When there is an attempt to retrieve a message number which does not exist in the mailbox, a :mod:`poplib.error_proto` exception will be raised. We transform this exception into an :exc:`IndexError` exception to signal the end of the sequence. The sequence protocol is just one of the truly excellent Python features which allows your application objects to become first class citizens. Next we need to implement a :class:`Msg` class to access the header and body of each message. .. code-block:: python class Msg: def __init__(self, mbox, msgnum): self.mbox = mbox self.msgnum = msgnum self.read_headers() def read_headers(self): res = self.mbox.top(self.msgnum, 0) hdrs = Headers() hdr = None for line in res[1]: if line and line[0] in string.whitespace: hdr = hdr + '\n' + line else: hdrs.append(hdr) hdr = line hdrs.append(hdr) self.hdrs = hdrs return hdrs def read_body(self): res = self.mbox.retr(self.msgnum) lines = res[1] for i in range(len(lines)): if not lines[i]: break self.body = string.join(lines[i:], '\n') return self.body Note that we retrieve the message headers in the constructor to ensure that the creation of the :class:`Msg` object will fail if there is no such message in the mailbox. The :class:`Mbox` class uses the exception raised by the :mod:`poplib` module to detect when a non-existent message is referenced. The :mod:`poplib` module returns the message headers as a flat list of text lines. It is up to the :mod:`poplib` user to process those lines and impose a higher level structure upon them. The :meth:`read_headers` method attaches header continuation lines to the corresponding header line before passing each complete header to a :class:`Headers` object. The :meth:`read_body` method retrieves the message body lines from the POP server and combines them into a single string. We are going to need to display message headers by name so our :class:`Headers` class implements the dictionary protocol. .. code-block:: python class Headers: def __init__(self): self.hdrs = {} def append(self, header): if not header: return parts = string.split(header, ': ', 1) name = string.capitalize(parts[0]) if len(parts) > 1: value = parts[1] else: value = '' curr = self.hdrs.get(name) if not curr: self.hdrs[name] = value return if type(curr) is type(''): curr = self.hdrs[name] = [curr] curr.append(value) def __getitem__(self, name): return self.hdrs.get(string.capitalize(name), '') Instead of raising a :exc:`KeyError` for undefined headers, the :meth:`__getitem__` method returns the empty string. This allows us to test the presence of headers in template files without being exposed to exception handling. Lets take all of these classes for a spin in the Python interpreter. .. code-block:: pycon >>> import popviewlib >>> mbox = popviewlib.Mbox('djc', '***') >>> msg = mbox[0] >>> msg.hdrs['From'] 'Owen Taylor ' >>> print msg.read_body() Daniel Egger writes: > Am 05 Aug 2001 12:00:15 -0400 schrieb Alex Larsson: > [snip] Next we will create the application components (the *controller*) in ``popview.py``. Albatross has a prepackaged application object which you can use for small applications; the :class:`SimpleApp` class. In anything but the most trivial applications it is probably a good idea to draw a site map like figure :ref:`fig-popview-sitemap` to help visualise the application. Our application will contain three pages; login, message list, and message detail. .. _fig-popview-sitemap: .. figure:: .build/figures/pagemap.* :align: center Popview Site Map The :class:`SimpleApp` class inherits from the :class:`PageObjectMixin` class which requires that the application define an object for each page in the application. Albatross stores the current application page identifier in the local namespace of the execution context as :attr:`__page__`. The value is automatically created and placed into the session. When the current page changes, the Albatross application object calls the :meth:`page_enter` function/method of the new page object. The :meth:`page_process` method is called to process a browser request, and finally :meth:`page_display` is called to generate the application response. The :class:`SimpleApp` class creates :class:`SimpleAppContext` execution context objects. By subclassing :class:`SimpleApp` and overriding :meth:`create_context` we can define our own execution context class. The prologue of the application imports the required application and execution context classes from Albatross and the :class:`Request` class from the deployment module. .. code-block:: python #!/usr/bin/python from albatross import SimpleApp, SimpleAppContext from albatross.cgiapp import Request import poplib import popviewlib Next in the file is the class for the login page. Note that we only implement glue (or *controller*) logic in the page objects. Each time a new page is served we will need to open the mail box and retrieve the relevant data. That means that the username and password will need to be stored in some type of session data storage. .. code-block:: python class LoginPage: name = 'login' def page_process(self, ctx): if ctx.req_equals('login'): if ctx.locals.username and ctx.locals.passwd: try: ctx.open_mbox() ctx.add_session_vars('username', 'passwd') except poplib.error_proto: return ctx.set_page('list') def page_display(self, ctx): ctx.run_template('login.html') The :meth:`req_equals` method of the execution context looks inside the browser request for a field with the specified name. It returns a TRUE value if such a field exists and it has a value not equal to ``None``. The test above will detect when a user presses the submit button named ``'login'`` on the ``login.html`` page. Next, here is the page object for displaying the list of messages in the mailbox. .. code-block:: python class ListPage: name = 'list' def page_process(self, ctx): if ctx.req_equals('detail'): ctx.set_page('detail') def page_display(self, ctx): ctx.open_mbox() ctx.run_template('list.html') The "detail" page displays the message detail. .. code-block:: python class DetailPage: name = 'detail' def page_process(self, ctx): if ctx.req_equals('list'): ctx.set_page('list') def page_display(self, ctx): ctx.open_mbox() ctx.read_msg() ctx.run_template('detail.html') And finally we define the application class and instantiate the application object. Note that we have subclassed :class:`SimpleApp` to create our own application class. This allows us to implement our own application level functionality as required. .. code-block:: python class AppContext(SimpleAppContext): def open_mbox(self): if hasattr(self.locals, 'mbox'): return self.locals.mbox = popviewlib.Mbox(self.locals.username, self.locals.passwd) def read_msg(self): if hasattr(self.locals, 'msg'): return self.locals.msg = self.locals.mbox[int(self.locals.msgnum) - 1] self.locals.msg.read_body() class App(SimpleApp): def __init__(self): SimpleApp.__init__(self, base_url='popview.py', template_path='.', start_page='login', secret='-=-secret-=-') for page_class in (LoginPage, ListPage, DetailPage): self.register_page(page_class.name, page_class()) def create_context(self): return AppContext(self) app = App() if __name__ == '__main__': app.run(Request()) The *base_url* argument to the application object constructor will be placed into the ``action`` attribute of all forms produced by the ```` tag. It will also form the left hand side of all hrefs produced by the ```` tag. The *template_path* argument is a relative path to the directory that contains the application template files. The *start_page* argument is the name of the application start page. When a browser starts a new session with the application it will be served the application start page. We have also created our own execution context to provide some application functionality as execution context methods. With the *model* and *controller* components in place we can now move onto the template files that comprise the *view* components of the application. First let's look at the ``login.html`` page. .. literalinclude:: ../samples/popview1/login.html :language: albatross When you look at the HTML produced by the application you will notice two extra ```` tags have been generated at the bottom of the form. They are displayed below (reformatted to fit on the page). .. _alb-hidden: .. code-block:: html If we fire up the Python interpreter we can have a look at what these fields contain. .. code-block:: pycon >>> import base64,zlib,cPickle >>> s = "eJzTDJeu3P90rZC6dde04xUhHL\n" + \ ... "WFjBqhHKXFqUV5ibmphUzeDKFsBYnFxeUphcxANmtOfnpmXiGLN0OpHgB7UBOp\n" >>> cPickle.loads(zlib.decompress(base64.decodestring(s))[16:]) {'username': 0, 'passwd': 0, 'login': 0} >>> s = "eJzT2sr5Jezh942TUrMty6q1j\n" + \ ... "WsLGUM54uMLEtNT4+MLmUJZc/LTM/MKmYv1AH8XEAY=\n" >>> cPickle.loads(zlib.decompress(base64.decodestring(s))[16:]) {'__page__': 'login'} The first string contains a dictionary that defines the name and type of the input fields that were present in the form. This is placed into the form by the :class:`NameRecorderMixin` class which is subclassed by :class:`SimpleAppContext`. If you look at the definition of :class:`SimpleAppContext` you will notice the following definition at the start of the class. .. code-block:: python NORMAL = 0 MULTI = 1 MULTISINGLE = 2 FILE = 3 The value ``0`` for each field in the dictionary above corresponds to field type ``NORMAL``. When merging the browser request into the execution context the dictionary of field names is used to assign the value ``None`` to any ``NORMAL`` or ``FILE`` fields, or ``[]`` to any ``LIST`` fields that were left empty by the user. This is useful because it lets us write application code which can ignore the fact that fields left empty by the user will not be sent by the browser when the form is submitted. The second string contains all of the session values for the application. In the application start page the only session variable that exists is the :attr:`__page__` variable. The :class:`HiddenFieldSessionMixin` places this field in the form when the template is executed and pulls the field value back out of the browser request into the execution context when the session is loaded. The first 20 bytes of the decompressed string is an HMAC-SHA1 cryptographic signature. This was generated by combining the application secret (passed as the *secret* argument to the application object) with the pickle string with a cryptographic hash. When the field is sent back to the application the signing process is repeated. The pickle is only loaded if the sign sent by the browser and the regenerated signature are the same. Since the ``popview`` application has been provided for example purposes you will probably forgive the usage of the :class:`HiddenFieldSessionMixin` class to propagate session state. In a real application that placed usernames and passwords in the session you would probably do something to protect these values from prying eyes. Now let's look at the ``list.html`` template file. Note that the use of the ```` tag causes the HTML output to be streamed to the browser. Use of this tag can give your application a much more responsive feel when generating pages that involve lengthy processing. A slightly obscure feature of the page is the use of a separate form surrounding the ```` field used to select each message. An unfortunate limitation of the HTML ```` tag is that you cannot associate a value with the field because the browser returns the coordinates where the user pressed the mouse inside the image. In order to associate the message number with the image button we place the message number in a separate hidden ```` field and group the two fields using a form. You have to be careful creating a large number of forms on the page because each of these forms will also contain the ``__albform__`` and ``__albstate__`` hidden fields. If you have a lot of data in your session the size of the ``__albstate__`` field will cause the size of the generated HTML to explode. .. literalinclude:: ../samples/popview1/list.html :language: albatross Finally, here is the message detail page ``detail.html``. .. literalinclude:: ../samples/popview1/detail.html :language: albatross .. _app-popview2: Adding Pagination Support to Popview ==================================== If the previous section we constructed a simple application for viewing the contents of a mailbox via the :mod:`poplib` module. One problem with the application is that there is no limit to the number of messages that will be displayed. In this section we will build pagination support into the message list page. We will modify the application to display 15 messages on each page and will add buttons to navigate to the next and previous pages. The ``pagesize`` attribute of the ```` tag provides automatic pagination support for displaying sequences. The only extra code that we need to add is a ``__len__`` method to the :class:`Mbox` class which returns the number of messages in the mailbox. This ``__len__`` method is needed to allow the template file to test whether or not to display a next page control. The complete sample program is contained in the ``samples/popview2`` directory. Use the ``install.py`` script to install the sample. .. code-block:: sh cd samples/popview2 python install.py Add a ``__len__`` method to the :class:`Mbox` class in ``popviewlib.py``. .. code-block:: python class Mbox: def __init__(self, name, passwd): self.mbox = poplib.POP3(pophost) self.mbox.user(name) self.mbox.pass_(passwd) def __getitem__(self, i): try: return Msg(self.mbox, i + 1) except poplib.error_proto: raise IndexError def __len__(self): len, size = self.mbox.stat() return len Now we modify the template file ``list.html``. .. literalinclude:: ../samples/popview2/list.html :language: albatross The ```` tag just below the ```` tag contains two new attributes; ``pagesize`` and ``prepare``. The ``pagesize`` turns on pagination for the ```` :class:`ListIterator` object and defines the size of each page. In order to remember the current top of page between pages, the tag places the iterator into the session. When saving the iterator, only the top of page and pagesize are retained. The ``prepare`` attribute instructs the ```` tag to perform all tasks except actually display the content of the sequence. This allows us to place pagination controls before the actual display of the list. .. _app-popview3: Adding Server-Side Session Support to Popview ============================================= So far we have been saving all application state at the browser inside hidden fields. Sometimes it is preferable to retain state at the server side. Albatross includes support for server-side sessions in the :class:`SessionServerContextMixin` and :class:`SessionServerAppMixin` classes. In this section we will modify the ``popview.py`` program to use server-side sessions. The :class:`SessionServerAppMixin` class uses a socket to communicate with the session server (see :ref:`app-ses-daemon`). You will need to start this program before using the new ``popview`` application. In previous versions of the program we were careful to place all user response into forms. This allowed Albatross to transparently attach the session state to hidden fields inside the form. When using server-side sessions Albatross does not need to save any application state at the browser so we are free to use URL style user inputs. To illustrate the point, we will replace all of the form inputs with URL user inputs. The complete sample program is contained in the ``samples/popview3`` directory. Use the ``install.py`` script to install the sample. .. code-block:: sh cd samples/popview3 python install.py The new ``list.html`` template file follows. .. literalinclude:: ../samples/popview3/list.html :language: albatross Next the new ``detail.html`` template file. .. literalinclude:: ../samples/popview3/detail.html :language: albatross One of the more difficult tasks for developing stateful web applications is dealing with browser requests submitted from old pages in the browser history. When all application state is stored in hidden fields in the HTML, requests from old pages do not usually cause problems. This is because the old application state is provided in the same request. When application state is maintained at the server, requests from old pages can cause all sorts of problems. The current application state at the server represents the result of a sequence of browser requests. If the user submits a request from an old page in the browser history then the fields and values in the request will probably not be relevant to the current application state Making sure all application requests are uniquely named provides some protection against the application processing a request from another page which just happened the share the same request name. It is not a complete defense as you may receive a request from an old version of the same page. A request from an old version of a page is likely to make reference to values which no longer exist in the server session. Some online banking applications attempt to avoid this problem by opening browser windows that do not have history navigation controls. A user who uses keyboard accelerators for history navigation will not be hindered by the lack of navigation buttons. The ``popview`` application does not modify any of the data it uses so there is little scope for submissions from old pages to cause errors. By changing the base class for the application object we can gain support for server side sessions. Albatross includes a simple session server and supporting mixin classes. The new application prologue looks like this: .. code-block:: python #!/usr/bin/python from albatross import SimpleSessionApp, SessionAppContext from albatross.cgiapp import Request import popviewlib The execution context now inherits from :class:`SessionAppContext`: .. code-block:: python class AppContext(SessionAppContext): def open_mbox(self): if hasattr(self.locals, 'mbox'): return self.locals.mbox = popviewlib.Mbox(self.locals.username, self.locals.passwd) def read_msg(self): if hasattr(self.locals, 'msg'): return self.locals.msg = self.locals.mbox[int(self.locals.msgnum) - 1] self.locals.msg.read_body() And the new application class looks like this: .. code-block:: python class App(SimpleSessionApp): def __init__(self): SimpleSessionApp.__init__(self, base_url='popview.py', template_path='.', start_page='login', secret='-=-secret-=-', session_appid='popview3') for page_class in (LoginPage, ListPage, DetailPage): self.register_page(page_class.name, page_class()) def create_context(self): return AppContext(self) The *session_appid* argument to the constructor is used to uniquely identify the application at the server so that multiple applications can be accessed from the same browser without the session from one application modifying the session from another. Apart from changes to load the new template files, we also need to change the :class:`ListPage` class because we changed the method of selecting messages from the message list. .. code-block:: python class ListPage: name = 'list' def page_process(self, ctx): if ctx.req_equals('msgnum'): ctx.set_page('detail') def page_display(self, ctx): ctx.open_mbox() ctx.run_template('list.html') .. _app-popview4: Building Applications with Page Modules ======================================= Implementing an application as a monolithic program is fine for small applications. As the application grows the startup time becomes an issue, as does maintenance. Albatross provides a set of classes that allow you to implement each page in a separate Python module. In this section we will convert the ``popview`` application to this type of application. Converting a monolithic application to a page module application is usually fairly simple. First we must turn each page object into a page module. When we used page objects, the class that implemented each page was identified by the :attr:`name` class member. With page modules the name of the module identifies the page that it processes. The complete sample program is contained in the ``samples/popview4`` directory. Use the ``install.py`` script to install the sample. .. code-block:: sh cd samples/popview4 python install.py The :class:`LoginPage` class becomes ``login.py``. .. literalinclude:: ../samples/popview4/login.py :language: python The :class:`ListPage` class becomes ``list.py``. .. literalinclude:: ../samples/popview4/list.py :language: python And the :class:`DetailPage` class becomes ``detail.py``. .. literalinclude:: ../samples/popview4/detail.py :language: python When using page modules we do not need to register each page module. When Albatross needs to locate the code for a page it simply imports the module. So the entire ``popview.py`` program now looks like this: .. literalinclude:: ../samples/popview4/popview.py :language: python .. _app-random: Random Access Applications ========================== In the popview application the server is in complete control of the sequence of pages that are served to the browser. In some applications you want the user to be able to bookmark individual pages for later retrieval in any desired sequence. Albatross provides application classes built with the :class:`RandomPageModuleMixin` class for this very purpose. The ``random`` sample is provided to demonstrate the use of the :class:`RandomModularSessionApp` class. Use the ``install.py`` script to install the sample. .. code-block:: python cd samples/random python install.py The complete mainline of the ``randompage.py`` sample is shown below. .. literalinclude:: ../samples/random/randompage.py :language: python When processing the browser request the application determines which page to serve to the browser by inspecting the URL in the browser request. The page identifier is taken from the part of the URL which follows the *base_url* argument to the constructor. If the page identifier is empty then the application serves the page identified by the *start_page* argument to the constructor. If you point your browser at ``_ you will notice that the server has redirected your browser to the start page. The sample program defines two pages which demonstrate two different ways to direct user navigation through the application. The ``tree.html`` page template uses a form to capture user input. .. literalinclude:: ../samples/random/pages/tree.html :language: albatross During conversion to HTML the ```` tag automatically places the name of the current page into the ``action`` attribute. This makes the browser send the response back to the same page module (``tree.py``). .. literalinclude:: ../samples/random/pages/tree.py :language: python When the application receives the ``paginate`` request it uses the :meth:`redirect` method to direct the browser to a new page. The ``paginate.html`` page template uses a URLs to capture user input. .. literalinclude:: ../samples/random/pages/paginate.html :language: albatross During conversion to HTML the ```` tag automatically translates the ``href="tree"`` attribute into a URL which requests the ``tree`` page from the application. Since the browser is doing all of the work, the ``paginate.py`` module which handles the ``paginate`` page is very simple. .. literalinclude:: ../samples/random/pages/paginate.py :language: python .. _app-ses-server: The Albatross Session Server ============================ The Albatross Session Server works in concert with the execution context and application mixin classes in the :mod:`albatross.session` module to provide server-side session recording. The application and the session server communicate via TCP sockets, by default on port 34343. More than one Albatross application can share a single session server process, allowing applications to be deployed over multiple web server hosts while still sharing state. .. _app-ses-daemon: Session Server Daemon --------------------- The Session Server is started via the Albatross command line ``albatross session-server``. On POSIX systems (Linux, Solaris, Mac OS X), the server will attempt to run in the background and become a daemon. This can be overridden via the ``--foreground`` option. Daemon mode has three sub-commands - ``start``, ``stop`` and ``status``. Daemon mode writes a file recording the process ID of daemon. The location of this file is controlled with the ``--pidfile=FILE`` option. Daemon mode is not available on non-POSIX platforms such as Windows. .. code-block:: sh $ albatross session-server start server pid 20653 .. code-block:: sh $ albatross session-server status running, pid is 20653 .. code-block:: sh $ albatross session-server stop stopping 20653 . done Note that the daemon does not need to run as ``root``, provided it listens on a port above 1024, and can write to it's pid file (and optional log file). If possible, you should run it under a user ID not shared by any other processes (and not ``nobody``). You should also ensure that only authorised clients can connect to your session server, as the protocol provides no authentication or authorisation mechanisms. By default, the daemon operates on port 34343. This can be changed with the ``--port=PORT`` option. .. code-block:: none Usage: albatross session-server [options]... Start or stop an albatross session server is one of: start start a new daemon stop kill the current daemon status request the daemon's status Options: -h, --help show this help message and exit -p PORT, --port=PORT Listen on PORT (default: 34343) -l LOGFILE, --logfile=LOGFILE, --log=LOGFILE Write log to LOGFILE (default: none) -D, --debug Generate additional debugging logs -k PIDFILE, --pidfile=PIDFILE, --pid-file=PIDFILE Record server pid in PIDFILE (default: /var/run/al- session-server.pid) --daemon, --bg Run in the background --foreground, --fg Run in the foreground .. _app-ses-server-start: Starting the Session Server on boot ----------------------------------- This depends on the specific startup system used by your platform and, if not done correctly, can prevent your system booting. System V rc.d ^^^^^^^^^^^^^ .. code-block:: sh #!/bin/sh # # SysV-style init.d script for the albatross session daemon # ### BEGIN INIT INFO # Provides: al-session-server # Required-Start: $remote_fs # Required-Stop: $remote_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Albatross session daemon # Description: Albatross session daemon ### END INIT INFO DAEMON="/usr/bin/albatross session-server" USER=albatross PIDFILE=/var/run/al-session-server.pid test -x ${DAEMON} || exit 0 umask 077 case "$1" in start) echo "Starting albatross session daemon" su ${USER} -c "${DAEMON} --pidfile ${PIDFILE} start" ;; stop) echo "Stopping albatross session daemon" su ${USER} -c "${DAEMON} --pidfile ${PIDFILE} stop" ;; *) echo "Usage: $* {start|stop}" exit 1 ;; esac exit 0 Upstart ^^^^^^^ This gets placed in ``/etc/init/al-session-server``, start as root with ``initctl start al-session-server``. .. code-block:: none description "Albatross Session Server" author "Albatross " start on local-filesystems stop on runlevel [!2345] respawn exec /usr/bin/albatross session-server --foreground OS X launchd ^^^^^^^^^^^^ Copy the following to ``/Launch/LaunchDaemons/al-session-server.plist`` and run ``launchctl load /Library/LaunchDaemons/al-session-server.plist``. .. code-block:: xml Label au.com.obect-craft.session-server UserName albatross RunAtLoad Program /usr/local/bin/albatross ProgramArguments albatross session-server --foreground Solaris 10 SMF ^^^^^^^^^^^^^^ Save the following as ``al-session-server.xml`` and install with ``svccfg import al-session-server.xml``, then start the server with ``svcadm enable al-session-server``. You can examine the state of the server using ``svcs -x al-session-server``. .. code-block:: xml systemd ^^^^^^^ To be written - tips gratefully received. Windows ^^^^^^^ To be written - tips gratefully received. .. _app-ses-simple-server: Sample Simple Session Server ---------------------------- The ``albatross.simpleserver`` module is a simple session server that records sessions in memory. It can be used as the basis for a more sophisticated session server, or imported and instantiated directly. Internally the server uses a select loop to allow connections from multiple applications simultaneously. Application constructor arguments which are relevant to the session server are: * *session_appid* This is used to identify the application with the session server. It is also used as the session id in the cookie sent to the browser. * *session_server* = ``'localhost'`` If you decide to run the session server on a different machine to the application you must pass the host name of the session server in this argument. * *server_port* = ``34343`` If you decide to run the session server on a different port you must pass the port number in this argument. * *session_age* = ``1800`` This argument defines the amount of time in seconds for which idle sessions will kept in the server. .. _app-ses-protocol: Server Protocol --------------- You can see the session server in action by using telnet. .. code-block:: none djc@rat:~$ telnet localhost 34343 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. new myapp 3600 OK 38b80b3f546c8cfa put myapp 38b80b3f546c8cfa OK - send data now, terminate with blank line here is my session data it is on multiple lines OK get myapp 38b80b3f546c8cfa OK - session follows here is my session data it is on multiple lines del myapp 38b80b3f546c8cfa OK get myapp 38b80b3f546c8cfa ERROR no such session quit Connection closed by foreign host. djc@rat:~$ All dialogue is line based using a CRLF end of line sequence. Session ids are generated by the server and each application has its own set of session ids. The application mixin class in the :mod:`albatross.session` module uses the *session_appid* argument to the constructor as the application id with the session server. Note that this application id is also used in the cookie sent to the browser. If a command was successful the server response line will start with the text ``'OK'`` otherwise it will start with ``'ERROR'``. .. _app-proto-new: Create New Session ^^^^^^^^^^^^^^^^^^ To create a new session for the *appid* application which will be deleted if left idle for more than *age* seconds the application sends a line of the form:: "new " appid " " age CRLF Successful response will be a line of the form:: "OK " sesid CRLF .. _app-proto-put: Save Session ^^^^^^^^^^^^ To save data into an existing session the application sends a line of the form:: "put " appid " " sesid CRLF If the session exists in the server it will respond with the following line:: "OK - send data now, terminate with blank line" CRLF The program then sends a sequence of text lines terminated by a single blank line. The server then responds with:: "OK" CRLF .. _app-proto-get: Retrieve Session ^^^^^^^^^^^^^^^^ To retrieve data for an existing session the application sends a line of the form:: "get " appid " " sesid CRLF If the session exists in the server it will respond with the following line:: "OK - session follows" CRLF The session data saved previously will then be sent terminated by a single blank line. .. _app-proto-del: Delete Session ^^^^^^^^^^^^^^ To delete an existing session the application sends a line of the form:: "del " appid " " sesid CRLF If the session exists it will be deleted and the server will respond with the following line:: "OK" CRLF .. _app-proto-quit: Quit ^^^^ To disconnect from the server the application sends a line of the form:: "quit" CRLF The server will then close the connection. .. _app-deployoptions: Application Deployment Options ============================== In all of the previous sections you will note that all of the programs used the :class:`Request` class from the :mod:`albatross.cgiapp` module to deploy the application as a CGI script. The choice of :class:`Request` class determines how you wish to deploy your application. Albatross supplies a number of pre-built Request implementations suited to various deployment methods. You should import the Request method from the appropriate module: +--------------------------------+----------------------------+ | Deployment Method | Request Module | +================================+============================+ | CGI | :mod:`albatross.cgiapp` | +--------------------------------+----------------------------+ | mod_python | :mod:`albatross.apacheapp` | +--------------------------------+----------------------------+ | FastCGI_python | :mod:`albatross.fcgiapp` | +--------------------------------+----------------------------+ | Stand-alone Python HTTP server | :mod:`albatross.httpdapp` | +--------------------------------+----------------------------+ By placing all deployment dependencies in a :class:`Request` class you are able to change deployment method with only minimal changes to your application mainline code. You could, for instance, carry out your initial development as a stand-alone Python HTTP server, where debugging is easier, and final deployment as FastCGI. You could also develop your own :class:`Request` class to deploy an Albatross application in other ways, such as using the Medusa web server (``_), or to provide a :class:`Request` class which for performing unit tests on your application. The chapter on mod_python contains an example where the popview application is changed from CGI to ``mod_python``. .. _app-deploycgi: ``CGI`` Deployment ------------------ The :mod:`albatross.cgiapp` module contains a :class:`Request` class to allow you to deploy your application using ``CGI``. CGI is the simplest and most common application deployment scheme. The application is started afresh by your web server to service each client request, and is passed client input via the command line and stdin, and returns it's output via stdout. An example of a CGI application: .. code-block:: python #!/usr/bin/python from albatross.cgiapp import Request class Application(...): ... app = Application() if __name__ == '__main__': app.run(Request()) .. _app-deploymodpython: ``mod_python`` Deployment ------------------------- The :mod:`albatross.apacheapp` module contains a :class:`Request` class to allow you to deploy your application using ``mod_python`` [#]_. In the following example, we change the popview application from CGI to ``mod_python``. The complete sample program is contained in the ``samples/popview5`` directory. Use the ``install.py`` script to install the sample. .. code-block:: sh cd samples/popview5 python install.py The new ``popview.py`` mainline follows. .. literalinclude:: ../samples/popview5/popview.py :language: python The :func:`handler` function is called by ``mod_python`` when a browser request is received that must be handled by the program. You also need to create a ``.htaccess`` file to tell Apache to run the application using ``mod_python``. : .. code-block:: apacheconf DirectoryIndex popview.py SetHandler python-program PythonHandler popview Assuming you install the popview sample below the ``/var/www`` directory you will need configure Apache settings for the ``/var/www/alsamp`` directory: .. code-block:: apacheconf AllowOverride FileInfo Indexes Order allow,deny Allow from all .. _app-deployfastcgi: ``FastCGI`` Deployment ---------------------- The :mod:`albatross.fcgiapp` module contains a :class:`Request` class to allow you to deploy your application using ``FastCGI`` [#]_. Applications deployed via CGI often perform poorly under load, because the application is started afresh to service each client request, and the start-up time can account for a significant proportion of request service time. ``FastCGI`` attempts to address this by turning the application into a persistent server that can handle many client requests. Unlike ``mod_python``, where applications run within the web server, ``FastCGI`` applications communicate with the web server via a platform-independent socket protocol. This improves security and the resilience of the web service. To deploy your application via ``FastCGI`` and Apache, you need to configure Apache to load ``mod_fastcgi.so``, configure it to start your script as a ``FastCGI`` server, and use the ``albatross.fcgiapp`` :class:`Request` class in your application. As an example of Apache configuration: .. code-block:: apacheconf LoadModule fastcgi_module /usr/lib/apache/1.3/mod_fastcgi.so AddHandler fastcgi-script py Options +ExecCGI And the application main-line: .. code-block:: python #!/usr/bin/python from albatross.fcgiapp import Request, running class Application(...): ... app = Application() if __name__ == '__main__': while running(): app.run(Request()) .. _app-deploypyhttpd: Stand-alone Python HTTP Server Deployment ----------------------------------------- The standard Python libraries provide a pure-Python HTTP server in the ``BaseHTTPServer`` module. Code contributed by Matt Goodall allows you to deploy your Albatross applications as stand-alone scripts using this module to service HTTP requests. Unlike the other ``Request`` classes, applications deployed via the stand-alone Python HTTP server do not require you to instantiate the ``Request`` class directly. Instead, the ``albatross serve`` script imports your application (specifically, the script that instantiates the application object), and starts the HTTP server. Currently, applications deployed via the stand-alone http server are single threaded, meaning that other requests are blocked while the current request is being serviced. In many cases this is not a problem as the requests are handled quickly, but you should consider other deployment options for production use. The stand-alone http server makes it particularly easy to develop Albatross applications, and is a great way to debug applications without the complications that a general purpose web server necessarily entail. Most of the Albatross samples can be run under the stand-alone server: .. code-block:: sh $ cd samples/tree2 $ albatross serve --port=8080 tree.app /alsamp/images ../images/ .. _app-exceptions: Albatross Exceptions ==================== .. exception:: AlbatrossError An abstract base class all Albatross exceptions inherit from. .. exception:: UserError Raised on abnormal input from the user. All current use of this exception is through the :exc:`SecurityError` subclass. .. exception:: ApplicationError Raised on invalid Albatross use by the application, such as attempting to set a response header after the headers have been sent to the client. Template errors are also instances of this exception. .. exception:: InternalError Raised if Albatross detects an internal error (bug). .. exception:: ServerError Raised on difficulties communicating with the session server or errors reading server-side session files. .. exception:: SecurityError A subclass of :exc:`UserError`, this exception is raised when Albatross detects potentially hostile client activity. .. exception:: TemplateLoadError A subclass of :exc:`ApplicationError`, this exception is raised if a template cannot be loaded. .. exception:: SessionExpired A subclass of :exc:`UserError`, this exception is raised when a client attempts to submit form results against a session that has expired. .. rubric:: Footnotes .. [#] Prior to Albatross version 1.36, in the absence of an ``__albform__`` field, all request fields would be merged. .. [#] For more information on ``mod_python``, including installation instructions, see ``_. .. [#] For more information on ``FastCGI``, including installation instructions, see ``_.