| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.web.test.test_web -*- | |
| 2 # | |
| 3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 4 # See LICENSE for details. | |
| 5 | |
| 6 | |
| 7 """A twisted web component framework. | |
| 8 | |
| 9 This module is DEPRECATED. | |
| 10 """ | |
| 11 | |
| 12 import warnings | |
| 13 warnings.warn("This module is deprecated, please use Woven instead.", Deprecatio
nWarning) | |
| 14 | |
| 15 # System Imports | |
| 16 import string, time, types, traceback, pprint, sys, os | |
| 17 import linecache | |
| 18 import re | |
| 19 from cStringIO import StringIO | |
| 20 | |
| 21 # Twisted Imports | |
| 22 from twisted.python import failure, log, rebuild, reflect, util | |
| 23 from twisted.internet import defer | |
| 24 from twisted.web import http | |
| 25 | |
| 26 # Sibling Imports | |
| 27 import html, resource, error | |
| 28 import util as webutil | |
| 29 | |
| 30 #backwards compatibility | |
| 31 from util import formatFailure, htmlrepr, htmlUnknown, htmlDict, htmlList,\ | |
| 32 htmlInst, htmlString, htmlReprTypes | |
| 33 | |
| 34 | |
| 35 | |
| 36 from server import NOT_DONE_YET | |
| 37 | |
| 38 True = (1==1) | |
| 39 False = not True | |
| 40 | |
| 41 | |
| 42 # magic value that sez a widget needs to take over the whole page. | |
| 43 | |
| 44 FORGET_IT = 99 | |
| 45 | |
| 46 def listify(x): | |
| 47 return [x] | |
| 48 def _ellipsize(x): | |
| 49 y = repr(x) | |
| 50 if len(y) > 1024: | |
| 51 return y[:1024]+"..." | |
| 52 return y | |
| 53 | |
| 54 | |
| 55 class Widget: | |
| 56 """A component of a web page. | |
| 57 """ | |
| 58 title = None | |
| 59 def getTitle(self, request): | |
| 60 return self.title or reflect.qual(self.__class__) | |
| 61 | |
| 62 def display(self, request): | |
| 63 """Implement me to represent your widget. | |
| 64 | |
| 65 I must return a list of strings and twisted.internet.defer.Deferred | |
| 66 instances. | |
| 67 """ | |
| 68 raise NotImplementedError("%s.display" % reflect.qual(self.__class__)) | |
| 69 | |
| 70 class StreamWidget(Widget): | |
| 71 """A 'streamable' component of a webpage. | |
| 72 """ | |
| 73 | |
| 74 def stream(self, write, request): | |
| 75 """Call 'write' multiple times with a string argument to represent this
widget. | |
| 76 """ | |
| 77 raise NotImplementedError("%s.stream" % reflect.qual(self.__class__)) | |
| 78 | |
| 79 def display(self, request): | |
| 80 """Produce a list containing a single string. | |
| 81 """ | |
| 82 l = [] | |
| 83 try: | |
| 84 result = self.stream(l.append, request) | |
| 85 if result is not None: | |
| 86 return result | |
| 87 return l | |
| 88 except: | |
| 89 return [webutil.formatFailure(failure.Failure())] | |
| 90 | |
| 91 class WidgetMixin(Widget): | |
| 92 """A mix-in wrapper for a Widget. | |
| 93 | |
| 94 This mixin can be used to wrap functionality in any other widget with a | |
| 95 method of your choosing. It is designed to be used for mix-in classes that | |
| 96 can be mixed in to Form, StreamWidget, Presentation, etc, to augment the | |
| 97 data available to the 'display' methods of those classes, usually by adding | |
| 98 it to a Session. | |
| 99 """ | |
| 100 | |
| 101 def display(self): | |
| 102 raise NotImplementedError("%s.display" % self.__class__) | |
| 103 | |
| 104 def displayMixedWidget(self, request): | |
| 105 for base in reflect.allYourBase(self.__class__): | |
| 106 if issubclass(base, Widget) and not issubclass(base, WidgetMixin): | |
| 107 return base.display(self, request) | |
| 108 | |
| 109 class Presentation(Widget): | |
| 110 """I am a widget which formats a template with interspersed python expressio
ns. | |
| 111 """ | |
| 112 template = ''' | |
| 113 Hello, %%%%world%%%%. | |
| 114 ''' | |
| 115 world = "you didn't assign to the 'template' attribute" | |
| 116 def __init__(self, template=None, filename=None): | |
| 117 if filename: | |
| 118 self.template = open(filename).read() | |
| 119 elif template: | |
| 120 self.template = template | |
| 121 self.variables = {} | |
| 122 self.tmpl = string.split(self.template, "%%%%") | |
| 123 | |
| 124 def addClassVars(self, namespace, Class): | |
| 125 for base in Class.__bases__: | |
| 126 # Traverse only superclasses that know about Presentation. | |
| 127 if issubclass(base, Presentation) and base is not Presentation: | |
| 128 self.addClassVars(namespace, base) | |
| 129 # 'lower' classes in the class heirarchy take precedence. | |
| 130 for k in Class.__dict__.keys(): | |
| 131 namespace[k] = getattr(self, k) | |
| 132 | |
| 133 def addVariables(self, namespace, request): | |
| 134 self.addClassVars(namespace, self.__class__) | |
| 135 | |
| 136 def prePresent(self, request): | |
| 137 """Perform any tasks which must be done before presenting the page. | |
| 138 """ | |
| 139 | |
| 140 def formatTraceback(self, tb): | |
| 141 return [html.PRE(tb)] | |
| 142 | |
| 143 def streamCall(self, call, *args, **kw): | |
| 144 """Utility: Call a method like StreamWidget's 'stream'. | |
| 145 """ | |
| 146 io = StringIO() | |
| 147 apply(call, (io.write,) + args, kw) | |
| 148 return io.getvalue() | |
| 149 | |
| 150 def display(self, request): | |
| 151 tm = [] | |
| 152 flip = 0 | |
| 153 namespace = {} | |
| 154 self.prePresent(request) | |
| 155 self.addVariables(namespace, request) | |
| 156 # This variable may not be obscured... | |
| 157 namespace['request'] = request | |
| 158 namespace['self'] = self | |
| 159 for elem in self.tmpl: | |
| 160 flip = not flip | |
| 161 if flip: | |
| 162 if elem: | |
| 163 tm.append(elem) | |
| 164 else: | |
| 165 try: | |
| 166 x = eval(elem, namespace, namespace) | |
| 167 except: | |
| 168 log.deferr() | |
| 169 tm.append(webutil.formatFailure(failure.Failure())) | |
| 170 else: | |
| 171 if isinstance(x, types.ListType): | |
| 172 tm.extend(x) | |
| 173 elif isinstance(x, Widget): | |
| 174 val = x.display(request) | |
| 175 if not isinstance(val, types.ListType): | |
| 176 raise Exception("%s.display did not return a list, i
t returned %s!" % (x.__class__, repr(val))) | |
| 177 tm.extend(val) | |
| 178 else: | |
| 179 # Only two allowed types here should be deferred and | |
| 180 # string. | |
| 181 tm.append(x) | |
| 182 return tm | |
| 183 | |
| 184 | |
| 185 def htmlFor_hidden(write, name, value): | |
| 186 write('<INPUT TYPE="hidden" NAME="%s" VALUE="%s" />' % (name, value)) | |
| 187 | |
| 188 def htmlFor_file(write, name, value): | |
| 189 write('<INPUT SIZE="60" TYPE="file" NAME="%s" />' % name) | |
| 190 | |
| 191 def htmlFor_string(write, name, value): | |
| 192 write('<INPUT SIZE="60" TYPE="text" NAME="%s" VALUE="%s" />' % (name, value)
) | |
| 193 | |
| 194 def htmlFor_password(write, name, value): | |
| 195 write('<INPUT SIZE="60" TYPE="password" NAME="%s" />' % name) | |
| 196 | |
| 197 def htmlFor_text(write, name, value): | |
| 198 write('<textarea COLS="60" ROWS="10" NAME="%s" WRAP="virtual">%s</textarea>'
% (name, value)) | |
| 199 | |
| 200 def htmlFor_menu(write, name, value, allowMultiple=False): | |
| 201 "Value of the format [(optionName, displayName[, selected]), ...]" | |
| 202 | |
| 203 write(' <select NAME="%s"%s>\n' % | |
| 204 (name, (allowMultiple and " multiple") or '')) | |
| 205 | |
| 206 for v in value: | |
| 207 optionName, displayName, selected = util.padTo(3, v) | |
| 208 selected = (selected and " selected") or '' | |
| 209 write(' <option VALUE="%s"%s>%s</option>\n' % | |
| 210 (optionName, selected, displayName)) | |
| 211 if not value: | |
| 212 write(' <option VALUE=""></option>\n') | |
| 213 write(" </select>\n") | |
| 214 | |
| 215 def htmlFor_multimenu(write, name, value): | |
| 216 "Value of the format [(optionName, displayName[, selected]), ...]" | |
| 217 return htmlFor_menu(write, name, value, True) | |
| 218 | |
| 219 def htmlFor_checkbox(write, name, value): | |
| 220 "A checkbox." | |
| 221 if value: | |
| 222 value = 'checked = "1"' | |
| 223 else: | |
| 224 value = '' | |
| 225 write('<INPUT TYPE="checkbox" NAME="__checkboxes__" VALUE="%s" %s />\n' % (n
ame, value)) | |
| 226 | |
| 227 def htmlFor_checkgroup(write, name, value): | |
| 228 "A check-group." | |
| 229 for optionName, displayName, checked in value: | |
| 230 checked = (checked and 'checked = "1"') or '' | |
| 231 write('<INPUT TYPE="checkbox" NAME="%s" VALUE="%s" %s />%s<br />\n' % (n
ame, optionName, checked, displayName)) | |
| 232 | |
| 233 def htmlFor_radio(write, name, value): | |
| 234 "A radio button group." | |
| 235 for optionName, displayName, checked in value: | |
| 236 checked = (checked and 'checked = "1"') or '' | |
| 237 write('<INPUT TYPE="radio" NAME="%s" VALUE="%s" %s />%s<br />\n' % (name
, optionName, checked, displayName)) | |
| 238 | |
| 239 class FormInputError(Exception): | |
| 240 pass | |
| 241 | |
| 242 class Form(Widget): | |
| 243 """I am a web form. | |
| 244 | |
| 245 In order to use me, you probably want to set self.formFields (or override | |
| 246 'getFormFields') and override 'process'. In order to demonstrate how this | |
| 247 is done, here is a small sample Form subclass:: | |
| 248 | |
| 249 | from twisted.web import widgets | |
| 250 | class HelloForm(widgets.Form): | |
| 251 | formFields = [ | |
| 252 | ['string', 'Who to greet?', 'whoToGreet', 'World', | |
| 253 | 'This is for choosing who to greet.'], | |
| 254 | ['menu', 'How to greet?', 'how', [('cheerfully', 'with a smile'
), | |
| 255 | ('sullenly', 'without enthusi
asm'), | |
| 256 | ('spontaneously', 'on the spu
r of the moment')]] | |
| 257 | 'This is for choosing how to greet them.'] | |
| 258 | def process(self, write, request, submit, whoToGreet, how): | |
| 259 | write('The web wakes up and %s says, \"Hello, %s!\"' % (how, wh
oToGreet)) | |
| 260 | |
| 261 If you load this widget, you will see that it displays a form with 2 inputs | |
| 262 derived from data in formFields. Note the argument names to 'process': | |
| 263 after 'write' and 'request', they are the same as the 3rd elements ('Input | |
| 264 Name' parameters) of the formFields list. | |
| 265 """ | |
| 266 | |
| 267 formGen = { | |
| 268 'hidden': htmlFor_hidden, | |
| 269 'file': htmlFor_file, | |
| 270 'string': htmlFor_string, | |
| 271 'int': htmlFor_string, | |
| 272 'float': htmlFor_string, | |
| 273 'text': htmlFor_text, | |
| 274 'menu': htmlFor_menu, | |
| 275 'multimenu': htmlFor_multimenu, | |
| 276 'password': htmlFor_password, | |
| 277 'checkbox': htmlFor_checkbox, | |
| 278 'checkgroup': htmlFor_checkgroup, | |
| 279 'radio': htmlFor_radio, | |
| 280 } | |
| 281 | |
| 282 formParse = { | |
| 283 'int': int, | |
| 284 'float': float, | |
| 285 } | |
| 286 | |
| 287 formFields = [ | |
| 288 ] | |
| 289 | |
| 290 # do we raise an error when we get extra args or not? | |
| 291 formAcceptExtraArgs = 0 | |
| 292 | |
| 293 def getFormFields(self, request, fieldSet = None): | |
| 294 """I return a list of lists describing this form, or a Deferred. | |
| 295 | |
| 296 This information is used both to display the form and to process it. | |
| 297 The list is in the following format:: | |
| 298 | |
| 299 | [['Input Type', 'Display Name', 'Input Name', 'Input Value', '
Description'], | |
| 300 | ['Input Type 2', 'Display Name 2', 'Input Name 2', 'Input Value 2',
'Description 2'] | |
| 301 | ...] | |
| 302 | |
| 303 Valid values for 'Input Type' are: | |
| 304 | |
| 305 - 'hidden': a hidden field that contains a string that the user won't
change | |
| 306 | |
| 307 - 'string': a short string | |
| 308 | |
| 309 - 'int': an integer, e.g. 1, 0, 25 or -23 | |
| 310 | |
| 311 - 'float': a float, e.g. 1.0, 2, -3.45, or 28.4324231 | |
| 312 | |
| 313 - 'text': a longer text field, suitable for entering paragraphs | |
| 314 | |
| 315 - 'menu': an HTML SELECT input, a list of choices | |
| 316 | |
| 317 - 'multimenu': an HTML SELECT input allowing multiple choices | |
| 318 | |
| 319 - 'checkgroup': a group of checkboxes | |
| 320 | |
| 321 - 'radio': a group of radio buttons | |
| 322 | |
| 323 - 'password': a 'string' field where the contents are not visible as t
he user types | |
| 324 | |
| 325 - 'file': a file-upload form (EXPERIMENTAL) | |
| 326 | |
| 327 'Display Name' is a descriptive string that will be used to | |
| 328 identify the field to the user. | |
| 329 | |
| 330 The 'Input Name' must be a legal Python identifier that describes both | |
| 331 the value's name on the HTML form and the name of an argument to | |
| 332 'self.process()'. | |
| 333 | |
| 334 The 'Input Value' is usually a string, but its value can depend on the | |
| 335 'Input Type'. 'int' it is an integer, 'menu' it is a list of pairs of | |
| 336 strings, representing (value, name) pairs for the menu options. Input | |
| 337 value for 'checkgroup' and 'radio' should be a list of ('inputName', | |
| 338 'Display Name', 'checked') triplets. | |
| 339 | |
| 340 The 'Description' field is an (optional) string which describes the form | |
| 341 item to the user. | |
| 342 | |
| 343 If this result is statically determined for your Form subclass, you can | |
| 344 assign it to FormSubclass.formFields; if you need to determine it | |
| 345 dynamically, you can override this method. | |
| 346 | |
| 347 Note: In many cases it is desirable to use user input for defaults in | |
| 348 the form rather than those supplied by your calculations, which is what | |
| 349 this method will do to self.formFields. If this is the case for you, | |
| 350 but you still need to dynamically calculate some fields, pass your | |
| 351 results back through this method by doing:: | |
| 352 | |
| 353 | def getFormFields(self, request): | |
| 354 | myFormFields = [self.myFieldCalculator()] | |
| 355 | return widgets.Form.getFormFields(self, request, myFormFields) | |
| 356 | |
| 357 """ | |
| 358 fields = [] | |
| 359 if fieldSet is None: | |
| 360 fieldSet = self.formFields | |
| 361 if not self.shouldProcess(request): | |
| 362 return fieldSet | |
| 363 | |
| 364 for field in fieldSet: | |
| 365 if len(field)==5: | |
| 366 inputType, displayName, inputName, inputValue, description = fie
ld | |
| 367 else: | |
| 368 inputType, displayName, inputName, inputValue = field | |
| 369 description = "" | |
| 370 | |
| 371 if inputType == 'checkbox': | |
| 372 if request.args.has_key('__checkboxes__'): | |
| 373 if inputName in request.args['__checkboxes__']: | |
| 374 inputValue = 1 | |
| 375 else: | |
| 376 inputValue = 0 | |
| 377 else: | |
| 378 inputValue = 0 | |
| 379 elif inputType in ('checkgroup', 'radio'): | |
| 380 if request.args.has_key(inputName): | |
| 381 keys = request.args[inputName] | |
| 382 else: | |
| 383 keys = [] | |
| 384 iv = inputValue | |
| 385 inputValue = [] | |
| 386 for optionName, optionDisplayName, checked in iv: | |
| 387 checked = optionName in keys | |
| 388 inputValue.append([optionName, optionDisplayName, checked]) | |
| 389 elif request.args.has_key(inputName): | |
| 390 iv = request.args[inputName][0] | |
| 391 if inputType in ['menu', 'multimenu']: | |
| 392 if iv in inputValue: | |
| 393 inputValue.remove(iv) | |
| 394 inputValue.insert(0, iv) | |
| 395 else: | |
| 396 inputValue = iv | |
| 397 fields.append([inputType, displayName, inputName, inputValue, descri
ption]) | |
| 398 return fields | |
| 399 | |
| 400 submitNames = ['Submit'] | |
| 401 actionURI = '' | |
| 402 | |
| 403 def format(self, form, write, request): | |
| 404 """I display an HTML FORM according to the result of self.getFormFields. | |
| 405 """ | |
| 406 write('<form ENCTYPE="multipart/form-data" METHOD="post" ACTION="%s">\n' | |
| 407 '<table BORDER="0">\n' % (self.actionURI or request.uri)) | |
| 408 | |
| 409 for field in form: | |
| 410 if len(field) == 5: | |
| 411 inputType, displayName, inputName, inputValue, description = fie
ld | |
| 412 else: | |
| 413 inputType, displayName, inputName, inputValue = field | |
| 414 description = "" | |
| 415 write('<tr>\n<td ALIGN="right" VALIGN="top"><B>%s</B></td>\n' | |
| 416 '<td VALIGN="%s">\n' % | |
| 417 (displayName, ((inputType == 'text') and 'top') or 'middle')) | |
| 418 self.formGen[inputType](write, inputName, inputValue) | |
| 419 write('\n<br />\n<font size="-1">%s</font></td>\n</tr>\n' % descript
ion) | |
| 420 | |
| 421 | |
| 422 write('<tr><td></td><td ALIGN="left"><hr />\n') | |
| 423 for submitName in self.submitNames: | |
| 424 write('<INPUT TYPE="submit" NAME="submit" VALUE="%s" />\n' % submitN
ame) | |
| 425 write('</td></tr>\n</table>\n' | |
| 426 '<INPUT TYPE="hidden" NAME="__formtype__" VALUE="%s" />\n' | |
| 427 % (reflect.qual(self.__class__))) | |
| 428 fid = self.getFormID() | |
| 429 if fid: | |
| 430 write('<INPUT TYPE="hidden" NAME="__formid__" VALUE="%s" />\n' % fid
) | |
| 431 write("</form>\n") | |
| 432 | |
| 433 def getFormID(self): | |
| 434 """Override me: I disambiguate between multiple forms of the same type. | |
| 435 | |
| 436 In order to determine which form an HTTP POST request is for, you must | |
| 437 have some unique identifier which distinguishes your form from other | |
| 438 forms of the same class. An example of such a unique identifier would | |
| 439 be: on a page with multiple FrobConf forms, each FrobConf form refers | |
| 440 to a particular Frobnitz instance, which has a unique id(). The | |
| 441 FrobConf form's getFormID would probably look like this:: | |
| 442 | |
| 443 | def getFormID(self): | |
| 444 | return str(id(self.frobnitz)) | |
| 445 | |
| 446 By default, this method will return None, since distinct Form instances | |
| 447 may be identical as far as the application is concerned. | |
| 448 """ | |
| 449 | |
| 450 def process(self, write, request, submit, **kw): | |
| 451 """Override me: I process a form. | |
| 452 | |
| 453 I will only be called when the correct form input data to process this | |
| 454 form has been received. | |
| 455 | |
| 456 I take a variable number of arguments, beginning with 'write', | |
| 457 'request', and 'submit'. 'write' is a callable object that will append | |
| 458 a string to the response, 'request' is a twisted.web.request.Request | |
| 459 instance, and 'submit' is the name of the submit action taken. | |
| 460 | |
| 461 The remainder of my arguments must be correctly named. They will each b
e named after one of the | |
| 462 | |
| 463 """ | |
| 464 write("<pre>Submit: %s <br /> %s</pre>" % (submit, html.PRE(pprint.Prett
yPrinter().pformat(kw)))) | |
| 465 | |
| 466 def _doProcess(self, form, write, request): | |
| 467 """(internal) Prepare arguments for self.process. | |
| 468 """ | |
| 469 args = request.args.copy() | |
| 470 kw = {} | |
| 471 for field in form: | |
| 472 inputType, displayName, inputName, inputValue = field[:4] | |
| 473 if inputType == 'checkbox': | |
| 474 if request.args.has_key('__checkboxes__'): | |
| 475 if inputName in request.args['__checkboxes__']: | |
| 476 formData = 1 | |
| 477 else: | |
| 478 formData = 0 | |
| 479 else: | |
| 480 formData = 0 | |
| 481 elif inputType in ['checkgroup', 'radio', 'multimenu']: | |
| 482 if args.has_key(inputName): | |
| 483 formData = args[inputName] | |
| 484 del args[inputName] | |
| 485 else: | |
| 486 formData = [] | |
| 487 else: | |
| 488 if not args.has_key(inputName): | |
| 489 raise FormInputError("missing field %s." % repr(inputName)) | |
| 490 formData = args[inputName] | |
| 491 del args[inputName] | |
| 492 if not len(formData) == 1: | |
| 493 raise FormInputError("multiple values for field %s." %repr(i
nputName)) | |
| 494 formData = formData[0] | |
| 495 method = self.formParse.get(inputType) | |
| 496 if method: | |
| 497 try: | |
| 498 formData = method(formData) | |
| 499 except: | |
| 500 raise FormInputError("%s: %s" % (displayName, "error")) | |
| 501 kw[inputName] = formData | |
| 502 submitAction = args.get('submit') | |
| 503 if submitAction: | |
| 504 submitAction = submitAction[0] | |
| 505 for field in ['submit', '__formtype__', '__checkboxes__']: | |
| 506 if args.has_key(field): | |
| 507 del args[field] | |
| 508 if args and not self.formAcceptExtraArgs: | |
| 509 raise FormInputError("unknown fields: %s" % repr(args)) | |
| 510 return apply(self.process, (write, request, submitAction), kw) | |
| 511 | |
| 512 def formatError(self,error): | |
| 513 """Format an error message. | |
| 514 | |
| 515 By default, this will make the message appear in red, bold italics. | |
| 516 """ | |
| 517 return '<font color="#f00"><b><i>%s</i></b></font><br />\n' % error | |
| 518 | |
| 519 def shouldProcess(self, request): | |
| 520 args = request.args | |
| 521 fid = self.getFormID() | |
| 522 return (args and # there are arguments to the request | |
| 523 args.has_key('__formtype__') and # this is a widgets.Form reques
t | |
| 524 args['__formtype__'][0] == reflect.qual(self.__class__) and # it
is for a form of my type | |
| 525 ((not fid) or # I am only allowed one form per page | |
| 526 (args.has_key('__formid__') and # if I distinguish myself from
others, the request must too | |
| 527 args['__formid__'][0] == fid))) # I am in fact the same | |
| 528 | |
| 529 def tryAgain(self, err, req): | |
| 530 """Utility method for re-drawing the form with an error message. | |
| 531 | |
| 532 This is handy in forms that process Deferred results. Normally you can | |
| 533 just raise a FormInputError() and this will happen by default. | |
| 534 | |
| 535 """ | |
| 536 l = [] | |
| 537 w = l.append | |
| 538 w(self.formatError(err)) | |
| 539 self.format(self.getFormFields(req), w, req) | |
| 540 return l | |
| 541 | |
| 542 def display(self, request): | |
| 543 """Display the form.""" | |
| 544 form = self.getFormFields(request) | |
| 545 if isinstance(form, defer.Deferred): | |
| 546 if self.shouldProcess(request): | |
| 547 form.addCallback(lambda form, f=self._displayProcess, r=request:
f(r, form)) | |
| 548 else: | |
| 549 form.addCallback(lambda form, f=self._displayFormat, r=request:
f(r, form)) | |
| 550 return [form] | |
| 551 else: | |
| 552 if self.shouldProcess(request): | |
| 553 return self._displayProcess(request, form) | |
| 554 else: | |
| 555 return self._displayFormat(request, form) | |
| 556 | |
| 557 def _displayProcess(self, request, form): | |
| 558 l = [] | |
| 559 write = l.append | |
| 560 try: | |
| 561 val = self._doProcess(form, write, request) | |
| 562 if val: | |
| 563 l.extend(val) | |
| 564 except FormInputError, fie: | |
| 565 write(self.formatError(str(fie))) | |
| 566 return l | |
| 567 | |
| 568 def _displayFormat(self, request, form): | |
| 569 l = [] | |
| 570 self.format(form, l.append, request) | |
| 571 return l | |
| 572 | |
| 573 | |
| 574 | |
| 575 class DataWidget(Widget): | |
| 576 def __init__(self, data): | |
| 577 self.data = data | |
| 578 def display(self, request): | |
| 579 return [self.data] | |
| 580 | |
| 581 class Time(Widget): | |
| 582 def display(self, request): | |
| 583 return [time.ctime(time.time())] | |
| 584 | |
| 585 class Container(Widget): | |
| 586 def __init__(self, *widgets): | |
| 587 self.widgets = widgets | |
| 588 | |
| 589 def display(self, request): | |
| 590 value = [] | |
| 591 for widget in self.widgets: | |
| 592 d = widget.display(request) | |
| 593 value.extend(d) | |
| 594 return value | |
| 595 | |
| 596 class _RequestDeferral: | |
| 597 def __init__(self): | |
| 598 self.deferred = defer.Deferred() | |
| 599 self.io = StringIO() | |
| 600 self.write = self.io.write | |
| 601 | |
| 602 def finish(self): | |
| 603 self.deferred.callback([self.io.getvalue()]) | |
| 604 | |
| 605 def possiblyDeferWidget(widget, request): | |
| 606 # web in my head get it out get it out | |
| 607 try: | |
| 608 disp = widget.display(request) | |
| 609 # if this widget wants to defer anything -- well, I guess we've got to | |
| 610 # defer it. | |
| 611 for elem in disp: | |
| 612 if isinstance(elem, defer.Deferred): | |
| 613 req = _RequestDeferral() | |
| 614 RenderSession(disp, req) | |
| 615 return req.deferred | |
| 616 return string.join(disp, '') | |
| 617 except: | |
| 618 io = StringIO() | |
| 619 traceback.print_exc(file=io) | |
| 620 return html.PRE(io.getvalue()) | |
| 621 | |
| 622 class RenderSession: | |
| 623 """I handle rendering of a list of deferreds, outputting their | |
| 624 results in correct order.""" | |
| 625 | |
| 626 class Sentinel: | |
| 627 pass | |
| 628 | |
| 629 def __init__(self, lst, request): | |
| 630 self.lst = lst | |
| 631 self.request = request | |
| 632 self.needsHeaders = 0 | |
| 633 self.beforeBody = 1 | |
| 634 self.forgotten = 0 | |
| 635 self.pauseList = [] | |
| 636 for i in range(len(self.lst)): | |
| 637 item = self.lst[i] | |
| 638 if isinstance(item, defer.Deferred): | |
| 639 self._addDeferred(item, self.lst, i) | |
| 640 self.keepRendering() | |
| 641 | |
| 642 def _addDeferred(self, deferred, lst, idx): | |
| 643 sentinel = self.Sentinel() | |
| 644 if hasattr(deferred, 'needsHeader'): | |
| 645 # You might want to set a header from a deferred, in which | |
| 646 # case you have to set an attribute -- needsHeader. | |
| 647 self.needsHeaders = self.needsHeaders + 1 | |
| 648 args = (sentinel, 1) | |
| 649 else: | |
| 650 args = (sentinel, 0) | |
| 651 lst[idx] = sentinel, deferred | |
| 652 deferred.pause() | |
| 653 self.pauseList.append(deferred) | |
| 654 deferred.addCallbacks(self.callback, self.callback, | |
| 655 callbackArgs=args, errbackArgs=args) | |
| 656 | |
| 657 | |
| 658 def callback(self, result, sentinel, decNeedsHeaders): | |
| 659 if self.forgotten: | |
| 660 return | |
| 661 if result != FORGET_IT: | |
| 662 self.needsHeaders = self.needsHeaders - decNeedsHeaders | |
| 663 else: | |
| 664 result = [FORGET_IT] | |
| 665 | |
| 666 # Make sure result is a sequence, | |
| 667 if not type(result) in (types.ListType, types.TupleType): | |
| 668 result = [result] | |
| 669 | |
| 670 # If the deferred does not wish to produce its result all at | |
| 671 # once, it can give us a partial result as | |
| 672 # (NOT_DONE_YET, partial_result) | |
| 673 ## XXX: How would a deferred go about producing the result in multiple | |
| 674 ## stages?? --glyph | |
| 675 if result[0] is NOT_DONE_YET: | |
| 676 done = 0 | |
| 677 result = result[1] | |
| 678 if not type(result) in (types.ListType, types.TupleType): | |
| 679 result = [result] | |
| 680 else: | |
| 681 done = 1 | |
| 682 | |
| 683 for i in xrange(len(result)): | |
| 684 item = result[i] | |
| 685 if isinstance(item, defer.Deferred): | |
| 686 self._addDeferred(item, result, i) | |
| 687 | |
| 688 for position in range(len(self.lst)): | |
| 689 item = self.lst[position] | |
| 690 if type(item) is types.TupleType and len(item) > 0: | |
| 691 if item[0] is sentinel: | |
| 692 break | |
| 693 else: | |
| 694 raise AssertionError('Sentinel for Deferred not found!') | |
| 695 | |
| 696 if done: | |
| 697 self.lst[position:position+1] = result | |
| 698 else: | |
| 699 self.lst[position:position] = result | |
| 700 | |
| 701 self.keepRendering() | |
| 702 | |
| 703 | |
| 704 def keepRendering(self): | |
| 705 while self.pauseList: | |
| 706 pl = self.pauseList | |
| 707 self.pauseList = [] | |
| 708 for deferred in pl: | |
| 709 deferred.unpause() | |
| 710 return | |
| 711 | |
| 712 if self.needsHeaders: | |
| 713 # short circuit actual rendering process until we're sure no | |
| 714 # more deferreds need to set headers... | |
| 715 return | |
| 716 | |
| 717 assert self.lst is not None, "This shouldn't happen." | |
| 718 while 1: | |
| 719 item = self.lst[0] | |
| 720 if self.beforeBody and FORGET_IT in self.lst: | |
| 721 # If I haven't moved yet, and the widget wants to take | |
| 722 # over the page, let it do so! | |
| 723 self.forgotten = 1 | |
| 724 return | |
| 725 | |
| 726 if isinstance(item, types.StringType): | |
| 727 self.beforeBody = 0 | |
| 728 self.request.write(item) | |
| 729 elif type(item) is types.TupleType and len(item) > 0: | |
| 730 if isinstance(item[0], self.Sentinel): | |
| 731 return | |
| 732 elif isinstance(item, failure.Failure): | |
| 733 self.request.write(webutil.formatFailure(item)) | |
| 734 else: | |
| 735 self.beforeBody = 0 | |
| 736 unknown = html.PRE(repr(item)) | |
| 737 self.request.write("RENDERING UNKNOWN: %s" % unknown) | |
| 738 | |
| 739 del self.lst[0] | |
| 740 if len(self.lst) == 0: | |
| 741 self.lst = None | |
| 742 self.request.finish() | |
| 743 return | |
| 744 | |
| 745 | |
| 746 ## XXX: is this needed? | |
| 747 class WidgetResource(resource.Resource): | |
| 748 def __init__(self, widget): | |
| 749 self.widget = widget | |
| 750 resource.Resource.__init__(self) | |
| 751 | |
| 752 def render(self, request): | |
| 753 RenderSession(self.widget.display(request), request) | |
| 754 return NOT_DONE_YET | |
| 755 | |
| 756 | |
| 757 class Page(resource.Resource, Presentation): | |
| 758 | |
| 759 def __init__(self): | |
| 760 resource.Resource.__init__(self) | |
| 761 Presentation.__init__(self) | |
| 762 | |
| 763 def render(self, request): | |
| 764 displayed = self.display(request) | |
| 765 RenderSession(displayed, request) | |
| 766 return NOT_DONE_YET | |
| 767 | |
| 768 | |
| 769 class WidgetPage(Page): | |
| 770 """ | |
| 771 I am a Page that takes a Widget in its constructor, and displays that | |
| 772 Widget wrapped up in a simple HTML template. | |
| 773 """ | |
| 774 stylesheet = ''' | |
| 775 a | |
| 776 { | |
| 777 font-family: Lucida, Verdana, Helvetica, Arial, sans-serif; | |
| 778 color: #369; | |
| 779 text-decoration: none; | |
| 780 } | |
| 781 | |
| 782 th | |
| 783 { | |
| 784 font-family: Lucida, Verdana, Helvetica, Arial, sans-serif; | |
| 785 font-weight: bold; | |
| 786 text-decoration: none; | |
| 787 text-align: left; | |
| 788 } | |
| 789 | |
| 790 pre, code | |
| 791 { | |
| 792 font-family: "Courier New", Courier, monospace; | |
| 793 } | |
| 794 | |
| 795 p, body, td, ol, ul, menu, blockquote, div | |
| 796 { | |
| 797 font-family: Lucida, Verdana, Helvetica, Arial, sans-serif; | |
| 798 color: #000; | |
| 799 } | |
| 800 ''' | |
| 801 | |
| 802 template = '''<html> | |
| 803 <head> | |
| 804 <title>%%%%self.title%%%%</title> | |
| 805 <style> | |
| 806 %%%%self.stylesheet%%%% | |
| 807 </style> | |
| 808 <base href="%%%%request.prePathURL()%%%%"> | |
| 809 </head> | |
| 810 | |
| 811 <body> | |
| 812 <h1>%%%%self.title%%%%</h1> | |
| 813 %%%%self.widget%%%% | |
| 814 </body> | |
| 815 </html> | |
| 816 ''' | |
| 817 | |
| 818 title = 'No Title' | |
| 819 widget = 'No Widget' | |
| 820 | |
| 821 def __init__(self, widget): | |
| 822 Page.__init__(self) | |
| 823 self.widget = widget | |
| 824 if hasattr(widget, 'stylesheet'): | |
| 825 self.stylesheet = widget.stylesheet | |
| 826 | |
| 827 def prePresent(self, request): | |
| 828 self.title = self.widget.getTitle(request) | |
| 829 | |
| 830 def render(self, request): | |
| 831 displayed = self.display(request) | |
| 832 RenderSession(displayed, request) | |
| 833 return NOT_DONE_YET | |
| 834 | |
| 835 class Gadget(resource.Resource): | |
| 836 """I am a collection of Widgets, to be rendered through a Page Factory. | |
| 837 self.pageFactory should be a Resource that takes a Widget in its | |
| 838 constructor. The default is twisted.web.widgets.WidgetPage. | |
| 839 """ | |
| 840 | |
| 841 isLeaf = 0 | |
| 842 | |
| 843 def __init__(self): | |
| 844 resource.Resource.__init__(self) | |
| 845 self.widgets = {} | |
| 846 self.files = [] | |
| 847 self.modules = [] | |
| 848 self.paths = {} | |
| 849 | |
| 850 def render(self, request): | |
| 851 #Redirect to view this entity as a collection. | |
| 852 request.setResponseCode(http.FOUND) | |
| 853 # TODO who says it's not https? | |
| 854 request.setHeader("location","http%s://%s%s/" % ( | |
| 855 request.isSecure() and 's' or '', | |
| 856 request.getHeader("host"), | |
| 857 (string.split(request.uri,'?')[0]))) | |
| 858 return "NO DICE!" | |
| 859 | |
| 860 def putWidget(self, path, widget): | |
| 861 """ | |
| 862 Gadget.putWidget(path, widget) | |
| 863 Add a Widget to this Gadget. It will be rendered through the | |
| 864 pageFactory associated with this Gadget, whenever 'path' is requested. | |
| 865 """ | |
| 866 self.widgets[path] = widget | |
| 867 | |
| 868 #this is an obsolete function | |
| 869 def addFile(self, path): | |
| 870 """ | |
| 871 Gadget.addFile(path) | |
| 872 Add a static path to this Gadget. This method is obsolete, use | |
| 873 Gadget.putPath instead. | |
| 874 """ | |
| 875 | |
| 876 log.msg("Gadget.addFile() is deprecated.") | |
| 877 self.paths[path] = path | |
| 878 | |
| 879 def putPath(self, path, pathname): | |
| 880 """ | |
| 881 Gadget.putPath(path, pathname) | |
| 882 Add a static path to this Gadget. Whenever 'path' is requested, | |
| 883 twisted.web.static.File(pathname) is sent. | |
| 884 """ | |
| 885 self.paths[path] = pathname | |
| 886 | |
| 887 def getWidget(self, path, request): | |
| 888 return self.widgets.get(path) | |
| 889 | |
| 890 def pageFactory(self, *args, **kwargs): | |
| 891 """ | |
| 892 Gadget.pageFactory(*args, **kwargs) -> Resource | |
| 893 By default, this method returns self.page(*args, **kwargs). It | |
| 894 is only for backwards-compatibility -- you should set the 'pageFactory' | |
| 895 attribute on your Gadget inside of its __init__ method. | |
| 896 """ | |
| 897 #XXX: delete this after a while. | |
| 898 if hasattr(self, "page"): | |
| 899 log.msg("Gadget.page is deprecated, use Gadget.pageFactory instead") | |
| 900 return apply(self.page, args, kwargs) | |
| 901 else: | |
| 902 return apply(WidgetPage, args, kwargs) | |
| 903 | |
| 904 def getChild(self, path, request): | |
| 905 if path == '': | |
| 906 # ZOOP! | |
| 907 if isinstance(self, Widget): | |
| 908 return self.pageFactory(self) | |
| 909 widget = self.getWidget(path, request) | |
| 910 if widget: | |
| 911 if isinstance(widget, resource.Resource): | |
| 912 return widget | |
| 913 else: | |
| 914 p = self.pageFactory(widget) | |
| 915 p.isLeaf = getattr(widget,'isLeaf',0) | |
| 916 return p | |
| 917 elif self.paths.has_key(path): | |
| 918 prefix = getattr(sys.modules[self.__module__], '__file__', '') | |
| 919 if prefix: | |
| 920 prefix = os.path.abspath(os.path.dirname(prefix)) | |
| 921 return static.File(os.path.join(prefix, self.paths[path])) | |
| 922 | |
| 923 elif path == '__reload__': | |
| 924 return self.pageFactory(Reloader(map(reflect.namedModule, [self.__mo
dule__] + self.modules))) | |
| 925 else: | |
| 926 return error.NoResource("No such child resource in gadget.") | |
| 927 | |
| 928 | |
| 929 class TitleBox(Presentation): | |
| 930 | |
| 931 template = '''\ | |
| 932 <table %%%%self.widthOption%%%% cellpadding="1" cellspacing="0" border="0"><tr>\ | |
| 933 <td bgcolor="%%%%self.borderColor%%%%"><center><font color="%%%%self.titleTextCo
lor%%%%">%%%%self.title%%%%</font></center>\ | |
| 934 <table width="100%" cellpadding="3" cellspacing="0" border="0"><tr>\ | |
| 935 <td bgcolor="%%%%self.boxColor%%%%"><font color="%%%%self.boxTextColor%%%%">%%%%
self.widget%%%%</font></td>\ | |
| 936 </tr></table></td></tr></table>\ | |
| 937 ''' | |
| 938 | |
| 939 borderColor = '#000000' | |
| 940 titleTextColor = '#ffffff' | |
| 941 boxTextColor = '#000000' | |
| 942 boxColor = '#ffffff' | |
| 943 widthOption = 'width="100%"' | |
| 944 | |
| 945 title = 'No Title' | |
| 946 widget = 'No Widget' | |
| 947 | |
| 948 def __init__(self, title, widget): | |
| 949 """Wrap a widget with a given title. | |
| 950 """ | |
| 951 self.widget = widget | |
| 952 self.title = title | |
| 953 Presentation.__init__(self) | |
| 954 | |
| 955 | |
| 956 class Reloader(Presentation): | |
| 957 template = ''' | |
| 958 Reloading... | |
| 959 <ul> | |
| 960 %%%%reload(request)%%%% | |
| 961 </ul> ... reloaded! | |
| 962 ''' | |
| 963 def __init__(self, modules): | |
| 964 Presentation.__init__(self) | |
| 965 self.modules = modules | |
| 966 | |
| 967 def reload(self, request): | |
| 968 request.redirect("..") | |
| 969 x = [] | |
| 970 write = x.append | |
| 971 for module in self.modules: | |
| 972 rebuild.rebuild(module) | |
| 973 write('<li>reloaded %s<br />' % module.__name__) | |
| 974 return x | |
| 975 | |
| 976 class Sidebar(StreamWidget): | |
| 977 bar = [ | |
| 978 ['Twisted', | |
| 979 ['mirror', 'http://coopweb.org/ssd/twisted/'], | |
| 980 ['mailing list', 'cgi-bin/mailman/listinfo/twisted-python'] | |
| 981 ] | |
| 982 ] | |
| 983 | |
| 984 headingColor = 'ffffff' | |
| 985 headingTextColor = '000000' | |
| 986 activeHeadingColor = '000000' | |
| 987 activeHeadingTextColor = 'ffffff' | |
| 988 sectionColor = '000088' | |
| 989 sectionTextColor = '008888' | |
| 990 activeSectionColor = '0000ff' | |
| 991 activeSectionTextColor = '00ffff' | |
| 992 | |
| 993 def __init__(self, highlightHeading, highlightSection): | |
| 994 self.highlightHeading = highlightHeading | |
| 995 self.highlightSection = highlightSection | |
| 996 | |
| 997 def getList(self): | |
| 998 return self.bar | |
| 999 | |
| 1000 def stream(self, write, request): | |
| 1001 write("<table width=120 cellspacing=1 cellpadding=1 border=0>") | |
| 1002 for each in self.getList(): | |
| 1003 if each[0] == self.highlightHeading: | |
| 1004 headingColor = self.activeHeadingColor | |
| 1005 headingTextColor = self.activeHeadingTextColor | |
| 1006 canHighlight = 1 | |
| 1007 else: | |
| 1008 headingColor = self.headingColor | |
| 1009 headingTextColor = self.headingTextColor | |
| 1010 canHighlight = 0 | |
| 1011 write('<tr><td colspan=2 bgcolor="#%s"><font color="%s">' | |
| 1012 '<strong>%s</strong>' | |
| 1013 '</font></td></td></tr>\n' % (headingColor, headingTextColor,
each[0])) | |
| 1014 for name, link in each[1:]: | |
| 1015 if canHighlight and (name == self.highlightSection): | |
| 1016 sectionColor = self.activeSectionColor | |
| 1017 sectionTextColor = self.activeSectionTextColor | |
| 1018 else: | |
| 1019 sectionColor = self.sectionColor | |
| 1020 sectionTextColor = self.sectionTextColor | |
| 1021 write('<tr><td align=right bgcolor="#%s" width=6>-</td>' | |
| 1022 '<td bgcolor="#%s"><a href="%s"><font color="#%s">%s' | |
| 1023 '</font></a></td></tr>' | |
| 1024 % (sectionColor, sectionColor, request.sibLink(link), sec
tionTextColor, name)) | |
| 1025 write("</table>") | |
| 1026 | |
| 1027 # moved from template.py | |
| 1028 from twisted.web.woven import template | |
| 1029 from twisted.python import components | |
| 1030 | |
| 1031 class WebWidgetNodeMutator(template.NodeMutator): | |
| 1032 """A WebWidgetNodeMutator replaces the node that is passed in to generate | |
| 1033 with the result of generating the twisted.web.widget instance it adapts. | |
| 1034 """ | |
| 1035 def generate(self, request, node): | |
| 1036 widget = self.data | |
| 1037 displayed = widget.display(request) | |
| 1038 try: | |
| 1039 html = string.join(displayed) | |
| 1040 except: | |
| 1041 pr = Presentation() | |
| 1042 pr.tmpl = displayed | |
| 1043 #strList = pr.display(request) | |
| 1044 html = string.join(displayed) | |
| 1045 stringMutator = template.StringNodeMutator(html) | |
| 1046 return stringMutator.generate(request, node) | |
| 1047 | |
| 1048 components.registerAdapter(WebWidgetNodeMutator, Widget, template.INodeMutator) | |
| 1049 | |
| 1050 import static | |
| OLD | NEW |