comparison doc/tutorial.rst @ 60:682cd33e564c draft

Documentation (incomplete).
author David Barts <n5jrn@me.com>
date Sat, 08 Jun 2019 07:43:15 -0700
parents
children
comparison
equal deleted inserted replaced
59:60907204a265 60:682cd33e564c
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.