Python 3 Web Development Beginner's Guide

Python 3 Web Development Beginner's Guide Use Python to create, theme, and deploy unique web applications Michel Ande...
Author:  Michel Anders

467 downloads 4816 Views 3MB Size Report

This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!

Report copyright / DMCA form

'''%(limit,n,username if mine else "all", offset,'checked="yes"'if mine else "", pattern)

The list() method takes a number of keyword arguments to determine which books to return. It doesn't return a complete HTML page but just a list of
elements representing the selection of books together with some additional information on the number of books selected and the button elements used to browse through the list: ‹‹

The offset argument determines where the list of matching books will start. Counting starts at 0.

‹‹

The limit argument determines the number of matching books to return. This is a maximum, less books will be returned if they are not available. When we have 14 matching books, an offset of 10, with a limit of 10, will return 10 books through 13.

‹‹

If the mine argument is non-zero, the list of matching books is limited to the ones owned by the user issuing the request.

‹‹

if the pattern argument is not an empty string, the list of matching books is limited to the ones that contain the pattern string in their title.

‹‹

The _ (underscore) argument is ignored. We configured our AJAX calls not to be cached (in booksweb.js) and jQuery prevents caching by appending each time an _ argument with a random value to the URL it requests. The URL will look different each time to the browser this way, and this will prevent caching.

The implementation of the list() method starts off by validating that the user is logged in and then retrieving the corresponding User object. The next steps systematically validate the arguments passed to the method and raise a ValueError or TypeError if something fails to validate. The offset argument, for example, should be larger or equal to zero (highlighted). Once the arguments are validated, these are handed off to the booksdb.listbooks() function, that will take care of the actual selection and will return a tuple consisting of the number of matching books and the actual list of books sorted on their title. This list of books is used to step through and generate the appropriate HTML markup. For each book, we fetch the authors of the book (highlighted) and then yield a string with HTML markup. This HTML contains the title of the book and the name of the first author. If there is more information we would like to present, for example, the ISBN of the book, we could easily add it here. By using yield to return the results one-by–one, we save ourselves the trouble of constructing a complete string first before returning it in one go.

[ 161 ]

Entities and Relations

The final yield statement contains a
element with the id="navigation". We choose to return the complete navigation markup, including buttons, to enable us to easily set the values of these buttons. The pattern element, for example, should display the current text we filter on. We could pass this as separate information and use client-side JavaScript to set these values but this would complicate the JavaScript quite a bit. Still, the offset and limit values together with the total number of matching books is returned inside a

element. This serves two goals: we can display this as an informational message to the user, but it is also necessary information for the navigation buttons to function.

Time for action – adding a new book The screen to add a new book to the database is a simple form. What we need to implement is: ‹‹

Some HTML to make it possible to display the form

‹‹

A method in our CherryPy application that will produce this HTML

‹‹

A method to process the input once this form is submitted

There is no need to implement two different methods here because based on the arguments passed to the method we can decide whether to return a form or to process the submitted contents of the same form. Although it may be considered bad form to design a method to do two things, it does keep related functionality together.

What just happened? The addbookform class variable contains the template that refers to a number of string variables to interpolate. There is also a <script> element to add some extra functionality that we examine later: Chapter5/booksweb.py addbookform='''

Add new book
[ 162 ]

Chapter 5
'''

The addbook() method itself is used both to display the initial screen and to process the results, that is, it acts as the target of the
element's action attribute and processes the values from the various and
[ 178 ]

Chapter 6
%s
<script src="/wikiweb.js" type="text/javascript">

The element contains both links to CSS style sheets and <script> elements that refer to the jQuery libraries. This time, we choose again to retrieve these libraries from a public content delivery network. The highlighted lines show the top-level
elements that define the structure of the page. In this case, we have identified a navigation part and a content part and this is reflected in the HTML markup. Enclosed in the navigation part are the search functions, each in their own
element. The content part contains just an interpolation placeholder %s for now, that will be filled in by the method that serves this markup. Just before the end of the body of the markup is a final <script> element that refers to a JavaScript file that will perform actions specific to our application and we will examine those later.

The application methods The markup from the previous section is served by methods of the Wiki class, an instance of which class can be mounted as a CherryPy application. The index() method, for example, is where we produce the markup for the opening screen (the complete file is available as wikiweb.py and contains several other methods that we will examine in the following sections): Chapter6/wikiweb.py @cherrypy.expose def index(self): item = '
  • %s
  • ' topiclist = "\n".join( [item%(t,t)for t in wiki.gettopiclist()]) content = '
      %s
    '%( topiclist,) return basepage % content

    First, we define the markup for every topic we will display in the main area of the opening page (highlighted). The markup consists of a list item that contains an anchor element that refers to a URL relative to the page showing the opening screen. Using relative URLs allows us to mount the class that implements this part of the application anywhere in the tree that serves the CherryPy application. The show() method that will serve this URL takes a topic parameter whose value is interpolated in the next line for each topic that is present in the database. [ 179 ]

    Building a Wiki

    The result is joined to a single string that is interpolated into yet another string that encapsulates all the list items we just generated in an unordered list (a
      element in the markup) and this is finally returned as the interpolated content of the basepage variable. In the definition of the index() method, we see a pattern that will be repeated often in the wiki application: methods in the delivery layer, like index(), concern themselves with constructing and serving markup to the client and delegate the actual retrieval of information to a module that knows all about the wiki itself. Here the list of topics is produced by the wiki.gettopiclist() function, while index() converts this information to markup. Separation of these activities helps to keep the code readable and therefore maintainable.

      Time for action – implementing a wiki topic screen When we request a URL of the form show?topic=value, this will result in calling the show() method. If value equals an existing topic, the following (as yet unstyled) screen is the result:

      Just as for the opening screen, we take steps to: ‹‹

      Identify the main areas on screen

      ‹‹

      Identify specific functionality

      ‹‹

      Identify any hidden functionality

      The page structure is very similar to the opening screen, with the same navigational items, but instead of a list of topics, we see the content of the requested topic together with some additional information like the tags associated with this subject and a button that may be clicked to edit the contents of this topic. After all, collaboratively editing content is what a Wiki is all about.

      [ 180 ]

      Chapter 6

      We deliberately made the choice not to refresh the contents of just a part of the opening screen with an AJAX call, but opted instead for a simple link that replaces the whole page. This way, there will be an unambiguous URL in the address bar of the browser that will point at the topic. This allows for easy bookmarking. An AJAX call would have left the URL of the opening screen that is visible in the address bar of the browser unaltered and although there are ways to alleviate this problem, we settle for this simple solution here.

      What just happened? As the main structure we identified is almost identical to the one for the opening page, the show() method will reuse the markup in basepage.html. Chapter6/wikiweb.py @cherrypy.expose def show(self,topic): topic = topic.capitalize() currentcontent,tags = wiki.gettopic(topic) currentcontent = "".join(wiki.render(currentcontent)) tags = ['
    • %s
    • '%( t,t) for t in tags] content = '''

      %s

      Edit
      %s
        %s
      revisions
      ''' % ( topic, topic, currentcontent,"\n".join(tags)) return basepage % content

      The show() method delegates most of the work to the wiki.gettopic() method (highlighted) that we will examine in the next section and concentrates on creating the markup it will deliver to the client. wiki.gettopic() will return a tuple that consists of both the current content of the topic and a list of tags. Those tags are converted to
    • elements with anchors that point to the searchtags URL. This list of tags provides a simple way for the reader to find related topics with a single click. The searchtags URL takes a tags argument so a single
    • element constructed this way may look like this:
    • Python
    • . The content and the clickable list of tags are embedded in the markup of the basepage together with an anchor that points to the edit URL. Later, we will style this anchor to look like a button and when the user clicks it, it will present a page where the content may be edited. [ 181 ]

      Building a Wiki

      Time for action – editing wiki topics In the previous section, we showed how to present the user with the contents of a topic but a wiki is not just about finding topics, but must present the user with a way to edit the content as well. This edit screen is presented in the following screenshot:

      Besides the navigation column on the left, within the edit area, we can point out the following functionality: ‹‹

      Elements to alter the title of the subject.

      ‹‹

      Modify the tags (if any) associated with the topic.

      ‹‹

      A large text area to edit the contents of the topic. On the top of the text area, we see a number of buttons that can be used to insert references to other topics, external links, and images.

      ‹‹

      A Save button that will submit the changes to the server.

      What just happened? The edit() method in wikiweb.py is responsible for showing the edit screen as well as processing the information entered by the user, once the save button is clicked: Chapter6/wikiweb.py @cherrypy.expose def edit(self,topic, content=None,tags=None,originaltopic=None):

      [ 182 ]

      Chapter 6 user = self.logon.checkauth( logonurl=self.logon.path, returntopage=True)

      if content is None : currentcontent,tags = wiki.gettopic(topic) html = '''
      preview
      %s
      <script> $("#imagedialog").dialog( [ 183 ]

      Building a Wiki {autoOpen:false, width:600, height:600}); '''%(topic, topic, currentcontent, ", ".join(tags), "".join(self.images())) return basepage % html else : wiki.updatetopic(originaltopic,topic,content,tags) raise cherrypy.HTTPRedirect('show?topic='+topic)

      The first priority of the edit() method is to verify that the user is logged in as we want only known users to edit the topics. By setting the returntopage parameter to true, the checkauth() method will return to this page once the user is authenticated. The edit() method is designed to present the edit screen for a topic as well as to process the result of this editing when the user clicks the Save button and therefore takes quite a number of parameters. The distinction is made based on the content parameter. If this parameter is not present (highlighted), the method will produce the markup to show the various elements in the edit screen. If the content parameter is not equal to None, the edit() method was called as a result of submitting the content of the form presented in the edit screen, in which case, we delegate the actual update of the content to the wiki.updatetopic() method. Finally, we redirect the client to a URL that will show the edited content again in its final form without the editing tools. At this point, you may wonder what all this business is about with both a topic and an originaltopic parameter. In order to allow the user to change the title of the topic while that title is also used to find the topic entity that we are editing, we pass the title of the topic as a hidden variable in the edit form, and use this value to retrieve the original topic entity, a ploy necessary because, at this point, we may have a new title and yet have to find the associated topic that still resides in the database with the old title. Cross Site Request Forgery When we process the data sent to the edit() function we make sure that only authenticated users submit anything. Unfortunately, this might not be enough if the user is tricked into sending an authenticated request on behalf of someone else. This is called Cross Site Request Forgery (CSRF) and although there are ways to prevent this, these methods are out of scope for this example. Security conscious people should read up on these exploits, however, and a good place to start is http://www.owasp.org/index.php/Main_Page and for Python-specific discussions http://www.pythonsecurity.org/. [ 184 ]

      Download from Wow! eBook <www.wowebook.com>

      Chapter 6

      Pop quiz What other attribute of the Topic entity could we have passed to retrieve a reference to the topic we are editing?

      Additional functionality In the opening screen as well as in the pages showing the content of topics and in the editing page, there is a lot of hidden functionality. We already encountered several functions of the wiki module and we will examine them in detail in this section together with some JavaScript functionality to enhance the user interface.

      Time for action – selecting an image On the page that allows us to edit a topic, we have half hidden an important element: the dialog to insert an image. If the insert image button is clicked, a dialog is present, as shown in the following image:

      [ 185 ]

      Building a Wiki

      Because a dialog is, in a way, a page of its own, we take the same steps to identify the functional components: ‹‹

      Identify the main structure

      ‹‹

      Identify specific functional components

      ‹‹

      Identify hidden functionality

      The dialog consists of two forms. The top one consists of an input field that can be used to look for images with a given title. It will be augmented with jQuery UI's auto complete functionality. The second form gives the user the possibility to upload a new file while the rest of the dialog is filled with any number of images. Clicking on one of the images will close the dialog and insert a reference to that image in the text area of the edit page. It is also possible to close the dialog again without selecting an image by either clicking the small close button on the top-right or by pressing the Escape key.

      What just happened ? The whole dialog consists of markup that is served by the images() method. Chapter6/wikiweb.py @cherrypy.expose def images(self,title=None,description=None,file=None): if not file is None: data = file.file.read() wikidb.Image(title=title,description=description, data=data,type=str(file.content_type)) yield '''
      [ 186 ]

      Chapter 6
      ''' yield '
      \n' for img in self.getimages(): yield img yield '
      '

      There is some trickiness here to understand well: from the edit() method, we call this images() method to provide the markup that we insert in the page that is delivered to the client requesting the edit URL, but because we have decorated the images() method with a @cherrypy.expose decorator, the images() method is visible from the outside and may be requested with the images URL. If accessed that way, CherryPy will take care of adding the correct response headers. Being able to call this method this way is useful in two ways: because the dialog is quite a complex page with many elements, we may check how it looks without being bothered by it being part of a dialog, and we can use it as the target of the form that is part of the images dialog and that allows us to upload new images. As with the edit() method, the distinction is again made based on a whether a certain parameter is present. The parameter that serves this purpose is file and will contain a file object if this method is called in response to an image being submitted (highlighted). The file object is a cherrypy.file object, not a Python built in file object, and has several attributes, including an attribute called file that is a regular Python stream object. This Python stream object serves as an interface to a temporary file that CherryPy has created to store the uploaded file. We can use the streams read() method to get at its content. Sorry about all the references to file, I agree it is possibly a bit confusing. Read it twice if needed and relax. This summary may be convenient: This item

      has a

      which is a

      The images() method

      file parameter

      herrypy.file object

      A cherrypy.file object

      file attribute

      Python stream object

      A Python stream object

      name attribute

      name of a file on disk

      The Python stream can belong to a number of classes where all implement the same API. Refer to http://docs.python.org/py3k/library/ functions.html#open for details on Python streams.

      The cherrypy.file also has a content_type attribute whose string representation we use together with the title and the binary data to create a new Image instance. The next step is to present the HTML markup that will produce the dialog, possibly including the uploaded image. This markup contains two forms. [ 187 ]

      Building a Wiki

      The first one (highlighted in the previous code snippet) consists of an input field and a submit button. The input field will be augmented with auto complete functionality as we will see when we examine wikiweb.js. The submit button will replace the selection of images when clicked. This is also implemented in wikiweb.js by adding a click handler that will perform an AJAX call to the getimages URL. The next form is the file upload form. What makes it a file upload form is the element of the type file (highlighted). Behind the scenes, CherryPy will store the contents of a file type element in a temporary file and pass it to the method servicing the requested URL by submitting the form. There is a final bit of magic to pay attention to: we insert the markup for the dialog as part of the markup that is served by the edit() method, yet the dialog only shows if the user clicks the insert image button. This magic is performed by jQuery UI's dialog widget and we convert the
      element containing the dialog's markup by calling its dialog method, as shown in this snippet of markup served by the edit() method(): <script>$("#imagedialog").dialog({autoOpen:false});

      By setting the autoOpen option to false, we ensure that the dialog remains hidden when the page is loaded, after all, the dialog should only be opened if the user clicks the insert image button. Opening the dialog is accomplished by several pieces of JavaScript (full code available as wikiweb.js). The first piece associates a click handler with the insert image button that will pass the open option to the dialog, causing it to display itself:

      Chapter6/wikiweb.js $("#insertimage").click(function(){ $("#imagedialog").dialog("open"); });

      Note that the default action of a dialog is to close itself when the Escape key is pressed, so we don't have to do anything about that. Within the dialog, we have to configure the images displayed there to insert a reference in the text area when clicked and then close the dialog. We do this by configuring a live handler for the click event. A live handler will apply to elements that match the selector (in this case, images with the selectable-image class) even if they are not present yet. This is crucial, as we may upload new images that are not yet present in the list of images shown when the dialog is first loaded: Chapter6/wikiweb.js $(".selectable-image").live('click',function(){ $("#imagedialog").dialog("close"); [ 188 ]

      Chapter 6 var insert = "<" + $(this).attr("id").substring(3) + "," + $(this).attr("alt") + ">";



      var Area = $("#edittopic textarea"); var area = Area[0]; var oldposition = Area.getCursorPosition();



      var pre = area.value.substring(0, oldposition); var post = area.value.substring(oldposition);

      area.value = pre + insert + post;

      Area.focus().setCursorPosition(oldposition + insert.length); });

      The first activity of this handler is to close the dialog. The next step is to determine what text we would like to insert into the text area (highlighted). In this case, we have decided to represent a reference to an image within the database as a number followed by a description within angled brackets. For example, image number 42 in the database might be represented as <42,"Picture of a shovel">. When we examine the render() method in wikiweb.py, we will see how we will convert this angled bracket notation to HTML markup. The remaining part of the function is concerned with inserting this reference into the