Edgewall Software

Changes between Version 26 and Version 27 of GenshiTutorial


Ignore:
Timestamp:
Aug 30, 2007, 11:17:09 AM (17 years ago)
Author:
cmlenz
Comment:

Simplify data model, comments no longer hierarchical

Legend:

Unmodified
Added
Removed
Modified
  • GenshiTutorial

    v26 v27  
    4040#!/usr/bin/env python
    4141
    42 import os
    43 import pickle
    44 import sys
     42import operator, os, pickle, sys
    4543
    4644import cherrypy
     
    5856
    5957
     58def main(filename):
     59    # Some global configuration; note that this could be moved into a configuration file
     60    cherrypy.config.update({
     61        'request.throw_errors': True,
     62        'tools.encode.on': True, 'tools.encode.encoding': 'utf-8',
     63        'tools.decode.on': True,
     64        'tools.trailing_slash.on': True,
     65        'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)),
     66    })
     67
     68    # Initialize the application, and add EvalException for more helpful error messages
     69    app = cherrypy.Application(Root(data))
     70    app.wsgiapp.pipeline.append(('paste_exc', EvalException))
     71    cherrypy.quickstart(app, '/', {
     72        '/media': {
     73            'tools.staticdir.on': True,
     74            'tools.staticdir.dir': 'static'
     75        }
     76    })
     77
     78if __name__ == '__main__':
     79    main(sys.argv[1])
     80}}}
     81
     82Enter the tutorial directory in the terminal, and run:
     83
     84{{{
     85$ PYTHONPATH=. python geddit/controller.py geddit.db
     86}}}
     87
     88You should see a log message pointing you to the URL where the application is being served, which is usually http://localhost:8080/. Visiting that page will respond with just the string “Geddit”, as that's what the `index()` method of the `Root` object returns.
     89
     90Note that we've configured !CherryPy to serve static files from the `geddit/static` directory. !CherryPy will complain that that directory does not exist, so create it, but leave it empty for now. We'll add static resources later on in the tutorial.
     91
     92== Basic Template Rendering ==
     93
     94So far the code doesn't actually use Genshi, or even any kind of templating. Let's change that.
     95
     96Inside of the `geddit` directory, create a directory called `templates`, and inside that directory create a file called `index.html`, with the following content:
     97
     98{{{
     99#!genshi
     100<html xmlns="http://www.w3.org/1999/xhtml"
     101      xmlns:py="http://genshi.edgewall.org/">
     102  <head>
     103    <title>$title</title>
     104  </head>
     105  <body>
     106    <div id="header">
     107      <h1>$title</h1>
     108    </div>
     109
     110    <p>Welcome!</p>
     111
     112    <div id="footer">
     113      <hr />
     114      <p class="legalese">© 2007 Edgewall Software</p>
     115    </div>
     116  </body>
     117</html>
     118}}}
     119
     120This is basically an almost static HTML file with some simple variable substitution.
     121
     122We now need to change the controller code so that this template is used. First, add the Genshi `TemplateLoader` to the imports at the top of the `geddit/controller.py` file, and instantiate a loader for the `geddit/templates` directory:
     123
     124{{{
     125#!python
     126import cherrypy
     127from genshi.template import TemplateLoader
     128from paste.evalexception.middleware import EvalException
     129
     130loader = TemplateLoader(
     131    os.path.join(os.path.dirname(__file__), 'templates'),
     132    auto_reload=True
     133)
     134}}}
     135
     136Next, change the implementation of the `index()` method of the `Root` class to look like this:
     137
     138{{{
     139#!python
     140    @cherrypy.expose
     141    def index(self):
     142        tmpl = loader.load('index.html')
     143        return tmpl.generate(title='Geddit').render('html', doctype='html')
     144}}}
     145
     146This asks the template loader for a template named `index.html`, generates the output stream, and finally serializes the output to HTML. When you now reload the page in your browser, you should get back the following HTML response:
     147
     148{{{
     149#!xml
     150<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
     151<html>
     152  <head>
     153    <title>Geddit</title>
     154  </head>
     155  <body>
     156    <div id="header">
     157      <h1>Geddit</h1>
     158    </div>
     159
     160    <p>Welcome!</p>
     161
     162    <div id="footer">
     163      <hr />
     164      <p class="legalese">© 2007 Edgewall Software</p>
     165    </div>
     166  </body>
     167</html>
     168}}}
     169
     170== Data Model ==
     171
     172To continue, we'll need to first add some Python classes to define the data model the application will use. As mentioned above, we're using a simple pickle file for persistence, so all we need to do here is create a couple of very simply Python classes.
     173
     174Inside the `geddit` directory, create a file named `model.py`, with the following content:
     175
     176{{{
     177#!python
     178from datetime import datetime
     179
     180
     181class Submission(object):
     182
     183    def __init__(self, username, url, title):
     184        self.username = username
     185        self.url = url
     186        self.title = title
     187        self.time = datetime.utcnow()
     188        self.code = hex(hash(tuple([username, url, title, self.time])))[2:]
     189        self.comments = []
     190
     191    def __repr__(self):
     192        return '<%s %r>' % (type(self).__name__, self.title)
     193
     194    def add_comment(self, username, content):
     195        self.comments.append(Comment(username, content))
     196
     197
     198class Comment(object):
     199
     200    def __init__(self, username, content):
     201        self.username = username
     202        self.content = content
     203        self.time = datetime.utcnow()
     204
     205    def __repr__(self):
     206        return '<%s>' % (type(self).__name__)
     207}}}
     208
     209You'll need to import those classes in `geddit/controllers.py`, just below the other imports:
     210
     211{{{
     212#!python
     213from geddit.model import Submission, Comment
     214}}}
     215
     216And in the `main()` function, let's add some code to read our data from the pickle file, and write it back:
     217
     218{{{
     219#!python
    60220def main(filename):
    61221    # load data from the pickle file, or initialize it to an empty list
     
    67227            fileobj.close()
    68228    else:
    69         data = []
    70 
    71     # save data back to the pickle file when the server is stopped
     229        data = {}
     230
    72231    def _save_data():
     232        # save data back to the pickle file
    73233        fileobj = open(filename, 'wb')
    74234        try:
     
    78238    cherrypy.engine.on_stop_engine_list.append(_save_data)
    79239
    80     # Some global configuration; note that this could be moved into a configuration file
    81     cherrypy.config.update({
    82         'request.throw_errors': True,
    83         'tools.encode.on': True, 'tools.encode.encoding': 'utf-8',
    84         'tools.decode.on': True,
    85         'tools.trailing_slash.on': True,
    86         'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)),
    87     })
    88 
    89     # Initialize the application, and add EvalException for more helpful error messages
    90     app = cherrypy.Application(Root(data))
    91     app.wsgiapp.pipeline.append(('paste_exc', EvalException))
    92     cherrypy.quickstart(app, '/', {
    93         '/media': {
    94             'tools.staticdir.on': True,
    95             'tools.staticdir.dir': 'static'
    96         }
    97     })
    98 
    99 if __name__ == '__main__':
    100     main(sys.argv[1])
    101 }}}
    102 
    103 Enter the tutorial directory in the terminal, and run:
     240}}}
     241
     242Now let's add some initial content to our “database”.
     243
     244 '''Note: You'll need to stop the !CherryPy server to do the following, otherwise your changes will get overwritten'''.
     245
     246In the terminal, from the tutorial directory, launch the interactive Python shell by executing `PYTHONPATH=. python`, and enter the following code:
     247
     248{{{
     249#!pycon
     250>>> from geddit.model import *
     251>>> submission1 = Submission(username='joe', url='http://example.org/', title='An example')
     252>>> submission1.add_comment(username='jack', content='Bla bla bla')
     253>>> submission1.add_comment(username='joe', content='Bla bla bla, bla bla.')
     254>>> submission2 = Submission(username='annie', url='http://reddit.com/', title='The real thing')
     255
     256>>> import pickle
     257>>> pickle.dump({
     258...     submission1.code: submission1, submission2.code: submission2
     259... }, open('geddit.db', 'wb'))
     260}}}
     261
     262You should now have two submissions in the pickle file, with the first submission having a comment, as well as a reply to that comment. Restart the CherryPy server by running:
    104263
    105264{{{
     
    107266}}}
    108267
    109 You should see a log message pointing you to the URL where the application is being served, which is usually http://localhost:8080/. Visiting that page will respond with just the string “Geddit”, as that's what the `index()` method of the `Root` object returns.
    110 
    111 Note that we've configured !CherryPy to serve static files from the `geddit/static` directory. !CherryPy will complain that that directory does not exist, so create it, but leave it empty for now. We'll add static resources later on in the tutorial.
    112 
    113 == Basic Template Rendering ==
    114 
    115 So far the code doesn't actually use Genshi, or even any kind of templating. Let's change that.
    116 
    117 Inside of the `geddit` directory, create a directory called `templates`, and inside that directory create a file called `index.html`, with the following content:
    118 
    119 {{{
    120 #!genshi
    121 <html xmlns="http://www.w3.org/1999/xhtml"
    122       xmlns:py="http://genshi.edgewall.org/">
    123   <head>
    124     <title>$title</title>
    125   </head>
    126   <body>
    127     <div id="header">
    128       <h1>$title</h1>
    129     </div>
    130 
    131     <p>Welcome!</p>
    132 
    133     <div id="footer">
    134       <hr />
    135       <p class="legalese">© 2007 Edgewall Software</p>
    136     </div>
    137   </body>
    138 </html>
    139 }}}
    140 
    141 This is basically an almost static HTML file with some simple variable substitution.
    142 
    143 We now need to change the controller code so that this template is used. First, add the Genshi `TemplateLoader` to the imports at the top of the `geddit/controller.py` file, and instantiate a loader for the `geddit/templates` directory:
    144 
    145 {{{
    146 #!python
    147 import cherrypy
    148 from genshi.template import TemplateLoader
    149 from paste.evalexception.middleware import EvalException
    150 
    151 loader = TemplateLoader(
    152     os.path.join(os.path.dirname(__file__), 'templates'),
    153     auto_reload=True
    154 )
    155 }}}
    156 
    157 Next, change the implementation of the `index()` method of the `Root` class to look like this:
     268== Extending the Template ==
     269
     270Now let's change the `Root.index()` method in `geddit/controller.py` to pass the submissions list to the template:
    158271
    159272{{{
     
    161274    @cherrypy.expose
    162275    def index(self):
     276        submissions = sorted(self.data.values(),
     277                             key=operator.attrgetter('time'))
     278
    163279        tmpl = loader.load('index.html')
    164         return tmpl.generate(title='Geddit').render('html', doctype='html')
    165 }}}
    166 
    167 This asks the template loader for a template named `index.html`, generates the output stream, and finally serializes the output to HTML. When you now reload the page in your browser, you should get back the following HTML response:
    168 
    169 {{{
    170 #!xml
    171 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    172 <html>
    173   <head>
    174     <title>Geddit</title>
    175   </head>
    176   <body>
    177     <div id="header">
    178       <h1>Geddit</h1>
    179     </div>
    180 
    181     <p>Welcome!</p>
    182 
    183     <div id="footer">
    184       <hr />
    185       <p class="legalese">© 2007 Edgewall Software</p>
    186     </div>
    187   </body>
    188 </html>
    189 }}}
    190 
    191 == Data Model ==
    192 
    193 To continue, we'll need to first add some Python classes to define the data model the application will use. As mentioned above, we're using a simple pickle file for persistence, so all we need to do here is create a couple of very simply Python classes.
    194 
    195 Inside the `geddit` directory, create a file named `model.py`, with the following content:
    196 
    197 {{{
    198 #!python
    199 from datetime import datetime
    200 
    201 
    202 class Submission(object):
    203 
    204     def __init__(self, username, url, title):
    205         self.username = username
    206         self.url = url
    207         self.title = title
    208         self.time = datetime.utcnow()
    209         self.comments = []
    210 
    211     def __repr__(self):
    212         return '<%s %r>' % (type(self).__name__, self.title)
    213 
    214     def add_comment(self, username, content):
    215         comment = Comment(username, content, in_reply_to=self)
    216         self.comments.append(comment)
    217         return comment
    218 
    219 
    220 class Comment(object):
    221 
    222     def __init__(self, username, content, in_reply_to=None):
    223         self.username = username
    224         self.content = content
    225         self.in_reply_to = in_reply_to
    226         self.time = datetime.utcnow()
    227         self.replies = []
    228 
    229     def __repr__(self):
    230         return '<%s>' % (type(self).__name__)
    231 
    232     def add_reply(self, username, content):
    233         reply = Comment(username, content, in_reply_to=self)
    234         self.replies.append(reply)
    235         return reply
    236 }}}
    237 
    238 You'll need to import those classes in `geddit/controllers.py`, just below the other imports:
    239 
    240 {{{
    241 #!python
    242 from geddit.model import Submission, Comment
    243 }}}
    244 
    245 Now let's add some initial content to the “database”.
    246 
    247  '''Note: You'll need to stop the !CherryPy server to do that, otherwise the data is overwritten'''.
    248 
    249 In the terminal, from the tutorial directory, launch the interactive Python shell by executing `PYTHONPATH=. python`, and enter the following code:
    250 
    251 {{{
    252 #!pycon
    253 >>> from geddit.model import *
    254 >>> data = []
    255 >>> submission = Submission(username='joe', url='http://example.org/', title='An example')
    256 >>> comment = submission.add_comment(username='jack', content='Bla bla bla')
    257 >>> comment.add_reply(username='joe', content='Bla blabla bla bla bla')
    258 >>> data.append(submission)
    259 >>> submission = Submission(username='annie', url='http://reddit.com/', title='The real thing')
    260 >>> data.append(submission)
    261 >>> data
    262 [<Submission 'An example'>, <Submission 'The real thing'>]
    263 
    264 >>> import pickle
    265 >>> pickle.dump(data, open('geddit.db', 'wb'))
    266 >>> ^D
    267 }}}
    268 
    269 You should now have two submissions in the pickle file, with the first submission having a comment, as well as a reply to that comment. Restart the CherryPy server by running:
    270 
    271 {{{
    272 $ PYTHONPATH=. python geddit/controller.py geddit.db
    273 }}}
    274 
    275 == Extending the Template ==
    276 
    277 Now let's change the `Root.index()` method in `geddit/controller.py` to pass the submissions list to the template:
    278 
    279 {{{
    280 #!python
    281     @cherrypy.expose
    282     def index(self):
    283         tmpl = loader.load('index.html')
    284         stream = tmpl.generate(submissions=self.data)
     280        stream = tmpl.generate(submissions=submissions)
    285281        return stream.render('html', doctype='html')
    286282}}}
     
    303299
    304300    <ol py:if="submissions">
    305       <li py:for="submission in submissions">
     301      <li py:for="submission in reversed(submissions)">
    306302        <a href="${submission.url}">${submission.title}</a>
    307303        posted by ${submission.username}
     
    337333            # TODO: validate the input data!
    338334            submission = Submission(**data)
    339             self.data.append(submission)
     335            self.data[submission.code] = submission
    340336            raise cherrypy.HTTPRedirect('/')
    341337
     
    426422                data = form.to_python(data)
    427423                submission = Submission(**data)
    428                 self.data.append(submission)
     424                self.data[submission.code] = submission
    429425                raise cherrypy.HTTPRedirect('/')
    430426            except Invalid, e: