| 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) |