| | 58 | def 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 | |
| | 78 | if __name__ == '__main__': |
| | 79 | main(sys.argv[1]) |
| | 80 | }}} |
| | 81 | |
| | 82 | Enter the tutorial directory in the terminal, and run: |
| | 83 | |
| | 84 | {{{ |
| | 85 | $ PYTHONPATH=. python geddit/controller.py geddit.db |
| | 86 | }}} |
| | 87 | |
| | 88 | 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. |
| | 89 | |
| | 90 | 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. |
| | 91 | |
| | 92 | == Basic Template Rendering == |
| | 93 | |
| | 94 | So far the code doesn't actually use Genshi, or even any kind of templating. Let's change that. |
| | 95 | |
| | 96 | Inside 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 | |
| | 120 | This is basically an almost static HTML file with some simple variable substitution. |
| | 121 | |
| | 122 | 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: |
| | 123 | |
| | 124 | {{{ |
| | 125 | #!python |
| | 126 | import cherrypy |
| | 127 | from genshi.template import TemplateLoader |
| | 128 | from paste.evalexception.middleware import EvalException |
| | 129 | |
| | 130 | loader = TemplateLoader( |
| | 131 | os.path.join(os.path.dirname(__file__), 'templates'), |
| | 132 | auto_reload=True |
| | 133 | ) |
| | 134 | }}} |
| | 135 | |
| | 136 | Next, 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 | |
| | 146 | 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: |
| | 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 | |
| | 172 | 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. |
| | 173 | |
| | 174 | Inside the `geddit` directory, create a file named `model.py`, with the following content: |
| | 175 | |
| | 176 | {{{ |
| | 177 | #!python |
| | 178 | from datetime import datetime |
| | 179 | |
| | 180 | |
| | 181 | class 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 | |
| | 198 | class 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 | |
| | 209 | You'll need to import those classes in `geddit/controllers.py`, just below the other imports: |
| | 210 | |
| | 211 | {{{ |
| | 212 | #!python |
| | 213 | from geddit.model import Submission, Comment |
| | 214 | }}} |
| | 215 | |
| | 216 | And 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 |
| 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 | |
| | 242 | Now 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 | |
| | 246 | In 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 | |
| | 262 | 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: |
| 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 | |
| | 270 | Now let's change the `Root.index()` method in `geddit/controller.py` to pass the submissions list to the template: |
| 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) |