1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """Common code for implementing web user interfaces."""
17
18
19
20
21
22 import BaseHTTPServer
23 import cgi
24 import diagnostic
25 import errno
26 import htmlentitydefs
27 import os
28 import os.path
29 import common
30 import qm.platform
31 import qm.user
32 import re
33 import SimpleHTTPServer
34 import SocketServer
35 import socket
36 import string
37 import structured_text
38 import sys
39 import temporary_directory
40 import time
41 import traceback
42 import types
43 import urllib
44 import user
45 import random
46
47 import qm.external.DocumentTemplate as DocumentTemplate
48 sys.path.insert(1, os.path.dirname(DocumentTemplate.__file__))
49
50
51
52
53
54 session_id_field = "session"
55 """The name of the form field used to store the session ID."""
56
57
58
59
60
63
64
65
68
69
70
73
74
75
78
79
80
81
82
83
84
85
87 """Base class for classes to generate web pages from DTML.
88
89 The 'DtmlPage' object is used as the variable context when
90 generating HTML from DTML. Attributes and methods are available as
91 variables in DTML expressions.
92
93 This base class contains common variables and functions that are
94 available when generating all DTML files.
95
96 To generate HTML from a DTML template, instantiate a 'DtmlPage'
97 object, passing the name of the DTML template file to the
98 initializer function (or create a subclass which does this
99 automatically). Additional named attributes specified in the
100 initializer functions are set as attributes of the 'DtmlPage'
101 object, and thus available as variables in DTML Python expressions.
102
103 To generate HTML from the template, use the '__call__' method,
104 passing a 'WebRequest' object representing the request in response
105 to which the HTML page is being generated. The request set as the
106 'request' attribute of the 'DtmlPage' object. The 'WebRequest'
107 object may be omitted, if the generated HTML page is generic and
108 requires no information specific to the request or web session; in
109 this case, an empty request object is used.
110
111 This class also has an attribute, 'default_class', which is the
112 default 'DtmlPage' subclass to use when generating HTML. By
113 default, it is initialized to 'DtmlPage' itself, but applications
114 may derive a 'DtmlPage' subclass and point 'default_class' to it to
115 obtain customized versions of standard pages."""
116
117 html_stylesheet = "/stylesheets/qm.css"
118 """The URL for the cascading stylesheet to use with generated pages."""
119
120 common_javascript = "/common.js"
121
122 qm_bug_system_url = "mailto:qmtest@codesourcery.com"
123 """The public URL for the bug tracking system for the QM tools."""
124
125
126 - def __init__(self, dtml_template, **attributes):
127 """Create a new page.
128
129 'dtml_template' -- The file name of the DTML template from which
130 the page is generated. The file is assumed to reside in the
131 'dtml' subdirectory of the configured share directory.
132
133 '**attributes' -- Additional attributes to include in the
134 variable context."""
135
136 self.__dtml_template = dtml_template
137 for key, value in attributes.items():
138 setattr(self, key, value)
139
140
141 - def __call__(self, request=None):
142 """Generate an HTML page from the DTML template.
143
144 'request' -- A 'WebRequest' object containing a page request in
145 response to which an HTML page is being generated. Session
146 information from the request may be used when generating the
147 page. The request may be 'None', if none is available.
148
149 returns -- The generated HTML text."""
150
151
152 if request is None:
153 request = WebRequest("?")
154 self.request = request
155
156
157 template_path = os.path.join(qm.get_share_directory(), "dtml",
158 self.__dtml_template)
159
160 html_file = DocumentTemplate.HTMLFile(template_path)
161 return html_file(self)
162
163
164 - def GetProgramName(self):
165 """Return the name of this application program."""
166
167 return common.program_name
168
169
170 - def GetMainPageUrl(self):
171 """Return the URL for the main page."""
172
173 return "/"
174
175
176 - def WebRequest(self, script_url, **fields):
177 """Convenience constructor for 'WebRequest' objects.
178
179 Constructs a 'WebRequest' using the specified 'script_url' and
180 'fields', using the request associated with this object as the
181 base request."""
182
183 return apply(WebRequest, (script_url, self.request), fields)
184
185
187 """Return the XML header for the document."""
188
189 return \
190 '''<?xml version="1.0" encoding="iso-8859-1"?>
191 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
192 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
193 <html xmlns="http://www.w3.org/1999/xhtml">'''
194
195
197 """Return the header for an HTML document.
198
199 'description' -- A string describing this page.
200
201 'headers' -- Any additional HTML headers to place in the
202 '<head>' section of the HTML document."""
203
204 return \
205 '''<head>
206 <meta http-equiv="Content-Type"
207 content="text/html; charset=iso-8859-1"/>
208 %s
209 <meta http-equiv="Content-Style-Type"
210 content="text/css"/>
211 <link rel="stylesheet"
212 type="text/css"
213 href="%s"/>
214 <meta name="Generator"
215 content="%s"/>
216 <title>%s: %s</title>
217 </head>
218 ''' % (headers, self.html_stylesheet, self.GetProgramName(),
219 self.GetProgramName(), description)
220
221
222 - def GenerateStartBody(self, decorations=1):
223 """Return markup to start the body of the HTML document."""
224
225 return "<body>"
226
227
228 - def GenerateEndBody(self, decorations=1):
229 """Return markup to end the body of the HTML document."""
230
231 result = """
232 <br /><br />
233 """
234
235 return result + self.GenerateStartScript(self.common_javascript) \
236 + self.GenerateEndScript() + "</body>"
237
238
240 """Return the HTML for beginning a script.
241
242 'uri' -- If not None, a string giving the URI of the script.
243
244 returns -- A string consisting of HTML for beginning an
245 embedded script.
246
247 'GenerateEndScript' must be called later to terminate the script."""
248
249
250
251
252
253 result = '<script language="javascript" type="text/javascript"'
254 if uri is not None:
255 result = result + ' src="%s"' % uri
256 result = result + '>'
257
258 return result
259
260
262 """Return the HTML for ending an embedded script.
263
264 returns -- A string consisting of HTML for ending an
265 embedded script."""
266
267 return '</script>'
268
269
271 if redirect_request is None:
272
273 redirect_request = self.request
274 request = redirect_request.copy("login")
275 request["_redirect_url"] = redirect_request.GetUrl()
276
277
278 form = request.AsForm(method="post", name="login_form")
279 form = form + \
280 '''
281 <table cellpadding="0" cellspacing="0">
282 <tr><td>User name:</td></tr>
283 <tr><td>
284 <input type="text"
285 size="16"
286 name="_login_user_name"
287 value="%s"/>
288 </td></tr>
289 <tr><td>Password:</td></tr>
290 <tr><td>
291 <input type="password"
292 size="16"
293 name="_login_password"/>
294 </td></tr>
295 <tr><td>
296 <input type="button"
297 value=" Log In "
298 onclick="document.login_form.submit();"/>
299 </td></tr>
300 </table>
301 </form>
302 ''' % default_user_id
303 return form
304
305
307 """Generate HTML for a button to load a URL.
308
309 'title' -- The button title.
310
311 'script_url' -- The URL of the script.
312
313 'fields' -- Additional fields to add to the script request.
314
315 'css_class' -- The CSS class to use for the button, or 'None'.
316
317 The resulting HTML must be included in a form."""
318
319 request = apply(WebRequest, [script_url, self.request], fields)
320 return make_button_for_request(title, request, css_class)
321
322
323 - def MakeImageUrl(self, image):
324 """Generate a URL for an image."""
325
326 return "/images/%s" % image
327
328
329 - def MakeSpacer(self, width=1, height=1):
330 """Generate a spacer.
331
332 'width' -- The width of the spacer, in pixels.
333
334 'height' -- The height of the spacer, in pixels.
335
336 returns -- A transparent image of the requested size."""
337
338
339
340 return '<img border="0" width="%d" height="%d" src="%s"/>' \
341 % (width, height, self.MakeImageUrl("clear.gif"))
342
343
344 - def MakeRule(self, color="black"):
345 """Generate a plain horizontal rule."""
346
347 return '''
348 <table border="0" cellpadding="0" cellspacing="0" width="100%%">
349 <tr bgcolor="%s"><td>%s</td></tr>
350 </table>
351 ''' % (color, self.MakeSpacer())
352
353
354 - def UserIsInGroup(self, group_id):
355 """Return true if the user is a member of group 'group_id'.
356
357 Checks the group membership of the user associated with the
358 current session.
359
360 If there is no group named 'group_id' in the user database,
361 returns a false result."""
362
363 user_id = self.request.GetSession().GetUserId()
364 try:
365 group = user.database.GetGroup(group_id)
366 except KeyError:
367
368 return 0
369 else:
370 return user_id in group
371
372
373
374 DtmlPage.default_class = DtmlPage
375 """Set the default DtmlPage implementation to the base class."""
376
377 DtmlPage.web = sys.modules[DtmlPage.__module__]
378 """Make the functions in this module accessible."""
379
380
381
383 """Exception signalling an HTTP redirect response.
384
385 A script registered with a 'WebServer' instance can raise this
386 exception instead of returning HTML source text, to indicate that
387 the server should send an HTTP redirect (code 302) response to the
388 client instead of the usual code 202 response.
389
390 The exception argument is the URL of the redirect target. The
391 'request' attribute contains a 'WebRequest' for the redirect
392 target."""
393
394 - def __init__(self, redirect_target_request):
395 """Construct a redirection exception.
396
397 'redirect_target_request' -- The 'WebRequest' to which to
398 redirect the client."""
399
400
401 Exception.__init__(self, redirect_target_request.AsUrl())
402
403 self.request = redirect_target_request
404
405
406
408 """Handler for HTTP requests.
409
410 This class groups callback functions that are invoked in response
411 to HTTP requests by 'WebServer'.
412
413 Don't define '__init__' or store any persistent information in
414 this class or subclasses; a new instance is created for each
415 request. Instead, store the information in the server instance,
416 available through the 'server' attribute."""
417
418
419
420 SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map.update(
421 { '.css' : 'text/css',
422 '.js' : 'text/javascript' }
423 )
424
436
438 """Process HTTP POST requests."""
439
440
441 if self.headers.typeheader is None:
442 content_type_header = self.headers.type
443 else:
444 content_type_header = self.headers.typeheader
445 content_type, params = cgi.parse_header(content_type_header)
446
447 if content_type == "multipart/form-data":
448
449 fields = cgi.parse_multipart(self.rfile, params)
450
451
452 for name, value in fields.items():
453 if len(value) == 1:
454 fields[name] = value[0]
455
456
457 script_url, url_fields = parse_url_query(self.path)
458
459 fields.update(url_fields)
460
461 request = apply(WebRequest, (script_url, ), fields)
462
463 request.client_address = self.client_address[0]
464 self.__HandleRequest(request)
465 else:
466 self.send_response(400,
467 "Unexpected request (POST of %s)."
468 % content_type)
469
470
472 try:
473
474
475 try:
476 script_output = self.server.ProcessScript(request)
477 except NoSessionError, msg:
478 script_output = self.server.HandleNoSessionError(request, msg)
479 except InvalidSessionError, msg:
480 script_output = generate_login_form(request, msg)
481 except HttpRedirect, redirection:
482
483
484 self.send_response(302)
485 self.send_header("Location", str(redirection))
486 self.end_headers()
487 return
488 except SystemExit:
489 self.server.RequestShutdown()
490 script_output = ("<html><b>%s shutdown.</b></html>"
491 % qm.common.program_name)
492 except:
493
494
495 script_output = format_exception(sys.exc_info())
496
497 if isinstance(script_output, types.StringType):
498
499
500 mime_type = "text/html"
501 data = script_output
502 elif isinstance(script_output, types.TupleType):
503
504
505
506 mime_type, data = script_output
507 else:
508 raise ValueError
509 self.send_response(200)
510 self.send_header("Content-Type", mime_type)
511 self.send_header("Content-Length", len(data))
512
513
514
515 self.send_header("Cache-Control", "no-cache")
516 self.send_header("Pragma", "no-cache")
517 self.end_headers()
518 try:
519 self.wfile.write(data)
520 except IOError:
521
522
523
524 pass
525
526
528
529
530 if len(request.keys()) > 0:
531 self.send_error(400, "Unexpected request.")
532 return
533
534 try:
535 file = open(path, "rb")
536 except IOError:
537
538 self.send_error(404, "File not found.")
539 return
540
541 self.send_response(200)
542 self.send_header("Content-Type", self.guess_type(path))
543 self.send_header("Cache-Control", "public")
544 self.end_headers()
545 self.copyfile(file, self.wfile)
546
547
548 - def __HandlePageCacheRequest(self, request):
549 """Process a retrieval request from the global page cache."""
550
551
552 page = self.server.GetCachedPage(request)
553
554 self.send_response(200)
555 self.send_header("Content-Type", "text/html")
556 self.send_header("Content-Length", str(len(page)))
557 self.send_header("Cache-Control", "public")
558 self.end_headers()
559 self.wfile.write(page)
560
561
563 """Process a retrieval request from the session page cache."""
564
565
566 session_id = request.GetSessionId()
567 if session_id is None:
568
569
570 self.send_error(400, "Missing session ID.")
571 return
572
573 page = self.server.GetCachedPage(request, session_id)
574
575 self.send_response(200)
576 self.send_header("Content-Type", "text/html")
577 self.send_header("Content-Length", str(len(page)))
578 self.send_header("Cache-Control", "private")
579 self.end_headers()
580 self.wfile.write(page)
581
582
609
610
612 """Log a message; overrides 'BaseHTTPRequestHandler.log_message'."""
613
614
615 message = "%s - - [%s] %s\n" \
616 % (self.address_string(),
617 self.log_date_time_string(),
618 format%args)
619 self.server.LogMessage(message)
620
621
622
624 """Workaround for problems in 'BaseHTTPServer.HTTPServer'.
625
626 The Python 1.5.2 library's implementation of
627 'BaseHTTPServer.HTTPServer.server_bind' seems to have difficulties
628 when the local host address cannot be resolved by 'gethostbyaddr'.
629 This may happen for a variety of reasons, such as reverse DNS
630 misconfiguration. This subclass fixes that problem."""
631
633 """Override 'server_bind' to store the server name."""
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649 SocketServer.TCPServer.server_bind(self)
650 host, port = self.socket.getsockname()
651
652
653
654
655 if not host or host == '0.0.0.0':
656 host = socket.gethostname()
657
658
659 try:
660 hostname, hostnames, hostaddrs = socket.gethostbyaddr(host)
661 if '.' not in hostname:
662 for host in hostnames:
663 if '.' in host:
664 hostname = host
665 break
666 except socket.error:
667
668 hostname = qm.platform.get_host_name()
669
670 self.server_name = hostname
671 self.server_port = port
672
673
674
676 """A web server that serves ordinary files and dynamic content.
677
678 To configure the server to serve ordinary files, register the
679 directories containing those files with
680 'RegisterPathTranslations'. An arbitrary number of directories
681 may be specified, and all files in each directory and under it
682 are made available.
683
684 To congifure the server to serve dynamic content, register dynamic
685 URLs with 'RegisterScript'. A request matching the URL exactly
686 will cause the server to invoke the provided function.
687
688 The web server resolves request URLs in a two-step process.
689
690 1. The server checks if the URL matches exactly a script URL. If
691 a match is found, the corresponding function is invoked, and
692 its return value is sent to the client.
693
694 2. The server checks whether any registered path translation is a
695 prefix of the reqest URL. If it is, the path is translated
696 into a file system path, and the corresponding file is
697 returned.
698
699 The server also provides a rudimentary manual caching mechanism for
700 generated pages. The application may insert a generated page into
701 the page cache, if it is expected not to change. The application
702 can use this mechanism:
703
704 - to supress duplicate generation of the same page,
705
706 - or to pre-generate a page that may be requested later. This is
707 particularly handy if generating the page requires state
708 information that would be difficult to reconstruct later.
709
710 Pages may be shared across sessions, or may be specific to a
711 particular session."""
712
713
714 - def __init__(self,
715 port,
716 address="",
717 log_file=sys.stderr):
755
756
757
758
759
760
762 """Register a dynamic URL.
763
764 'script_path' -- The URL for this script. A request must
765 match this path exactly.
766
767 'script' -- A callable to invoke to generate the page
768 content.
769
770 If you register
771
772 web_server.RegisterScript('/cgi-bin/myscript', make_page)
773
774 then the URL 'http://my.server.com/cgi-bin/myscript' will
775 respond with the output of calling 'make_page'.
776
777 The script is passed a single argument, a 'WebRequest'
778 instance. It returns the HTML source, as a string, of the
779 page it generates. If it returns a tuple instead, the first
780 element is taken to be a MIME type and the second is the data.
781
782 The script may instead raise an 'HttpRedirect' instance,
783 indicating an HTTP redirect response should be sent to the
784 client."""
785
786 self.__scripts[script_path] = script
787
788
790 """Register a path translation.
791
792 'url_path' -- The path in URL-space to map from. URLs of
793 which 'url_path' is a prefix can be translated.
794
795 'file_path' -- The file system path corresponding to
796 'url_path'.
797
798 For example, if you register
799
800 web_server.RegisterPathTranslation('/images', '/path/to/pictures')
801
802 the URL 'http://my.server.com/images/big/tree.gif' will be
803 mapped to the file path '/path/to/pictures/big/tree.gif'."""
804
805 self.__translations[url_path] = file_path
806
807
809 """Return a true value if 'request' corresponds to a script."""
810
811 return self.__scripts.has_key(request.GetUrl())
812
813
815 """Process 'request' as a script.
816
817 'request' -- A 'WebRequest' object.
818
819 returns -- The output of the script."""
820
821 return self.__scripts[request.GetUrl()](request)
822
823
825 """Translate the URL in 'request' to a file system path.
826
827 'request' -- A 'WebRequest' object.
828
829 returns -- A path to the corresponding file, or 'None' if the
830 request URL didn't match any translations."""
831
832 path = request.GetUrl()
833
834 for url_path, file_path in self.__translations.items():
835
836 if path[:len(url_path)] == url_path:
837
838 sub_path = path[len(url_path):]
839
840 if os.path.isabs(sub_path):
841 sub_path = sub_path[1:]
842
843 if sub_path:
844 file_path = os.path.join(file_path, sub_path)
845 return file_path
846
847 return None
848
849
851 """Bind the server to the specified address and port.
852
853 Does not start serving."""
854
855
856
857 try:
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877 self.RequestHandlerClass = WebRequestHandler
878 self.socket = socket.socket(self.address_family,
879 self.socket_type)
880 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
881 self.server_address = (self.__address, self.__port)
882 self.server_bind()
883 self.server_activate()
884 except socket.error, error:
885 error_number, message = error
886 if error_number == errno.EADDRINUSE:
887
888 if self.__address == "":
889 address = "port %d" % self.__port
890 else:
891 address = "%s:%d" % (self.__address, self.__port)
892 raise AddressInUseError, address
893 elif error_number == errno.EACCES:
894
895 raise PrivilegedPortError, "port %d" % self.__port
896 else:
897
898 raise
899
900
902 """Start the web server.
903
904 preconditions -- The server must be bound."""
905
906 while not self.__shutdown_requested:
907 self.handle_request()
908
909
911 """Shut the server down after processing the current request."""
912
913 self.__shutdown_requested = 1
914
915
917 """Log a message."""
918
919 if self.__log_file is not None:
920 self.__log_file.write(message)
921 self.__log_file.flush()
922
923
925 """Return the host address on which this server is running.
926
927 returns -- A pair '(hostname, port)'."""
928
929 return (self.server_name, self.server_port)
930
931
933 """Return the 'AttachmentStore' used for new 'Attachment's.
934
935 returns -- The 'AttachmentStore' used for new 'Attachment's."""
936
937 return self.__temporary_store
938
939
968
969
971 """Generate JavaScript for a confirmation dialog box.
972
973 'url' -- The location in the main browser window is set to the URL
974 if the user confirms the action.
975
976 See 'make_popup_dialog_script' for a description of 'function_name'
977 and 'message' and information on how to use the return value."""
978
979
980
981 open_script = "window.opener.document.location = %s;" \
982 % make_javascript_string(url)
983
984 buttons = [
985 ( "Yes", open_script ),
986 ( "No", None ),
987 ]
988 return self.MakePopupDialog(message, buttons, title="Confirm")
989
990
992 """Generate JavaScript to show a popup dialog box.
993
994 The popup dialog box displays a message and one or more buttons.
995 Each button can have a JavaScript statement (or statements)
996 associated with it; if the button is clicked, the statement is
997 invoked. After any button is clicked, the popup window is closed as
998 well.
999
1000 'message' -- HTML source of the message to display in the popup
1001 window.
1002
1003 'buttons' -- A sequence of button specifications. Each is a pair
1004 '(caption, script)'. 'caption' is the button caption. 'script' is
1005 the JavaScript statement to invoke when the button is clicked, or
1006 'None'.
1007
1008 'title' -- The popup window title.
1009
1010 returns -- JavaScript statements to show the dialog box, suiteable
1011 for use as an event handler."""
1012
1013
1014 page = make_popup_page(message, buttons, title)
1015 page_url = self.CachePage(page).AsUrl()
1016
1017 return "window.open('%s', 'popup', 'width=480,height=200,resizable')" \
1018 % page_url
1019
1020
1021 - def CachePage(self, page_text, session_id=None):
1022 """Cache an HTML page.
1023
1024 'page_text' -- The text of the page.
1025
1026 'session_id' -- The session ID for this page, or 'None'.
1027
1028 returns -- A 'WebRequest' object with which the cached page can be
1029 retrieved later.
1030
1031 If 'session_id' is 'None', the page is placed in the global page
1032 cache. Otherwise, it is placed in the session page cache for that
1033 session."""
1034
1035 if session_id is None:
1036
1037 dir_path = self.__cache_path
1038 script_name = _page_cache_name
1039 else:
1040
1041
1042 dir_path = os.path.join(self.__cache_path, "sessions", session_id)
1043 script_name = _session_cache_name
1044
1045 if not os.path.isdir(dir_path):
1046 os.mkdir(dir_path, 0700)
1047
1048
1049 global _counter
1050 page_name = str(_counter)
1051 _counter = _counter + 1
1052
1053 page_file_name = os.path.join(dir_path, page_name)
1054 page_file = open(page_file_name, "w", 0600)
1055 page_file.write(page_text)
1056 page_file.close()
1057
1058
1059 request = WebRequest(script_name, page=page_name)
1060 if session_id is not None:
1061 request.SetSessionId(session_id)
1062 return request
1063
1064
1065 - def GetCachedPage(self, request, session_id=None):
1066 """Retrieve a page from the page cache.
1067
1068 'request' -- The URL requesting the page from the cache.
1069
1070 'session_id' -- The session ID for the request, or 'None'.
1071
1072 returns -- The cached page, or a placeholder page if the page was
1073 not found in the cache.
1074
1075 If 'session_id' is 'None', the page is retrieved from the global
1076 page cache. Otherwise, it is retrieved from the session page cache
1077 for that session."""
1078
1079 page_file_name = self.__GetPathForCachedPage(request, session_id)
1080 if os.path.isfile(page_file_name):
1081
1082 return open(page_file_name, "r").read()
1083 else:
1084
1085 return """
1086 <html>
1087 <body>
1088 <h3>Cache Error</h3>
1089 <p>You have requested a page that no longer is in the server's
1090 cache. The server may have been restarted, or the page may
1091 have expired. Please start over.</p>
1092 <!-- %s -->
1093 </body>
1094 </html>
1095 """ % url
1096
1097
1098 - def __GetPathForCachedPage(self, request, session_id):
1099 """Return the path for a cached page.
1100
1101 'request' -- The URL requesting the page from the cache.
1102
1103 'session_id' -- The session ID for the request, or 'None'."""
1104
1105 if session_id is None:
1106 dir_path = self.__cache_path
1107 else:
1108
1109
1110 dir_path = os.path.join(self.__cache_path, "sessions", session_id)
1111
1112 page_name = request["page"]
1113 return os.path.join(dir_path, page_name)
1114
1115
1132
1133
1135 """Handle internal errors."""
1136
1137 return DtmlPage.default_class("problems.dtml")(request)
1138
1139
1144
1145
1147 """Handle an error gracefully."""
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160 return
1161
1162
1163
1165 """An object representing a request from the web server.
1166
1167 A 'WebRequest' object behaves as a dictionary of key, value pairs
1168 representing query arguments, for instance query fields in a POST,
1169 or arguments encoded in a URL query string. It has some other
1170 methods as well."""
1171
1172 - def __init__(self, script_url, base=None, keep_fields=False, **fields):
1173 """Create a new request object.
1174
1175 'script_url' -- The URL of the script that processes this
1176 query.
1177
1178 'base' -- A request object from which the session ID will be
1179 duplicated, or 'None'.
1180
1181 'fields' -- The query arguments."""
1182
1183 self.__url = script_url
1184 self.__fields = {}
1185 if base and keep_fields:
1186 self.__fields.update(base.__fields)
1187 self.__fields.update(fields)
1188
1189 if base is not None:
1190 session = base.GetSessionId()
1191 if session is not None:
1192 self.SetSessionId(session)
1193 self.client_address = base.client_address
1194
1195
1201
1202
1204 """Return the URL of the script that processes this request."""
1205
1206 return self.__url
1207
1208
1210 """Return the name of the script that processes this request.
1211
1212 The script name is the final element of the full URL path."""
1213
1214 return string.split(self.__url, "/")[-1]
1215
1216
1218 """Set the session ID for this request to 'session_id'."""
1219
1220 self[session_id_field] = session_id
1221
1222
1224 """Return the session ID for this request.
1225
1226 returns -- A session ID, or 'None'."""
1227
1228 return self.get(session_id_field, None)
1229
1230
1232 """Return the session for this request.
1233
1234 raises -- 'NoSessionError' if no session ID is specified in the
1235 request.
1236
1237 raises -- 'InvalidSessionError' if the session ID specified in
1238 the request is invalid."""
1239
1240 session_id = self.GetSessionId()
1241 if session_id is None:
1242 raise NoSessionError, qm.error("session required")
1243 else:
1244 return get_session(self, session_id)
1245
1246
1248 """Return the URL representation of this request.
1249
1250 'fields_at_end' -- If not 'None', the name of the URL query
1251 arguments that should be placed last in the list of arugmnets
1252 (other than this, the order of query arguments is not
1253 defined)."""
1254
1255 if len(self.keys()) == 0:
1256
1257 return self.GetUrl()
1258 else:
1259
1260 return "%s?%s" % (self.GetUrl(), urllib.urlencode(self))
1261
1262
1300
1301
1302
1303
1305 return self.__fields[key]
1306
1307
1310
1311
1313 del self.__fields[key]
1314
1315
1316 - def get(self, key, default=None):
1318
1319
1321 return self.__fields.keys()
1322
1323
1326
1327
1329 return self.__fields.items()
1330
1331
1333 """Return a duplicate of this request.
1334
1335 'url' -- The URL for the request copy. If 'None', use the
1336 URL of the source.
1337
1338 '**fields' -- Additional fields to set in the copy."""
1339
1340
1341 if url is None:
1342 url = self.__url
1343
1344
1345 new_fields = self.__fields.copy()
1346 new_fields.update(fields)
1347
1348 new_request = apply(WebRequest, (url, ), new_fields)
1349
1350 if hasattr(self, "client_address"):
1351 new_request.client_address = self.client_address
1352
1353 return new_request
1354
1355
1356
1358 """A 'WebRequest' object initialized from the CGI environment."""
1359
1361 """Create a new request from the current CGI environment.
1362
1363 preconditions -- The CGI environment (environment variables
1364 etc.) must be in place."""
1365
1366 assert os.environ.has_key("GATEWAY_INTERFACE")
1367 assert os.environ["GATEWAY_INTERFACE"][:3] == "CGI"
1368
1369 self.__fields = cgi.FieldStorage()
1370
1371
1373 return os.environ["SCRIPT_NAME"]
1374
1375
1378
1379
1381 return self.__fields.keys()
1382
1383
1386
1387
1389 """Return a copy of the request.
1390
1391 The copy isn't tied to the CGI environment, so it can be
1392 modified safely."""
1393
1394 fields = {}
1395 for key in self.keys():
1396 fields[key] = self[key]
1397 return apply(WebRequest, (self.GetUrl(), ), fields)
1398
1399
1401
1402
1403 random.seed()
1404 try:
1405 import hashlib
1406 md5 = hashlib.md5()
1407 md5.update("%f" % random.random())
1408 digest = md5.digest()
1409 except:
1410 import md5
1411
1412 digest = md5.new("%f" % random.random()).digest()
1413
1414
1415
1416 digest = [hex(ord(c))[2:] for c in digest]
1417
1418 return ''.join(digest)
1419
1420
1422 """A persistent user session.
1423
1424 A 'Session' object represents an ongoing user interaction with the
1425 web server."""
1426
1427 - def __init__(self, request, user_id, expiration_timeout=21600):
1428 """Create a new session.
1429
1430 'request' -- A 'WebRequest' object in response to which this
1431 session is created.
1432
1433 'user_id' -- The ID of the user owning the session.
1434
1435 'expiration_timeout -- The expiration time, in seconds. If a
1436 session is not accessed for this duration, it is expired and no
1437 longer usable."""
1438
1439 self.__user_id = user_id
1440 self.__expiration_timeout = expiration_timeout
1441
1442 self.__client_address = request.client_address
1443
1444 self.__id = _create_session_id()
1445
1446 self.Touch()
1447
1448
1449 sessions[self.__id] = self
1450
1451
1453 """Update the last access time on the session to now."""
1454
1455 self.__last_access_time = time.time()
1456
1457
1459 """Return the session ID."""
1460
1461 return self.__id
1462
1463
1465 """Return the ID of the user who owns this session."""
1466
1467 return self.__user_id
1468
1469
1471 """Return the user record for the owning user.
1472
1473 returns -- A 'qm.user.User' object."""
1474
1475 return user.database[self.__user_id]
1476
1477
1482
1483
1485 """Return true if this session has expired."""
1486
1487 age = time.time() - self.__last_access_time
1488 return age > self.__expiration_timeout
1489
1490
1492 """Make sure the session is OK for a request.
1493
1494 'request' -- A 'WebRequest' object.
1495
1496 raises -- 'InvalidSessionError' if the session is invalid for
1497 the request."""
1498
1499
1500
1501 if self.__client_address != request.client_address:
1502 raise InvalidSessionError, qm.error("session wrong IP")
1503
1504 if self.IsExpired():
1505 raise InvalidSessionError, qm.error("session expired")
1506
1507
1508
1509
1510
1511
1512
1514 """Parse a URL-encoded query.
1515
1516 This function parses query strings encoded in URLs, such as
1517 '/script.cgi?key1=val1&key2=val2'. For this example, it would
1518 return '("/script.cgi", {"key1" : "val1", "key2" : "val2"})'
1519
1520 'url' -- The URL to parse.
1521
1522 returns -- A pair containing the the base script path and a
1523 mapping of query field names to values."""
1524
1525
1526 if "?" in url:
1527
1528
1529 script_url, query_string = string.split(url, "?", 1)
1530
1531 fields = cgi.parse_qs(query_string)
1532
1533
1534
1535
1536 for key, value_list in fields.items():
1537 if len(value_list) != 1:
1538
1539 print "WARNING: Multiple values in query."
1540 fields[key] = value_list[0]
1541 else:
1542
1543 script_url = url
1544 fields = {}
1545
1546 script_url = urllib.unquote(script_url)
1547 return (script_url, fields)
1548
1549
1551 """Generate an HTTP response consisting of HTML text.
1552
1553 'html_text' -- The HTML souce text to return.
1554
1555 'stream' -- The stream to write the response, by default
1556 'sys.stdout.'."""
1557
1558 stream.write("Content-type: text/html\n\n")
1559 stream.write(html_text)
1560
1561
1563 """Generate an HTTP response for an exception.
1564
1565 'exc_info' -- A three-element tuple containing exception info, of
1566 the form '(type, value, traceback)'. If 'None', use the exception
1567 currently being handled.
1568
1569 'stream' -- The stream to write the response, by default
1570 'sys.stdout.'."""
1571
1572 if exc_info == None:
1573 exc_info = sys.exc_info()
1574
1575 stream.write("Content-type: text/html\n\n");
1576 stream.write(format_exception(exc_info))
1577
1578
1600
1601
1602
1607
1608
1609
1610 __entity_regex = re.compile("&(\w+);")
1611
1612
1613
1614
1616 entity = match.group(1)
1617 try:
1618 return htmlentitydefs.entitydefs[entity]
1619 except KeyError:
1620 return "&%s;" % entity
1621
1622
1627
1628
1630 """Render 'text' as HTML."""
1631
1632 if text == "":
1633
1634
1635
1636 return " "
1637 else:
1638 return structured_text.to_html(text)
1639
1640
1642 """Create a request and return a URL for it.
1643
1644 'script_name' -- The script name for the request.
1645
1646 'base_request' -- If not 'None', the base request for the generated
1647 request.
1648
1649 'fields' -- Additional fields to include in the request."""
1650
1651 request = apply(WebRequest, (script_name, base_request), fields)
1652 return request.AsUrl()
1653
1654
1669
1670
1693
1694
1696 """Retrieve the session corresponding to 'session_id'.
1697
1698 'request' -- A 'WebRequest' object for which to get the session.
1699
1700 raises -- 'InvalidSessionError' if the session ID is invalid, or is
1701 invalid for this 'request'."""
1702
1703
1704 __clean_up_expired_sessions()
1705
1706 try:
1707
1708 session = sessions[session_id]
1709 except KeyError:
1710
1711 raise InvalidSessionError, qm.error("session invalid")
1712
1713 session.Validate(request)
1714
1715 session.Touch()
1716 return session
1717
1718
1720 """Remove any sessions that are expired."""
1721
1722 for session_id, session in sessions.items():
1723 if session.IsExpired():
1724 del sessions[session_id]
1725
1726
1728 """Handle a login request.
1729
1730 Authenticate the login using the user name and password stored in
1731 the '_login_user_name' and '_login_password' request fields,
1732 respectively.
1733
1734 If authentication succeeds, redirect to the URL stored in the
1735 '_redirect_url' request field by raising an 'HttpRedirect', passing
1736 all other request fields along as well.
1737
1738 If '_redirect_url' is not specified in the request, the value of
1739 'default_redirect_url' is used instead."""
1740
1741
1742
1743 redirect_url = request.get("_redirect_url", default_redirect_url)
1744
1745 try:
1746 user_id = qm.user.authenticator.AuthenticateWebRequest(request)
1747 except qm.user.AuthenticationError:
1748
1749 message = qm.error("invalid login")
1750 redirect_request = WebRequest(redirect_url)
1751 return generate_login_form(redirect_request, message)
1752 except qm.user.AccountDisabledError:
1753
1754 message = qm.error("disabled account")
1755 redirect_request = WebRequest(redirect_url)
1756 return generate_login_form(redirect_request, message)
1757
1758
1759 for session in sessions.values():
1760 if session.GetUserId() == user_id:
1761
1762
1763 del sessions[session.GetId()]
1764
1765 session = Session(request, user_id)
1766 session_id = session.GetId()
1767
1768
1769
1770 redirect_request = request.copy(redirect_url)
1771
1772
1773 del redirect_request["_login_user_name"]
1774 del redirect_request["_login_password"]
1775 if redirect_request.has_key("_redirect_url"):
1776 del redirect_request["_redirect_url"]
1777
1778 redirect_request.SetSessionId(session_id)
1779
1780 raise HttpRedirect, redirect_request
1781
1782
1784 """Handle a logout request.
1785
1786 prerequisite -- 'request' must be in a valid session, which is
1787 ended.
1788
1789 After ending the session, redirect to the URL specified by the
1790 '_redirect_url' field of 'request'. If '_redirect_url' is not
1791 specified in the request, the value of 'default_redirect_url' is
1792 used instead."""
1793
1794
1795 session_id = request.GetSessionId()
1796 del sessions[session_id]
1797
1798
1799 redirect_url = request.get("_redirect_url", default_redirect_url)
1800 redirect_request = request.copy(redirect_url)
1801 if redirect_request.has_key("_redirect_url"):
1802 del redirect_request["_redirect_url"]
1803 del redirect_request[session_id_field]
1804
1805 raise HttpRedirect, redirect_request
1806
1807
1808 -def generate_error_page(request, error_text):
1809 """Generate a page to indicate a user error.
1810
1811 'request' -- The request that was being processed when the error
1812 was encountered.
1813
1814 'error_text' -- A description of the error, as structured text.
1815
1816 returns -- The generated HTML source for the page."""
1817
1818 page = DtmlPage.default_class("error.dtml", error_text=error_text)
1819 return page(request)
1820
1821
1832
1833
1834 -def make_set_control(form_name,
1835 field_name,
1836 add_page,
1837 select_name=None,
1838 initial_elements=[],
1839 request=None,
1840 rows=6,
1841 width=200,
1842 window_width=480,
1843 window_height=240,
1844 ordered=0):
1845 """Construct a control for representing a set of items.
1846
1847 'form_name' -- The name of form in which the control is included.
1848
1849 'field_name' -- The name of the input control that contains an
1850 encoded representation of the set's elements. See
1851 'encode_set_control_contents' and 'decode_set_control_contents'.
1852
1853 'select_name' -- The name of the select control that displays the
1854 elements of the set. If 'None', a control name is generated
1855 automatically.
1856
1857 'add_page' -- The URL for a popup web page that is displayed
1858 in response to the "Add..." button.
1859
1860 'initial_elements' -- The initial elements of the set.
1861
1862 'rows' -- The number of rows for the select control.
1863
1864 'width' -- The width of the select control.
1865
1866 'window_width', 'window_height' -- The width and height of the popup
1867 window for adding a new element.
1868
1869 'ordered' -- If true, controls are included for specifying the order
1870 of elements in the set."""
1871
1872
1873 if select_name is None:
1874 select_name = "_set_" + field_name
1875
1876
1877 select = '<select name="%s" size="%d" width="%d">\n' \
1878 % (select_name, rows, width)
1879
1880 for text, value in initial_elements:
1881 select = select + \
1882 '<option value="%s">%s</option>\n' % (value, escape(text))
1883 select = select + '</select>\n'
1884
1885
1886
1887 initial_values = map(lambda x: x[1], initial_elements)
1888 initial_value = encode_set_control_contents(initial_values)
1889 contents = '<input type="hidden" name="%s" value="%s"/>' \
1890 % (field_name, initial_value)
1891
1892 buttons = []
1893
1894
1895 buttons.append(make_button_for_popup("Add...", add_page,
1896 window_width, window_height))
1897
1898 buttons.append('''
1899 <input type="button"
1900 size="12"
1901 value=" Remove "
1902 onclick="remove_from_set(document.%s.%s, document.%s.%s);"
1903 />''' % (form_name, select_name, form_name, field_name))
1904
1905 if ordered:
1906 buttons.append('''
1907 <input type="button"
1908 size="12"
1909 value=" Move Up "
1910 onclick="move_in_set(document.%s.%s, document.%s.%s, -1);"
1911 />''' % (form_name, select_name, form_name, field_name))
1912 buttons.append('''
1913 <input type="button"
1914 size="12"
1915 value=" Move Down "
1916 onclick="move_in_set(document.%s.%s, document.%s.%s, 1);"
1917 />''' % (form_name, select_name, form_name, field_name))
1918
1919
1920 return contents + '''
1921 <table border="0" cellpadding="0" cellspacing="0"><tbody>
1922 <tr valign="top">
1923 <td>
1924 %s
1925 </td>
1926 <td> </td>
1927 <td>
1928 %s
1929 </td>
1930 </tr>
1931 </tbody></table>
1932 ''' % (select, string.join(buttons, "<br />"))
1933
1934
1936 """Encode 'values' for a set control.
1937
1938 'values' -- A sequence of values of elements of the set.
1939
1940 returns -- The encoded value for the control field."""
1941
1942 return string.join(values, ",")
1943
1944
1946 """Decode the contents of a set control.
1947
1948 'content_string' -- The text of the form field containing the
1949 encoded set contents.
1950
1951 returns -- A sequence of the values of the elements of the set."""
1952
1953
1954
1955 if string.strip(content_string) == "":
1956 return []
1957 return string.split(content_string, ",")
1958
1959
1964 """Construct a control for representing a set of properties.
1965
1966 'form_name' -- The name of form in which the control is included.
1967
1968 'field_name' -- The name of the input control that contains an
1969 encoded representation of the properties. See 'encode_properties'
1970 and 'decode_properties'.
1971
1972 'properties' -- A map from property names to values of the
1973 properties to include in the control initially.
1974
1975 'select_name' -- The name of the select control that displays the
1976 elements of the set. If 'None', a control name is generated
1977 automatically."""
1978
1979
1980 if select_name is None:
1981 select_name = "_propsel_" + field_name
1982 name_control_name = "_propname_" + field_name
1983 value_control_name = "_propval_" + field_name
1984 add_change_button_name = "_propaddedit_" + field_name
1985
1986
1987 select = '''
1988 <select name="%s"
1989 size="6"
1990 width="240"
1991 onchange="property_update_selection(document.%s.%s,
1992 document.%s.%s,
1993 document.%s.%s);
1994 document.%s.%s.value = ' Change ';"
1995 >\n''' \
1996 % (select_name, form_name, select_name,
1997 form_name, name_control_name, form_name, value_control_name,
1998 form_name, add_change_button_name)
1999
2000 keys = properties.keys()
2001 keys.sort()
2002 for k in keys:
2003 select = select + \
2004 '<option value="%s=%s">%s = %s</option>\n' \
2005 % (k, properties[k], k, properties[k])
2006 select = select + '</select>\n'
2007
2008
2009
2010 initial_value = encode_properties(properties)
2011 contents = '<input type="hidden" name="%s" value="%s"/>' \
2012 % (field_name, initial_value)
2013
2014
2015 name_control = \
2016 '''<input type="text"
2017 name="%s"
2018 size="32"
2019 onkeydown="document.%s.%s.value = ' Add ';"
2020 />''' % (name_control_name, form_name, add_change_button_name)
2021
2022 value_control = '<input type="text" name="%s" size="32"/>' \
2023 % value_control_name
2024
2025 vars = { 'form' : form_name,
2026 'button' : add_change_button_name,
2027 'select' : select_name,
2028 'field' : field_name,
2029 'name' : name_control_name,
2030 'value' : value_control_name }
2031
2032
2033
2034
2035
2036 add_change_button = \
2037 '''<input type="button"
2038 name="%(button)s"
2039 size="12"
2040 value=" Add "
2041 onclick="property_add_or_change
2042 (document.%(form)s.%(select)s,
2043 document.%(form)s.%(field)s,
2044 document.%(form)s.%(name)s,
2045 document.%(form)s.%(value)s);"
2046 />''' % vars
2047
2048
2049 remove_button = \
2050 '''<input type="button"
2051 size="12"
2052 value=" Remove "
2053 onclick="property_remove(document.%(form)s.%(select)s,
2054 document.%(form)s.%(field)s,
2055 document.%(form)s.%(name)s,
2056 document.%(form)s.%(value)s,
2057 document.%(form)s.%(button)s);"
2058 />''' % vars
2059
2060
2061 return contents + '''
2062 <table border="0" cellpadding="0" cellspacing="0"><tbody>
2063 <tr valign="top">
2064 <td colspan="2" width="240">%s</td>
2065 <td> </td>
2066 <td>%s</td>
2067 </tr>
2068 <tr>
2069 <td>Name: </td>
2070 <td align="right">%s </td>
2071 <td> </td>
2072 <td>%s</td>
2073 </tr>
2074 <tr>
2075 <td>Value: </td>
2076 <td align="right">%s </td>
2077 <td> </td>
2078 <td> </td>
2079 </tr>
2080 </tbody></table>
2081 ''' % (select, remove_button, name_control, add_change_button,
2082 value_control)
2083
2084
2086 """Construct a URL-encoded representation of a set of properties.
2087
2088 'properties' -- A map from property names to values. Names must be
2089 URL-safe strings. Values are arbitrary strings.
2090
2091 returns -- A URL-encoded string representation of 'properties'.
2092
2093 This function is the inverse of 'decode_properties'."""
2094
2095
2096
2097 result = map(lambda p: "%s=%s" % (p[0], urllib.quote_plus(p[1])),
2098 properties.items())
2099
2100 return string.join(result, ",")
2101
2102
2104 """Decode a URL-encoded representation of a set of properties.
2105
2106 'properties' -- A string containing URL-encoded properties.
2107
2108 returns -- A map from names to values.
2109
2110 This function is the inverse of 'encode_properties'."""
2111
2112 properties = string.strip(properties)
2113
2114 if properties == "":
2115 return {}
2116
2117
2118 properties = string.split(properties, ",")
2119
2120 result = {}
2121 for assignment in properties:
2122
2123 name, value = string.split(assignment, "=")
2124
2125 value = urllib.unquote_plus(value)
2126
2127 result[name] = value
2128
2129 return result
2130
2131
2133 """Return 'text' represented as a JavaScript string literal."""
2134
2135 text = string.replace(text, "\\", r"\\")
2136 text = string.replace(text, "'", r"\'")
2137 text = string.replace(text, "\"", r'\"')
2138 text = string.replace(text, "\n", r"\n")
2139
2140
2141 text = string.replace(text, "<", r"\074")
2142 return "'" + text + "'"
2143
2144
2146 """Make a link to pop up help text.
2147
2148 'help_text_tag' -- A message tag for the help diagnostic.
2149
2150 'label' -- The help link label.
2151
2152 'substitutions' -- Substitutions to the help diagnostic."""
2153
2154
2155 help_text = apply(diagnostic.get_help_set().Generate,
2156 (help_text_tag, "help", None),
2157 substitutions)
2158
2159 help_text = qm.structured_text.to_html(help_text)
2160
2161 return make_help_link_html(help_text, label)
2162
2163
2165 """Make a link to pop up help text.
2166
2167 'help_text' -- HTML source for the help text.
2168
2169 'label' -- The help link label."""
2170
2171 global _counter
2172
2173
2174 help_page = DtmlPage("help.dtml", help_text=help_text)
2175
2176 help_page_string = make_javascript_string(help_page())
2177
2178
2179
2180 help_variable_name = "_help_text_%d" % _counter
2181 _counter = _counter + 1
2182
2183
2184 return \
2185 '''<a class="help-link"
2186 href="javascript: void(0)"
2187 onclick="show_help(%s);">%s</a>
2188 %s
2189 var %s = %s;
2190 %s
2191 ''' % (help_variable_name, label,
2192 help_page.GenerateStartScript(),
2193 help_variable_name, help_page_string,
2194 help_page.GenerateEndScript())
2195
2196
2198 """Generate a popup dialog box page.
2199
2200 See 'make_popup_dialog_script' for an explanation of the
2201 parameters."""
2202
2203 page = \
2204 '''<html>
2205 <head>
2206 <title>%s</title>
2207 <meta http-equiv="Content-Style-Type" content="text/css"/>
2208 <link rel="stylesheet" type="text/css" href="/stylesheets/qm.css"/>
2209 </head>
2210 <body class="popup">
2211 <table border="0" cellpadding="0" cellspacing="8" width="100%%">
2212 <tr><td>
2213 %s
2214 </td></tr>
2215 <tr><td align="right">
2216 <form name="navigation">
2217 ''' % (title, message)
2218
2219 for caption, script in buttons:
2220 page = page + '''
2221 <input type="button"
2222 value=" %s "''' % caption
2223
2224
2225 if script is None:
2226 page = page + '''
2227 onclick="window.close();"'''
2228 else:
2229 page = page + '''
2230 onclick="%s; window.close();"''' % script
2231 page = page + '''
2232 />'''
2233
2234 page = page + '''
2235 </form>
2236 </td></tr>
2237 </table>
2238 </body>
2239 </html>
2240 '''
2241 return page
2242
2243
2244 -def make_choose_control(field_name,
2245 included_label,
2246 included_items,
2247 excluded_label,
2248 excluded_items,
2249 item_to_text=str,
2250 item_to_value=str,
2251 ordered=0):
2252 """Construct HTML controls for selecting a subset.
2253
2254 The user is presented with two list boxes next to each other. The
2255 box on the left lists items included in the subset. The box on the
2256 right lists items excluded from the subset but available for
2257 inclusion. Between the boxes are buttons for adding and removing
2258 items from the subset.
2259
2260 If 'ordered' is true, buttons are also shown for reordering items in
2261 the included list.
2262
2263 'field_name' -- The name of an HTML hidden form field that will
2264 contain an encoding of the items included in the subset. The
2265 encoding consists of the values corresponding to included items, in
2266 a comma-separated list.
2267
2268 'included_label' -- HTML source for the label for the left box,
2269 which displays the included items.
2270
2271 'included_items' -- Items initially included in the subset. This is
2272 a sequence of arbitrary objects or values.
2273
2274 'excluded_label' -- HTML source for the label for the right box,
2275 which displays the items available for inclusion but not currently
2276 included.
2277
2278 'excluded_items' -- Items not initially included but available for
2279 inclusion. This is a sequence of arbitrary objects or values.
2280
2281 'item_to_text' -- A function that produces a user-visible text
2282 description of an item.
2283
2284 'item_to_value' -- A function that produces a value for an item,
2285 used as the value for an HTML option object.
2286
2287 'ordered' -- If true, additional controls are displayed to allow the
2288 user to manipulate the order of items in the included set.
2289
2290 returns -- HTML source for the items. Must be placed in a
2291 form."""
2292
2293
2294
2295 buttons = []
2296
2297 initial_value = string.join(map(item_to_value, included_items), ",")
2298
2299
2300 hidden_control = '<input type="hidden" name="%s" value="%s">' \
2301 % (field_name, initial_value)
2302
2303 included_select_name = "_inc_" + field_name
2304 excluded_select_name = "_exc_" + field_name
2305
2306
2307
2308
2309 included_select = '''
2310 <select name="%s"
2311 width="160"
2312 size="8"
2313 onchange="document.form.%s.selectedIndex = -1;">''' \
2314 % (included_select_name, excluded_select_name)
2315
2316 for item in included_items:
2317 option = '<option value="%s">%s</option>\n' \
2318 % (item_to_value(item), item_to_text(item))
2319 included_select = included_select + option
2320 included_select = included_select + '</select>\n'
2321
2322
2323
2324
2325 excluded_select = '''
2326 <select name="%s"
2327 width="160"
2328 size="8"
2329 onchange="document.form.%s.selectedIndex = -1;">''' \
2330 % (excluded_select_name, included_select_name)
2331
2332 for item in excluded_items:
2333 option = '<option value="%s">%s</option>\n' \
2334 % (item_to_value(item), item_to_text(item))
2335 excluded_select = excluded_select + option
2336 excluded_select = excluded_select + '</select>\n'
2337
2338
2339 button = '''
2340 <input type="button"
2341 value=" << Add "
2342 onclick="move_option(document.form.%s, document.form.%s);
2343 document.form.%s.value =
2344 encode_select_options(document.form.%s);" />
2345 ''' % (excluded_select_name, included_select_name,
2346 field_name, included_select_name)
2347 buttons.append(button)
2348
2349
2350 button = '''
2351 <input
2352 type="button"
2353 value=" Remove >> "
2354 onclick="move_option(document.form.%s, document.form.%s);
2355 document.form.%s.value =
2356 encode_select_options(document.form.%s);" />
2357 ''' % (included_select_name, excluded_select_name,
2358 field_name, included_select_name)
2359 buttons.append(button)
2360
2361 if ordered:
2362
2363 button = '''
2364 <input type="button"
2365 value=" Move Up "
2366 onclick="swap_option(document.form.%s, -1);
2367 document.form.%s.value =
2368 encode_select_options(document.form.%s);"/>
2369 ''' % (included_select_name, field_name, included_select_name)
2370
2371 buttons.append(button)
2372
2373
2374 button = '''
2375 <input type="button"
2376 value=" Move Down "
2377 onclick="swap_option(document.form.%s, 1);
2378 document.form.%s.value =
2379 encode_select_options(document.form.%s);"/>
2380 ''' % (included_select_name, field_name, included_select_name)
2381 buttons.append(button)
2382
2383
2384 buttons = string.join(buttons, "\n<br />\n")
2385 return '''
2386 %(hidden_control)s
2387 <table border="0" cellpadding="0" cellspacing="0">
2388 <tr valign="center">
2389 <td>
2390 %(included_label)s:
2391 <br />
2392 %(included_select)s
2393 </td>
2394 <td align="center">
2395 %(buttons)s
2396 </td>
2397 <td>
2398 %(excluded_label)s:<br />
2399 %(excluded_select)s
2400 </td>
2401 </tr>
2402 </table>
2403 ''' % locals()
2404
2405
2430
2431
2456
2457
2459 """Equivalent to the JavaScript 'escape' built-in function."""
2460
2461 text = urllib.quote(text)
2462 text = string.replace(text, ",", "%2C")
2463 return text
2464
2465
2467 """Equivalent to the JavaScript 'unescape' built-in function."""
2468
2469 return urllib.unquote(text)
2470
2471
2483
2484
2485
2486
2487
2488
2489 sessions = {}
2490 """A mapping from session IDs to 'Session' instances."""
2491
2492 _counter = 0
2493 """A counter for generating somewhat-unique names."""
2494
2495 _page_cache_name = "page-cache"
2496 """The URL prefix for the global page cache."""
2497
2498 _session_cache_name = "session-cache"
2499 """The URL prefix for the session page cache."""
2500
2501
2502
2503
2504
2505
2506
2507