60
|
1 ********
|
|
2 Tutorial
|
|
3 ********
|
|
4
|
|
5 .. highlight:: none
|
|
6
|
|
7 ==============================================
|
|
8 First Step: Create a Directory for Your Webapp
|
|
9 ==============================================
|
|
10
|
|
11 TinCan is a lot like a web server; it serves a directory of files. There is
|
|
12 one catch, though: that directory must contain a ``WEB-INF`` subdirectory.
|
|
13 It's OK if ``WEB-INF`` ends up being empty (as it can, for a really simple
|
|
14 webapp). If ``WEB-INF`` is missing, TinCan will conclude the directory
|
|
15 doesn't contain a webapp and will refuse to serve it. (This is a deliberate
|
|
16 feature, added to prevent serving directories that don't contain webapps.)
|
|
17
|
|
18 So the first thing we must do is create a directory to hold our new webapp.
|
|
19 Let's call it ``demo``::
|
|
20 $ mkdir demo demo/WEB-INF
|
|
21
|
|
22 ===================
|
|
23 Adding Some Content
|
|
24 ===================
|
|
25 Use your favorite editor to create a file called ``hello.pspx``, and insert
|
|
26 the following content into it::
|
|
27
|
|
28 <!DOCTYPE html>
|
|
29 <html>
|
|
30 <head>
|
|
31 <title>Hello</title>
|
|
32 </head>
|
|
33 <body>
|
|
34 <h1>Hello, World</h1>
|
|
35 <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
|
|
36 </body>
|
|
37 </html>
|
|
38
|
|
39 Save the file to disk, and start up a test server::
|
|
40
|
|
41 $ launch demo
|
|
42
|
|
43 You should see something like the following::
|
|
44
|
|
45 adding route: /hello.pspx (GET)
|
|
46 Bottle v0.12.16 server starting up (using WSGIRefServer())...
|
|
47 Listening on http://localhost:8080/
|
|
48 Hit Ctrl-C to quit.
|
|
49
|
|
50 If you already have a server listening on port 8080, this naturally won't work.
|
|
51 Use the ``--port`` option to tell ``launch`` to listen on another port, e.g.::
|
|
52
|
|
53 $ launch --port=8000
|
|
54
|
|
55 When you have a working test server, point your browser at
|
|
56 ``http://localhost:8080/hello.pspx``
|
|
57 and you should see something like the following:
|
|
58
|
|
59 .. image:: hello1.png
|
|
60
|
|
61 What just happened?
|
|
62
|
|
63 #. TinCan found the ``hello.pspx`` file.
|
|
64
|
|
65 #. In that file, it found no special header directives. It also found no ``hello.py`` file.
|
|
66
|
|
67 #. So TinCan fell back to its default behavior, and created an instance of the ``tincan.Page`` class (the base class of all code-behind logic), and associated the template code in ``hello.pspx`` with it.
|
|
68
|
|
69 #. The ``${page.request.environ['PATH_INFO']}`` expression references objects found in that standard page object to return the part of the HTTP request path that is being interpreted as a route within the webapp itself.
|
|
70
|
|
71 ==============================
|
|
72 Adding Some Content of Our Own
|
|
73 ==============================
|
|
74
|
|
75 Use Control-C to force the test server to quit, then add two more files. First,
|
|
76 a template, ``hello2.pspx``::
|
|
77
|
|
78 <!DOCTYPE html>
|
|
79 <html>
|
|
80 <head>
|
|
81 <title>Hello</title>
|
|
82 </head>
|
|
83 <body>
|
|
84 <h1>Hello Again, World</h1>
|
|
85 <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
|
|
86 <p>The current time on the server is ${time}.</p>
|
|
87 </body>
|
|
88 </html>
|
|
89
|
|
90 Next, some code-behind in ``hello2.py``::
|
|
91
|
|
92 import time
|
|
93 import tincan
|
|
94
|
|
95 class Hello2(tincan.Page):
|
|
96 def handle(self):
|
|
97 self.time = time.ctime()
|
|
98
|
|
99 This time, when you launch the test server, you should notice a second route
|
|
100 being created::
|
|
101
|
|
102 $ ./launch demo
|
|
103 adding route: /hello.pspx (GET)
|
|
104 adding route: /hello2.pspx (GET)
|
|
105 Bottle v0.12.16 server starting up (using WSGIRefServer())...
|
|
106 Listening on http://localhost:8080/
|
|
107 Hit Ctrl-C to quit.
|
|
108
|
|
109 When you visit the new page, you should see something like this:
|
|
110
|
|
111 .. image:: hello2.png
|
|
112
|
|
113 What new happened this time?
|
|
114
|
|
115 #. You created a code-behind file with the same name as its associated template (only the extensions differ).
|
|
116
|
|
117 #. Because the file names match, TinCan deduced that the two files were related, one containing the code-behind for the associated template.
|
|
118
|
|
119 #. TinCan looked in the code-behind file for a something subclassing ``tincan.Page``, and created an instance of that class, complete with the standard ``request`` and ``response`` instance variables.
|
|
120
|
|
121 #. It then called the ``handle(self)`` method of that class, which defined yet another instance variable.
|
|
122
|
|
123 #. Because that instance variable *did not* start with an underscore, TinCan considered it to be exportable, and exported it as a template variable.
|
|
124
|
|
125 Suppose you had written the following code-behind instead::
|
|
126
|
|
127 from time import ctime
|
|
128 from tincan import Page
|
|
129
|
|
130 class Hello2(Page):
|
|
131 def handle(self):
|
|
132 self.time = ctime()
|
|
133
|
|
134 Every class is a subclass of itself, so what stops TinCan from getting all confused now that there are two identifiers in this module, ``Page`` and ``Hello2``, both of which are subclasses of ``tincan.Page``? For that matter, what if you had defined your own subclass of ``tincan.Page`` and subclassed it further, e.g.::
|
|
135
|
|
136 from time import ctime
|
|
137 from mymodule import MyPage
|
|
138
|
|
139 class Hello2(MyPage):
|
|
140 def handle(self):
|
|
141 self.time = ctime()
|
|
142
|
|
143 The answer is that the code would still have worked (you might want to try the first example above just to prove it). When deciding what class to use, TinCan looks for *the deepest subclass of tincan.Page it can find* in the code-behind. So in a code-behind file that contains both a reference to ``tincan.Page`` itself, and a subclass of ``tincan.Page``, the subclass will always "win." Likewise, a subclass of a subclass will always "win" over a direct subclass of the parent class.
|
|
144
|
|
145 A code-behind file need not share the same file name as its associated
|
|
146 template. If you use the ``#python`` header directive, you can tell TinCan
|
|
147 exactly which code-behind file to use. For example::
|
|
148
|
|
149 #python other.py
|
|
150 <!DOCTYPE html>
|
|
151 <html>
|
|
152 <head>
|
|
153 <title>Hello</title>
|
|
154 </head>
|
|
155 <body>
|
|
156 <h1>Hello Again, World</h1>
|
|
157 <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
|
|
158 <p>The current time on the server is ${time}.</p>
|
|
159 </body>
|
|
160 </html>
|
|
161
|
|
162 What happens if you edit ``hello2.pspx`` to look like the above but *do not* rename ``hello2.py`` to ``other.py``? What happens if you make ``hello2.pspx`` run with ``hello.py`` (and vice versa)? Try it and see!
|
|
163
|
|
164 \(Note that it is generally not a good idea to gratuitously name things inconsistently, as it makes it hard for other people — or even you, at a later time — to easily figure out what is going on.\)
|
|
165
|
|
166 ===========
|
|
167 Error Pages
|
|
168 ===========
|
|
169
|
|
170 If you tried the exercises in the paragraph immediately above, some of them let you see what happens when something goes wrong in TinCan. What if that standard error page is not to your liking, and you want to display something customized? TinCan lets you do that. Create a file called ``500.pspx``::
|
|
171
|
|
172 #errors 500
|
|
173 <!DOCTYPE html>
|
|
174 <html>
|
|
175 <head>
|
|
176 <title>${error.status_line}</title>
|
|
177 </head>
|
|
178 <body>
|
|
179 <h1>${error.status_line}</h1>
|
|
180 <p>How embarrassing! It seems there was an internal error serving
|
|
181 <kbd>${request.url}</kbd></p>
|
|
182 </body>
|
|
183 </html>
|
|
184
|
|
185 Now when you visit a page that doesn't work due to server-side errors (such as a template referencing an undefined variable), you'll see something like:
|
|
186
|
|
187 .. image:: 500.png
|
|
188
|
|
189 Error pages work a little differently from normal pages. They are based on subclasses of ``tincan.ErrorPage``. You get two standard template variables "for free:" ``error``, containing an instance of a ``bottle.HTTPError`` object pertaining to the error, and ``request``, a ``bottle.HTTPRequest`` object pertaining to the request that triggered the error.
|
|
190
|
|
191 It is not as common for error pages to have code-behind as it is for normal, non-error pages, but it is possible and allowed. As alluded to above, the class to subclass is ``tincan.ErrorPage``; such classes have a ``handle()`` method which may define instance variables which get exported to the associated template using the same basic rules as for normal pages.
|
|
192
|
|
193 If an error page itself causes an error, TinCan (the underlying Bottle framework, actually) will ignore it and display a fallback error page. This is to avoid triggering an infinite loop.
|
|
194
|
|
195 ====================================
|
|
196 Index Pages and Server-Side Forwards
|
|
197 ====================================
|
|
198
|
|
199 Remove any ``#python`` header directives you added to ``hello.pspx`` and ``hello2.pspx`` during your previous experiments and create ``index.pspx``::
|
|
200
|
|
201 #forward hello.pspx
|
|
202
|
|
203 That's it, just one line! When you launch the modified webapp, you should notice a couple new routes being created::
|
|
204
|
|
205 $ launch demo
|
|
206 adding route: /hello.pspx (GET)
|
|
207 adding route: /hello2.pspx (GET)
|
|
208 adding route: /index.pspx (GET)
|
|
209 adding route: / (GET)
|
|
210 Bottle v0.12.16 server starting up (using WSGIRefServer())...
|
|
211 Listening on http://localhost:8080/
|
|
212 Hit Ctrl-C to quit.
|
|
213
|
|
214 And if you navigate to ``http://localhost:8080/``, you should see our old friend the first page we created in this tutorial.
|
|
215
|
|
216 Just like how a web server recognizes ``index.html`` as special, and routes requests for its containing directory to it, TinCan recognizes ``index.pspx`` as special.
|
|
217
|
|
218 ``#forward`` creates what is known as a *server-side forward*. Unlike with an HTTP forward, all the action happens on the server side. The client never sees any intermediate 3xx response and never has to make a second HTTP request to resolve the URL. Any TinCan template file containing a ``#forward`` header directive will have *everything else in it* ignored, and otherwise act as if it were an exact copy of the route referenced in the ``#forward`` header.
|
|
219
|
|
220 It is permitted for a page to ``#forward`` to another ``#forward`` page (but you probably should think twice before doing this). ``#forward`` loops are not permitted and attempts to create them will be rejected and cause an error message at launch time.
|
|
221
|
|
222 =====================================
|
|
223 Form Data and Requests Other Than GET
|
|
224 =====================================
|
|
225
|
|
226 As a final example, here's a sample page, ``name.pspx``, that processes form data via a POST request, and which uses some `Chameleon TAL <https://chameleon.readthedocs.io/en/latest/reference.html#basics-tal>`_ to render and style the response page, call it::
|
|
227
|
|
228 #rem A page that accepts both GET and POST requests
|
|
229 #methods GET POST
|
|
230 <!DOCTYPE html>
|
|
231 <html>
|
|
232 <head>
|
|
233 <title>Form Test</title>
|
|
234 </head>
|
|
235 <body>
|
|
236 <h1>Form Test</h1>
|
|
237 <h2>Enter Your Name Below</h2>
|
|
238 <form method="post">
|
|
239 <input type="text" name="name"/>
|
|
240 <input type="submit" value="Submit"/>
|
|
241 </form>
|
|
242 <if tal:condition="message is not None" tal:omit-tag="True">
|
|
243 <h2 tal:content="message.subject"></h2>
|
|
244 <p tal:attributes="style message.style" tal:content="message.body"></p>
|
|
245 </if>
|
|
246 </body>
|
|
247 </html>
|
|
248
|
|
249 Here's the code-behind for that page::
|
|
250
|
|
251 from tincan import Page
|
|
252 from jsdict import JSDict
|
|
253
|
|
254 class Name(Page):
|
|
255 def handle(self):
|
|
256 self._error_message = JSDict({"subject": "Error", "style": "color: red;"})
|
|
257 if "name" not in self.request.forms:
|
|
258 if self.request.method == "GET":
|
|
259 self.message = None
|
|
260 else:
|
|
261 self.message = self.error("This should not happen!")
|
|
262 else:
|
|
263 name = self.request.forms["name"]
|
|
264 if name.strip() == "":
|
|
265 self.message = self.error("Please enter your name above.")
|
|
266 else:
|
|
267 self.message = JSDict({
|
|
268 "subject": "Hello, {0}".format(name.split()[0]),
|
|
269 "style": None,
|
|
270 "body": "Pleased to meet you!"
|
|
271 })
|
|
272
|
|
273 def error(self, message):
|
|
274 self._error_message.body = message
|
|
275 return self._error_message
|
|
276
|
|
277 This example requires you to create a third file, in ``WEB-INF/lib/jsdict.py``::
|
|
278
|
|
279 class JSDict(dict):
|
|
280 def __getattr__(self, name):
|
|
281 return self[name]
|
|
282
|
|
283 def __setattr__(self, name, value):
|
|
284 self[name] = value
|
|
285
|
|
286 def __delattr__(self, name):
|
|
287 del self[name]
|
|
288
|
|
289 ``WEB-INF/lib`` is the directory where webapp-specific library routines live. It gets added to ``sys.path`` automatically when your webapp is launched.
|
|
290
|
|
291 This example introduces two new header directives, ``#rem`` and ``#methods``. The former introduces a remark (comment); the latter specifies the request methods that this page will respond to. Not specifying a ``#methods`` line is equivalent to specifying ``#methods GET``. When you launch the webapp after adding this new page, you shound notice TinCan announce that it is creating a route that supports both GET and POST requests for it::
|
|
292
|
|
293 $ launch demo
|
|
294 adding route: /hello.pspx (GET)
|
|
295 adding route: /hello2.pspx (GET)
|
|
296 adding route: /index.pspx (GET)
|
|
297 adding route: / (GET)
|
|
298 adding route: /name.pspx (GET,POST)
|
|
299 Bottle v0.12.16 server starting up (using WSGIRefServer())...
|
|
300 Listening on http://localhost:8080/
|
|
301 Hit Ctrl-C to quit.
|