[2747] | 1 | #!/usr/bin/python2.3 |
---|
| 2 | # |
---|
| 3 | # vim:set et ts=4 fdc=0 fdn=2 fdl=0: |
---|
| 4 | # |
---|
| 5 | # There are no blank lines between blocks beacause i use folding from: |
---|
| 6 | # http://www.vim.org/scripts/script.php?script_id=515 |
---|
| 7 | # |
---|
| 8 | |
---|
| 9 | """= QWeb Framework = |
---|
| 10 | |
---|
| 11 | == What is QWeb ? == |
---|
| 12 | |
---|
| 13 | QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI] |
---|
| 14 | compatible web framework, it provides an infratructure to quickly build web |
---|
| 15 | applications consisting of: |
---|
| 16 | |
---|
| 17 | * A lightweight request handler (QWebRequest) |
---|
| 18 | * An xml templating engine (QWebXml and QWebHtml) |
---|
| 19 | * A simple name based controler (qweb_control) |
---|
| 20 | * A standalone WSGI Server (QWebWSGIServer) |
---|
| 21 | * A cgi and fastcgi WSGI wrapper (taken from flup) |
---|
| 22 | * A startup function that starts cgi, factgi or standalone according to the |
---|
| 23 | evironement (qweb_autorun). |
---|
| 24 | |
---|
| 25 | QWeb applications are runnable in standalone mode (from commandline), via |
---|
| 26 | FastCGI, Regular CGI or by any python WSGI compliant server. |
---|
| 27 | |
---|
| 28 | QWeb doesn't provide any database access but it integrates nicely with ORMs |
---|
| 29 | such as SQLObject, SQLAlchemy or plain DB-API. |
---|
| 30 | |
---|
| 31 | Written by Antony Lesuisse (email al AT udev.org) |
---|
| 32 | |
---|
| 33 | Homepage: http://antony.lesuisse.org/qweb/trac/ |
---|
| 34 | |
---|
| 35 | Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum] |
---|
| 36 | |
---|
| 37 | == Quick Start (for Linux, MacOS X and cygwin) == |
---|
| 38 | |
---|
| 39 | Make sure you have at least python 2.3 installed and run the following commands: |
---|
| 40 | |
---|
| 41 | {{{ |
---|
| 42 | $ wget http://antony.lesuisse.org/qweb/files/QWeb-0.7.tar.gz |
---|
| 43 | $ tar zxvf QWeb-0.7.tar.gz |
---|
| 44 | $ cd QWeb-0.7/examples/blog |
---|
| 45 | $ ./blog.py |
---|
| 46 | }}} |
---|
| 47 | |
---|
| 48 | And point your browser to http://localhost:8080/ |
---|
| 49 | |
---|
| 50 | You may also try AjaxTerm which uses qweb request handler. |
---|
| 51 | |
---|
| 52 | == Download == |
---|
| 53 | |
---|
| 54 | * Version 0.7: |
---|
| 55 | * Source [/qweb/files/QWeb-0.7.tar.gz QWeb-0.7.tar.gz] |
---|
| 56 | * Python 2.3 Egg [/qweb/files/QWeb-0.7-py2.3.egg QWeb-0.7-py2.3.egg] |
---|
| 57 | * Python 2.4 Egg [/qweb/files/QWeb-0.7-py2.4.egg QWeb-0.7-py2.4.egg] |
---|
| 58 | |
---|
| 59 | * [/qweb/trac/browser Browse the source repository] |
---|
| 60 | |
---|
| 61 | == Documentation == |
---|
| 62 | |
---|
| 63 | * [/qweb/trac/browser/trunk/README.txt?format=raw Read the included documentation] |
---|
| 64 | * QwebTemplating |
---|
| 65 | |
---|
| 66 | == Mailin-list == |
---|
| 67 | |
---|
| 68 | * Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum] |
---|
| 69 | * No mailing-list exists yet, discussion should happen on: [http://mail.python.org/mailman/listinfo/web-sig web-sig] [http://mail.python.org/pipermail/web-sig/ archives] |
---|
| 70 | |
---|
| 71 | QWeb Components: |
---|
| 72 | ---------------- |
---|
| 73 | |
---|
| 74 | QWeb also feature a simple components api, that enables developers to easily |
---|
| 75 | produces reusable components. |
---|
| 76 | |
---|
| 77 | Default qweb components: |
---|
| 78 | |
---|
| 79 | - qweb_static: |
---|
| 80 | A qweb component to serve static content from the filesystem or from |
---|
| 81 | zipfiles. |
---|
| 82 | |
---|
| 83 | - qweb_dbadmin: |
---|
| 84 | scaffolding for sqlobject |
---|
| 85 | |
---|
| 86 | License |
---|
| 87 | ------- |
---|
| 88 | qweb/fcgi.py wich is BSD-like from saddi.com. |
---|
| 89 | Everything else is put in the public domain. |
---|
| 90 | |
---|
| 91 | |
---|
| 92 | TODO |
---|
| 93 | ---- |
---|
| 94 | Announce QWeb to python-announce-list@python.org web-sig@python.org |
---|
| 95 | qweb_core |
---|
| 96 | rename request methods into |
---|
| 97 | request_save_files |
---|
| 98 | response_404 |
---|
| 99 | response_redirect |
---|
| 100 | response_download |
---|
| 101 | request callback_generator, callback_function ? |
---|
| 102 | wsgi callback_server_local |
---|
| 103 | xml tags explicitly call render_attributes(t_att)? |
---|
| 104 | priority form-checkbox over t-value (for t-option) |
---|
| 105 | |
---|
| 106 | """ |
---|
| 107 | |
---|
| 108 | import BaseHTTPServer,SocketServer,Cookie |
---|
| 109 | import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom |
---|
| 110 | try: |
---|
| 111 | import cPickle as pickle |
---|
| 112 | except ImportError: |
---|
| 113 | import pickle |
---|
| 114 | try: |
---|
| 115 | import cStringIO as StringIO |
---|
| 116 | except ImportError: |
---|
| 117 | import StringIO |
---|
| 118 | |
---|
| 119 | #---------------------------------------------------------- |
---|
| 120 | # Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim |
---|
| 121 | #---------------------------------------------------------- |
---|
| 122 | class QWebEval: |
---|
| 123 | def __init__(self,data): |
---|
| 124 | self.data=data |
---|
| 125 | def __getitem__(self,expr): |
---|
| 126 | if self.data.has_key(expr): |
---|
| 127 | return self.data[expr] |
---|
| 128 | r=None |
---|
| 129 | try: |
---|
| 130 | r=eval(expr,self.data) |
---|
| 131 | except NameError,e: |
---|
| 132 | pass |
---|
| 133 | except AttributeError,e: |
---|
| 134 | pass |
---|
| 135 | except Exception,e: |
---|
| 136 | print "qweb: expression error '%s' "%expr,e |
---|
| 137 | if self.data.has_key("__builtins__"): |
---|
| 138 | del self.data["__builtins__"] |
---|
| 139 | return r |
---|
| 140 | def eval_object(self,expr): |
---|
| 141 | return self[expr] |
---|
| 142 | def eval_str(self,expr): |
---|
| 143 | if expr=="0": |
---|
| 144 | return self.data[0] |
---|
| 145 | if isinstance(self[expr],unicode): |
---|
| 146 | return self[expr].encode("utf8") |
---|
| 147 | return str(self[expr]) |
---|
| 148 | def eval_format(self,expr): |
---|
| 149 | try: |
---|
| 150 | return str(expr%self) |
---|
| 151 | except: |
---|
| 152 | return "qweb: format error '%s' "%expr |
---|
| 153 | # if isinstance(r,unicode): |
---|
| 154 | # return r.encode("utf8") |
---|
| 155 | def eval_bool(self,expr): |
---|
| 156 | if self.eval_object(expr): |
---|
| 157 | return 1 |
---|
| 158 | else: |
---|
| 159 | return 0 |
---|
| 160 | class QWebXml: |
---|
| 161 | """QWeb Xml templating engine |
---|
| 162 | |
---|
| 163 | The templating engine use a very simple syntax, "magic" xml attributes, to |
---|
| 164 | produce any kind of texutal output (even non-xml). |
---|
| 165 | |
---|
| 166 | QWebXml: |
---|
| 167 | the template engine core implements the basic magic attributes: |
---|
| 168 | |
---|
| 169 | t-att t-raw t-esc t-if t-foreach t-set t-call t-trim |
---|
| 170 | |
---|
| 171 | """ |
---|
| 172 | def __init__(self,x=None,zipname=None): |
---|
| 173 | self.node=xml.dom.Node |
---|
| 174 | self._t={} |
---|
| 175 | self._render_tag={} |
---|
| 176 | prefix='render_tag_' |
---|
| 177 | for i in [j for j in dir(self) if j.startswith(prefix)]: |
---|
| 178 | name=i[len(prefix):].replace('_','-') |
---|
| 179 | self._render_tag[name]=getattr(self.__class__,i) |
---|
| 180 | |
---|
| 181 | self._render_att={} |
---|
| 182 | prefix='render_att_' |
---|
| 183 | for i in [j for j in dir(self) if j.startswith(prefix)]: |
---|
| 184 | name=i[len(prefix):].replace('_','-') |
---|
| 185 | self._render_att[name]=getattr(self.__class__,i) |
---|
| 186 | |
---|
| 187 | if x!=None: |
---|
| 188 | if zipname!=None: |
---|
| 189 | import zipfile |
---|
| 190 | zf=zipfile.ZipFile(zipname, 'r') |
---|
| 191 | self.add_template(zf.read(x)) |
---|
| 192 | else: |
---|
| 193 | self.add_template(x) |
---|
| 194 | def register_tag(self,tag,func): |
---|
| 195 | self._render_tag[tag]=func |
---|
| 196 | def add_template(self,x): |
---|
| 197 | if hasattr(x,'documentElement'): |
---|
| 198 | dom=x |
---|
| 199 | elif x.startswith("<?xml"): |
---|
| 200 | import xml.dom.minidom |
---|
| 201 | dom=xml.dom.minidom.parseString(x) |
---|
| 202 | else: |
---|
| 203 | import xml.dom.minidom |
---|
| 204 | dom=xml.dom.minidom.parse(x) |
---|
| 205 | for n in dom.documentElement.childNodes: |
---|
| 206 | if n.nodeName=="t": |
---|
| 207 | self._t[str(n.getAttribute("t-name"))]=n |
---|
| 208 | def get_template(self,name): |
---|
| 209 | return self._t[name] |
---|
| 210 | |
---|
| 211 | def eval_object(self,expr,v): |
---|
| 212 | return QWebEval(v).eval_object(expr) |
---|
| 213 | def eval_str(self,expr,v): |
---|
| 214 | return QWebEval(v).eval_str(expr) |
---|
| 215 | def eval_format(self,expr,v): |
---|
| 216 | return QWebEval(v).eval_format(expr) |
---|
| 217 | def eval_bool(self,expr,v): |
---|
| 218 | return QWebEval(v).eval_bool(expr) |
---|
| 219 | |
---|
| 220 | def render(self,tname,v={},out=None): |
---|
| 221 | if self._t.has_key(tname): |
---|
| 222 | return self.render_node(self._t[tname],v) |
---|
| 223 | else: |
---|
| 224 | return 'qweb: template "%s" not found'%tname |
---|
| 225 | def render_node(self,e,v): |
---|
| 226 | r="" |
---|
| 227 | if e.nodeType==self.node.TEXT_NODE or e.nodeType==self.node.CDATA_SECTION_NODE: |
---|
| 228 | r=e.data.encode("utf8") |
---|
| 229 | elif e.nodeType==self.node.ELEMENT_NODE: |
---|
| 230 | pre="" |
---|
| 231 | g_att="" |
---|
| 232 | t_render=None |
---|
| 233 | t_att={} |
---|
| 234 | for (an,av) in e.attributes.items(): |
---|
| 235 | an=str(an) |
---|
| 236 | if isinstance(av,types.UnicodeType): |
---|
| 237 | av=av.encode("utf8") |
---|
| 238 | else: |
---|
| 239 | av=av.nodeValue.encode("utf8") |
---|
| 240 | if an.startswith("t-"): |
---|
| 241 | for i in self._render_att: |
---|
| 242 | if an[2:].startswith(i): |
---|
| 243 | g_att+=self._render_att[i](self,e,an,av,v) |
---|
| 244 | break |
---|
| 245 | else: |
---|
| 246 | if self._render_tag.has_key(an[2:]): |
---|
| 247 | t_render=an[2:] |
---|
| 248 | t_att[an[2:]]=av |
---|
| 249 | else: |
---|
| 250 | g_att+=' %s="%s"'%(an,cgi.escape(av,1)); |
---|
| 251 | if t_render: |
---|
| 252 | if self._render_tag.has_key(t_render): |
---|
| 253 | r=self._render_tag[t_render](self,e,t_att,g_att,v) |
---|
| 254 | else: |
---|
| 255 | r=self.render_element(e,g_att,v,pre,t_att.get("trim",0)) |
---|
| 256 | return r |
---|
| 257 | def render_element(self,e,g_att,v,pre="",trim=0): |
---|
| 258 | g_inner=[] |
---|
| 259 | for n in e.childNodes: |
---|
| 260 | g_inner.append(self.render_node(n,v)) |
---|
| 261 | name=str(e.nodeName) |
---|
| 262 | inner="".join(g_inner) |
---|
| 263 | if trim==0: |
---|
| 264 | pass |
---|
| 265 | elif trim=='left': |
---|
| 266 | inner=inner.lstrip() |
---|
| 267 | elif trim=='right': |
---|
| 268 | inner=inner.rstrip() |
---|
| 269 | elif trim=='both': |
---|
| 270 | inner=inner.strip() |
---|
| 271 | if name=="t": |
---|
| 272 | return inner |
---|
| 273 | elif len(inner): |
---|
| 274 | return "<%s%s>%s%s</%s>"%(name,g_att,pre,inner,name) |
---|
| 275 | else: |
---|
| 276 | return "<%s%s/>"%(name,g_att) |
---|
| 277 | |
---|
| 278 | # Attributes |
---|
| 279 | def render_att_att(self,e,an,av,v): |
---|
| 280 | if an.startswith("t-attf-"): |
---|
| 281 | att,val=an[7:],self.eval_format(av,v) |
---|
| 282 | elif an.startswith("t-att-"): |
---|
| 283 | att,val=(an[6:],self.eval_str(av,v)) |
---|
| 284 | else: |
---|
| 285 | att,val=self.eval_object(av,v) |
---|
| 286 | return ' %s="%s"'%(att,cgi.escape(val,1)) |
---|
| 287 | |
---|
| 288 | # Tags |
---|
| 289 | def render_tag_raw(self,e,t_att,g_att,v): |
---|
| 290 | return self.eval_str(t_att["raw"],v) |
---|
| 291 | def render_tag_rawf(self,e,t_att,g_att,v): |
---|
| 292 | return self.eval_format(t_att["rawf"],v) |
---|
| 293 | def render_tag_esc(self,e,t_att,g_att,v): |
---|
| 294 | return cgi.escape(self.eval_str(t_att["esc"],v)) |
---|
| 295 | def render_tag_escf(self,e,t_att,g_att,v): |
---|
| 296 | return cgi.escape(self.eval_format(t_att["escf"],v)) |
---|
| 297 | def render_tag_foreach(self,e,t_att,g_att,v): |
---|
| 298 | expr=t_att["foreach"] |
---|
| 299 | enum=self.eval_object(expr,v) |
---|
| 300 | if enum!=None: |
---|
| 301 | var=t_att.get('as',expr).replace('.','_') |
---|
| 302 | d=v.copy() |
---|
| 303 | size=-1 |
---|
| 304 | if isinstance(enum,types.ListType): |
---|
| 305 | size=len(enum) |
---|
| 306 | elif isinstance(enum,types.TupleType): |
---|
| 307 | size=len(enum) |
---|
| 308 | elif hasattr(enum,'count'): |
---|
| 309 | size=enum.count() |
---|
| 310 | d["%s_size"%var]=size |
---|
| 311 | d["%s_all"%var]=enum |
---|
| 312 | index=0 |
---|
| 313 | ru=[] |
---|
| 314 | for i in enum: |
---|
| 315 | d["%s_value"%var]=i |
---|
| 316 | d["%s_index"%var]=index |
---|
| 317 | d["%s_first"%var]=index==0 |
---|
| 318 | d["%s_even"%var]=index%2 |
---|
| 319 | d["%s_odd"%var]=(index+1)%2 |
---|
| 320 | d["%s_last"%var]=index+1==size |
---|
| 321 | if index%2: |
---|
| 322 | d["%s_parity"%var]='odd' |
---|
| 323 | else: |
---|
| 324 | d["%s_parity"%var]='even' |
---|
| 325 | if isinstance(i,types.DictType): |
---|
| 326 | d.update(i) |
---|
| 327 | else: |
---|
| 328 | d[var]=i |
---|
| 329 | ru.append(self.render_element(e,g_att,d)) |
---|
| 330 | index+=1 |
---|
| 331 | return "".join(ru) |
---|
| 332 | else: |
---|
| 333 | return "qweb: t-foreach %s not found."%expr |
---|
| 334 | def render_tag_if(self,e,t_att,g_att,v): |
---|
| 335 | if self.eval_bool(t_att["if"],v): |
---|
| 336 | return self.render_element(e,g_att,v) |
---|
| 337 | else: |
---|
| 338 | return "" |
---|
| 339 | def render_tag_call(self,e,t_att,g_att,v): |
---|
| 340 | # TODO t-prefix |
---|
| 341 | if t_att.has_key("import"): |
---|
| 342 | d=v |
---|
| 343 | else: |
---|
| 344 | d=v.copy() |
---|
| 345 | d[0]=self.render_element(e,g_att,d) |
---|
| 346 | return self.render(t_att["call"],d) |
---|
| 347 | def render_tag_set(self,e,t_att,g_att,v): |
---|
| 348 | if t_att.has_key("eval"): |
---|
| 349 | v[t_att["set"]]=self.eval_object(t_att["eval"],v) |
---|
| 350 | else: |
---|
| 351 | v[t_att["set"]]=self.render_element(e,g_att,v) |
---|
| 352 | return "" |
---|
| 353 | |
---|
| 354 | #---------------------------------------------------------- |
---|
| 355 | # QWeb HTML (+deprecated QWebFORM and QWebOLD) |
---|
| 356 | #---------------------------------------------------------- |
---|
| 357 | class QWebURL: |
---|
| 358 | """ URL helper |
---|
| 359 | assert req.PATH_INFO== "/site/admin/page_edit" |
---|
| 360 | u = QWebURL(root_path="/site/",req_path=req.PATH_INFO) |
---|
| 361 | s=u.url2_href("user/login",{'a':'1'}) |
---|
| 362 | assert s=="../user/login?a=1" |
---|
| 363 | |
---|
| 364 | """ |
---|
| 365 | def __init__(self, root_path="/", req_path="/",defpath="",defparam={}): |
---|
| 366 | self.defpath=defpath |
---|
| 367 | self.defparam=defparam |
---|
| 368 | self.root_path=root_path |
---|
| 369 | self.req_path=req_path |
---|
| 370 | self.req_list=req_path.split("/")[:-1] |
---|
| 371 | self.req_len=len(self.req_list) |
---|
| 372 | def decode(self,s): |
---|
| 373 | h={} |
---|
| 374 | for k,v in cgi.parse_qsl(s,1): |
---|
| 375 | h[k]=v |
---|
| 376 | return h |
---|
| 377 | def encode(self,h): |
---|
| 378 | return urllib.urlencode(h.items()) |
---|
| 379 | def request(self,req): |
---|
| 380 | return req.REQUEST |
---|
| 381 | def copy(self,path=None,param=None): |
---|
| 382 | npath=self.defpath |
---|
| 383 | if path: |
---|
| 384 | npath=path |
---|
| 385 | nparam=self.defparam.copy() |
---|
| 386 | if param: |
---|
| 387 | nparam.update(param) |
---|
| 388 | return QWebURL(self.root_path,self.req_path,npath,nparam) |
---|
| 389 | def path(self,path=''): |
---|
| 390 | if not path: |
---|
| 391 | path=self.defpath |
---|
| 392 | pl=(self.root_path+path).split('/') |
---|
| 393 | i=0 |
---|
| 394 | for i in range(min(len(pl), self.req_len)): |
---|
| 395 | if pl[i]!=self.req_list[i]: |
---|
| 396 | break |
---|
| 397 | else: |
---|
| 398 | i+=1 |
---|
| 399 | dd=self.req_len-i |
---|
| 400 | if dd<0: |
---|
| 401 | dd=0 |
---|
| 402 | return '/'.join(['..']*dd+pl[i:]) |
---|
| 403 | def href(self,path='',arg={}): |
---|
| 404 | p=self.path(path) |
---|
| 405 | tmp=self.defparam.copy() |
---|
| 406 | tmp.update(arg) |
---|
| 407 | s=self.encode(tmp) |
---|
| 408 | if len(s): |
---|
| 409 | return p+"?"+s |
---|
| 410 | else: |
---|
| 411 | return p |
---|
| 412 | def form(self,path='',arg={}): |
---|
| 413 | p=self.path(path) |
---|
| 414 | tmp=self.defparam.copy() |
---|
| 415 | tmp.update(arg) |
---|
| 416 | r=''.join(['<input type="hidden" name="%s" value="%s"/>'%(k,cgi.escape(str(v),1)) for k,v in tmp.items()]) |
---|
| 417 | return (p,r) |
---|
| 418 | class QWebField: |
---|
| 419 | def __init__(self,name=None,default="",check=None): |
---|
| 420 | self.name=name |
---|
| 421 | self.default=default |
---|
| 422 | self.check=check |
---|
| 423 | # optional attributes |
---|
| 424 | self.type=None |
---|
| 425 | self.trim=1 |
---|
| 426 | self.required=1 |
---|
| 427 | self.cssvalid="form_valid" |
---|
| 428 | self.cssinvalid="form_invalid" |
---|
| 429 | # set by addfield |
---|
| 430 | self.form=None |
---|
| 431 | # set by processing |
---|
| 432 | self.input=None |
---|
| 433 | self.css=None |
---|
| 434 | self.value=None |
---|
| 435 | self.valid=None |
---|
| 436 | self.invalid=None |
---|
| 437 | self.validate(1) |
---|
| 438 | def validate(self,val=1,update=1): |
---|
| 439 | if val: |
---|
| 440 | self.valid=1 |
---|
| 441 | self.invalid=0 |
---|
| 442 | self.css=self.cssvalid |
---|
| 443 | else: |
---|
| 444 | self.valid=0 |
---|
| 445 | self.invalid=1 |
---|
| 446 | self.css=self.cssinvalid |
---|
| 447 | if update and self.form: |
---|
| 448 | self.form.update() |
---|
| 449 | def invalidate(self,update=1): |
---|
| 450 | self.validate(0,update) |
---|
| 451 | class QWebForm: |
---|
| 452 | class QWebFormF: |
---|
| 453 | pass |
---|
| 454 | def __init__(self,e=None,arg=None,default=None): |
---|
| 455 | self.fields={} |
---|
| 456 | # all fields have been submitted |
---|
| 457 | self.submitted=False |
---|
| 458 | self.missing=[] |
---|
| 459 | # at least one field is invalid or missing |
---|
| 460 | self.invalid=False |
---|
| 461 | self.error=[] |
---|
| 462 | # all fields have been submitted and are valid |
---|
| 463 | self.valid=False |
---|
| 464 | # fields under self.f for convenience |
---|
| 465 | self.f=self.QWebFormF() |
---|
| 466 | if e: |
---|
| 467 | self.add_template(e) |
---|
| 468 | # assume that the fields are done with the template |
---|
| 469 | if default: |
---|
| 470 | self.set_default(default,e==None) |
---|
| 471 | if arg!=None: |
---|
| 472 | self.process_input(arg) |
---|
| 473 | def __getitem__(self,k): |
---|
| 474 | return self.fields[k] |
---|
| 475 | def set_default(self,default,add_missing=1): |
---|
| 476 | for k,v in default.items(): |
---|
| 477 | if self.fields.has_key(k): |
---|
| 478 | self.fields[k].default=str(v) |
---|
| 479 | elif add_missing: |
---|
| 480 | self.add_field(QWebField(k,v)) |
---|
| 481 | def add_field(self,f): |
---|
| 482 | self.fields[f.name]=f |
---|
| 483 | f.form=self |
---|
| 484 | setattr(self.f,f.name,f) |
---|
| 485 | def add_template(self,e): |
---|
| 486 | att={} |
---|
| 487 | for (an,av) in e.attributes.items(): |
---|
| 488 | an=str(an) |
---|
| 489 | if an.startswith("t-"): |
---|
| 490 | att[an[2:]]=av.encode("utf8") |
---|
| 491 | for i in ["form-text", "form-password", "form-radio", "form-checkbox", "form-select","form-textarea"]: |
---|
| 492 | if att.has_key(i): |
---|
| 493 | name=att[i].split(".")[-1] |
---|
| 494 | default=att.get("default","") |
---|
| 495 | check=att.get("check",None) |
---|
| 496 | f=QWebField(name,default,check) |
---|
| 497 | if i=="form-textarea": |
---|
| 498 | f.type="textarea" |
---|
| 499 | f.trim=0 |
---|
| 500 | if i=="form-checkbox": |
---|
| 501 | f.type="checkbox" |
---|
| 502 | f.required=0 |
---|
| 503 | self.add_field(f) |
---|
| 504 | for n in e.childNodes: |
---|
| 505 | if n.nodeType==n.ELEMENT_NODE: |
---|
| 506 | self.add_template(n) |
---|
| 507 | def process_input(self,arg): |
---|
| 508 | for f in self.fields.values(): |
---|
| 509 | if arg.has_key(f.name): |
---|
| 510 | f.input=arg[f.name] |
---|
| 511 | f.value=f.input |
---|
| 512 | if f.trim: |
---|
| 513 | f.input=f.input.strip() |
---|
| 514 | f.validate(1,False) |
---|
| 515 | if f.check==None: |
---|
| 516 | continue |
---|
| 517 | elif callable(f.check): |
---|
| 518 | pass |
---|
| 519 | elif isinstance(f.check,str): |
---|
| 520 | v=f.check |
---|
| 521 | if f.check=="email": |
---|
| 522 | v=r"/^[^@#!& ]+@[A-Za-z0-9-][.A-Za-z0-9-]{0,64}\.[A-Za-z]{2,5}$/" |
---|
| 523 | if f.check=="date": |
---|
| 524 | v=r"/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/" |
---|
| 525 | if not re.match(v[1:-1],f.input): |
---|
| 526 | f.validate(0,False) |
---|
| 527 | else: |
---|
| 528 | f.value=f.default |
---|
| 529 | self.update() |
---|
| 530 | def validate_all(self,val=1): |
---|
| 531 | for f in self.fields.values(): |
---|
| 532 | f.validate(val,0) |
---|
| 533 | self.update() |
---|
| 534 | def invalidate_all(self): |
---|
| 535 | self.validate_all(0) |
---|
| 536 | def update(self): |
---|
| 537 | self.submitted=True |
---|
| 538 | self.valid=True |
---|
| 539 | self.errors=[] |
---|
| 540 | for f in self.fields.values(): |
---|
| 541 | if f.required and f.input==None: |
---|
| 542 | self.submitted=False |
---|
| 543 | self.valid=False |
---|
| 544 | self.missing.append(f.name) |
---|
| 545 | if f.invalid: |
---|
| 546 | self.valid=False |
---|
| 547 | self.error.append(f.name) |
---|
| 548 | # invalid have been submitted and |
---|
| 549 | self.invalid=self.submitted and self.valid==False |
---|
| 550 | def collect(self): |
---|
| 551 | d={} |
---|
| 552 | for f in self.fields.values(): |
---|
| 553 | d[f.name]=f.value |
---|
| 554 | return d |
---|
| 555 | class QWebURLEval(QWebEval): |
---|
| 556 | def __init__(self,data): |
---|
| 557 | QWebEval.__init__(self,data) |
---|
| 558 | def __getitem__(self,expr): |
---|
| 559 | r=QWebEval.__getitem__(self,expr) |
---|
| 560 | if isinstance(r,str): |
---|
| 561 | return urllib.quote_plus(r) |
---|
| 562 | else: |
---|
| 563 | return r |
---|
| 564 | class QWebHtml(QWebXml): |
---|
| 565 | """QWebHtml |
---|
| 566 | QWebURL: |
---|
| 567 | QWebField: |
---|
| 568 | QWebForm: |
---|
| 569 | QWebHtml: |
---|
| 570 | an extended template engine, with a few utility class to easily produce |
---|
| 571 | HTML, handle URLs and process forms, it adds the following magic attributes: |
---|
| 572 | |
---|
| 573 | t-href t-action t-form-text t-form-password t-form-textarea t-form-radio |
---|
| 574 | t-form-checkbox t-form-select t-option t-selected t-checked t-pager |
---|
| 575 | |
---|
| 576 | # explication URL: |
---|
| 577 | # v['tableurl']=QWebUrl({p=afdmin,saar=,orderby=,des=,mlink;meta_active=}) |
---|
| 578 | # t-href="tableurl?desc=1" |
---|
| 579 | # |
---|
| 580 | # explication FORM: t-if="form.valid()" |
---|
| 581 | # Foreach i |
---|
| 582 | # email: <input type="text" t-esc-name="i" t-esc-value="form[i].value" t-esc-class="form[i].css"/> |
---|
| 583 | # <input type="radio" name="spamtype" t-esc-value="i" t-selected="i==form.f.spamtype.value"/> |
---|
| 584 | # <option t-esc-value="cc" t-selected="cc==form.f.country.value"><t t-esc="cname"></option> |
---|
| 585 | # Simple forms: |
---|
| 586 | # <input t-form-text="form.email" t-check="email"/> |
---|
| 587 | # <input t-form-password="form.email" t-check="email"/> |
---|
| 588 | # <input t-form-radio="form.email" /> |
---|
| 589 | # <input t-form-checkbox="form.email" /> |
---|
| 590 | # <textarea t-form-textarea="form.email" t-check="email"/> |
---|
| 591 | # <select t-form-select="form.email"/> |
---|
| 592 | # <option t-value="1"> |
---|
| 593 | # <input t-form-radio="form.spamtype" t-value="1"/> Cars |
---|
| 594 | # <input t-form-radio="form.spamtype" t-value="2"/> Sprt |
---|
| 595 | """ |
---|
| 596 | # QWebForm from a template |
---|
| 597 | def form(self,tname,arg=None,default=None): |
---|
| 598 | form=QWebForm(self._t[tname],arg,default) |
---|
| 599 | return form |
---|
| 600 | |
---|
| 601 | # HTML Att |
---|
| 602 | def eval_url(self,av,v): |
---|
| 603 | s=QWebURLEval(v).eval_format(av) |
---|
| 604 | a=s.split('?',1) |
---|
| 605 | arg={} |
---|
| 606 | if len(a)>1: |
---|
| 607 | for k,v in cgi.parse_qsl(a[1],1): |
---|
| 608 | arg[k]=v |
---|
| 609 | b=a[0].split('/',1) |
---|
| 610 | path='' |
---|
| 611 | if len(b)>1: |
---|
| 612 | path=b[1] |
---|
| 613 | u=b[0] |
---|
| 614 | return u,path,arg |
---|
| 615 | def render_att_url_(self,e,an,av,v): |
---|
| 616 | u,path,arg=self.eval_url(av,v) |
---|
| 617 | if not isinstance(v.get(u,0),QWebURL): |
---|
| 618 | out='qweb: missing url %r %r %r'%(u,path,arg) |
---|
| 619 | else: |
---|
| 620 | out=v[u].href(path,arg) |
---|
| 621 | return ' %s="%s"'%(an[6:],cgi.escape(out,1)) |
---|
| 622 | def render_att_href(self,e,an,av,v): |
---|
| 623 | return self.render_att_url_(e,"t-url-href",av,v) |
---|
| 624 | def render_att_checked(self,e,an,av,v): |
---|
| 625 | if self.eval_bool(av,v): |
---|
| 626 | return ' %s="%s"'%(an[2:],an[2:]) |
---|
| 627 | else: |
---|
| 628 | return '' |
---|
| 629 | def render_att_selected(self,e,an,av,v): |
---|
| 630 | return self.render_att_checked(e,an,av,v) |
---|
| 631 | |
---|
| 632 | # HTML Tags forms |
---|
| 633 | def render_tag_rawurl(self,e,t_att,g_att,v): |
---|
| 634 | u,path,arg=self.eval_url(t_att["rawurl"],v) |
---|
| 635 | return v[u].href(path,arg) |
---|
| 636 | def render_tag_escurl(self,e,t_att,g_att,v): |
---|
| 637 | u,path,arg=self.eval_url(t_att["escurl"],v) |
---|
| 638 | return cgi.escape(v[u].href(path,arg)) |
---|
| 639 | def render_tag_action(self,e,t_att,g_att,v): |
---|
| 640 | u,path,arg=self.eval_url(t_att["action"],v) |
---|
| 641 | if not isinstance(v.get(u,0),QWebURL): |
---|
| 642 | action,input=('qweb: missing url %r %r %r'%(u,path,arg),'') |
---|
| 643 | else: |
---|
| 644 | action,input=v[u].form(path,arg) |
---|
| 645 | g_att+=' action="%s"'%action |
---|
| 646 | return self.render_element(e,g_att,v,input) |
---|
| 647 | def render_tag_form_text(self,e,t_att,g_att,v): |
---|
| 648 | f=self.eval_object(t_att["form-text"],v) |
---|
| 649 | g_att+=' type="text" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css) |
---|
| 650 | return self.render_element(e,g_att,v) |
---|
| 651 | def render_tag_form_password(self,e,t_att,g_att,v): |
---|
| 652 | f=self.eval_object(t_att["form-password"],v) |
---|
| 653 | g_att+=' type="password" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css) |
---|
| 654 | return self.render_element(e,g_att,v) |
---|
| 655 | def render_tag_form_textarea(self,e,t_att,g_att,v): |
---|
| 656 | type="textarea" |
---|
| 657 | f=self.eval_object(t_att["form-textarea"],v) |
---|
| 658 | g_att+=' name="%s" class="%s"'%(f.name,f.css) |
---|
| 659 | r="<%s%s>%s</%s>"%(type,g_att,cgi.escape(f.value,1),type) |
---|
| 660 | return r |
---|
| 661 | def render_tag_form_radio(self,e,t_att,g_att,v): |
---|
| 662 | f=self.eval_object(t_att["form-radio"],v) |
---|
| 663 | val=t_att["value"] |
---|
| 664 | g_att+=' type="radio" name="%s" value="%s"'%(f.name,val) |
---|
| 665 | if f.value==val: |
---|
| 666 | g_att+=' checked="checked"' |
---|
| 667 | return self.render_element(e,g_att,v) |
---|
| 668 | def render_tag_form_checkbox(self,e,t_att,g_att,v): |
---|
| 669 | f=self.eval_object(t_att["form-checkbox"],v) |
---|
| 670 | val=t_att["value"] |
---|
| 671 | g_att+=' type="checkbox" name="%s" value="%s"'%(f.name,val) |
---|
| 672 | if f.value==val: |
---|
| 673 | g_att+=' checked="checked"' |
---|
| 674 | return self.render_element(e,g_att,v) |
---|
| 675 | def render_tag_form_select(self,e,t_att,g_att,v): |
---|
| 676 | f=self.eval_object(t_att["form-select"],v) |
---|
| 677 | g_att+=' name="%s" class="%s"'%(f.name,f.css) |
---|
| 678 | return self.render_element(e,g_att,v) |
---|
| 679 | def render_tag_option(self,e,t_att,g_att,v): |
---|
| 680 | f=self.eval_object(e.parentNode.getAttribute("t-form-select"),v) |
---|
| 681 | val=t_att["option"] |
---|
| 682 | g_att+=' value="%s"'%(val) |
---|
| 683 | if f.value==val: |
---|
| 684 | g_att+=' selected="selected"' |
---|
| 685 | return self.render_element(e,g_att,v) |
---|
| 686 | |
---|
| 687 | # HTML Tags others |
---|
| 688 | def render_tag_pager(self,e,t_att,g_att,v): |
---|
| 689 | pre=t_att["pager"] |
---|
| 690 | total=int(self.eval_str(t_att["total"],v)) |
---|
| 691 | start=int(self.eval_str(t_att["start"],v)) |
---|
| 692 | step=int(self.eval_str(t_att.get("step","100"),v)) |
---|
| 693 | scope=int(self.eval_str(t_att.get("scope","5"),v)) |
---|
| 694 | # Compute Pager |
---|
| 695 | p=pre+"_" |
---|
| 696 | d={} |
---|
| 697 | d[p+"tot_size"]=total |
---|
| 698 | d[p+"tot_page"]=tot_page=total/step |
---|
| 699 | d[p+"win_start0"]=total and start |
---|
| 700 | d[p+"win_start1"]=total and start+1 |
---|
| 701 | d[p+"win_end0"]=max(0,min(start+step-1,total-1)) |
---|
| 702 | d[p+"win_end1"]=min(start+step,total) |
---|
| 703 | d[p+"win_page0"]=win_page=start/step |
---|
| 704 | d[p+"win_page1"]=win_page+1 |
---|
| 705 | d[p+"prev"]=(win_page!=0) |
---|
| 706 | d[p+"prev_start"]=(win_page-1)*step |
---|
| 707 | d[p+"next"]=(tot_page>=win_page+1) |
---|
| 708 | d[p+"next_start"]=(win_page+1)*step |
---|
| 709 | l=[] |
---|
| 710 | begin=win_page-scope |
---|
| 711 | end=win_page+scope |
---|
| 712 | if begin<0: |
---|
| 713 | end-=begin |
---|
| 714 | if end>tot_page: |
---|
| 715 | begin-=(end-tot_page) |
---|
| 716 | i=max(0,begin) |
---|
| 717 | while i<=min(end,tot_page) and total!=step: |
---|
| 718 | l.append( { p+"page0":i, p+"page1":i+1, p+"start":i*step, p+"sel":(win_page==i) }) |
---|
| 719 | i+=1 |
---|
| 720 | d[p+"active"]=len(l)>1 |
---|
| 721 | d[p+"list"]=l |
---|
| 722 | # Update v |
---|
| 723 | v.update(d) |
---|
| 724 | return "" |
---|
| 725 | |
---|
| 726 | #---------------------------------------------------------- |
---|
| 727 | # QWeb Simple Controller |
---|
| 728 | #---------------------------------------------------------- |
---|
| 729 | def qweb_control(self,jump='main',p=[]): |
---|
| 730 | """ qweb_control(self,jump='main',p=[]): |
---|
| 731 | A simple function to handle the controler part of your application. It |
---|
| 732 | dispatch the control to the jump argument, while ensuring that prefix |
---|
| 733 | function have been called. |
---|
| 734 | |
---|
| 735 | qweb_control replace '/' to '_' and strip '_' from the jump argument. |
---|
| 736 | |
---|
| 737 | name1 |
---|
| 738 | name1_name2 |
---|
| 739 | name1_name2_name3 |
---|
| 740 | |
---|
| 741 | """ |
---|
| 742 | jump=jump.replace('/','_').strip('_') |
---|
| 743 | if not hasattr(self,jump): |
---|
| 744 | return 0 |
---|
| 745 | done={} |
---|
| 746 | todo=[] |
---|
| 747 | while 1: |
---|
| 748 | if jump!=None: |
---|
| 749 | tmp="" |
---|
| 750 | todo=[] |
---|
| 751 | for i in jump.split("_"): |
---|
| 752 | tmp+=i+"_"; |
---|
| 753 | if not done.has_key(tmp[:-1]): |
---|
| 754 | todo.append(tmp[:-1]) |
---|
| 755 | jump=None |
---|
| 756 | elif len(todo): |
---|
| 757 | i=todo.pop(0) |
---|
| 758 | done[i]=1 |
---|
| 759 | if hasattr(self,i): |
---|
| 760 | f=getattr(self,i) |
---|
| 761 | r=f(*p) |
---|
| 762 | if isinstance(r,types.StringType): |
---|
| 763 | jump=r |
---|
| 764 | else: |
---|
| 765 | break |
---|
| 766 | return 1 |
---|
| 767 | |
---|
| 768 | #---------------------------------------------------------- |
---|
| 769 | # QWeb WSGI Request handler |
---|
| 770 | #---------------------------------------------------------- |
---|
| 771 | class QWebSession(dict): |
---|
| 772 | def __init__(self,environ,**kw): |
---|
| 773 | dict.__init__(self) |
---|
| 774 | default={ |
---|
| 775 | "path" : tempfile.gettempdir(), |
---|
| 776 | "cookie_name" : "QWEBSID", |
---|
| 777 | "cookie_lifetime" : 0, |
---|
| 778 | "cookie_path" : '/', |
---|
| 779 | "cookie_domain" : '', |
---|
| 780 | "limit_cache" : 1, |
---|
| 781 | "probability" : 0.01, |
---|
| 782 | "maxlifetime" : 3600, |
---|
| 783 | "disable" : 0, |
---|
| 784 | } |
---|
| 785 | for k,v in default.items(): |
---|
| 786 | setattr(self,'session_%s'%k,kw.get(k,v)) |
---|
| 787 | # Try to find session |
---|
| 788 | self.session_found_cookie=0 |
---|
| 789 | self.session_found_url=0 |
---|
| 790 | self.session_found=0 |
---|
| 791 | self.session_orig="" |
---|
| 792 | # Try cookie |
---|
| 793 | c=Cookie.SimpleCookie() |
---|
| 794 | c.load(environ.get('HTTP_COOKIE', '')) |
---|
| 795 | if c.has_key(self.session_cookie_name): |
---|
| 796 | sid=c[self.session_cookie_name].value[:64] |
---|
| 797 | if re.match('[a-f0-9]+$',sid) and self.session_load(sid): |
---|
| 798 | self.session_id=sid |
---|
| 799 | self.session_found_cookie=1 |
---|
| 800 | self.session_found=1 |
---|
| 801 | # Try URL |
---|
| 802 | if not self.session_found_cookie: |
---|
| 803 | mo=re.search('&%s=([a-f0-9]+)'%self.session_cookie_name,environ.get('QUERY_STRING','')) |
---|
| 804 | if mo and self.session_load(mo.group(1)): |
---|
| 805 | self.session_id=mo.group(1) |
---|
| 806 | self.session_found_url=1 |
---|
| 807 | self.session_found=1 |
---|
| 808 | # New session |
---|
| 809 | if not self.session_found: |
---|
| 810 | self.session_id='%032x'%random.randint(1,2**128) |
---|
| 811 | self.session_trans_sid="&%s=%s"%(self.session_cookie_name,self.session_id) |
---|
| 812 | # Clean old session |
---|
| 813 | if random.random() < self.session_probability: |
---|
| 814 | self.session_clean() |
---|
| 815 | def session_get_headers(self): |
---|
| 816 | h=[] |
---|
| 817 | if (not self.session_disable) and (len(self) or len(self.session_orig)): |
---|
| 818 | self.session_save() |
---|
| 819 | if not self.session_found_cookie: |
---|
| 820 | c=Cookie.SimpleCookie() |
---|
| 821 | c[self.session_cookie_name] = self.session_id |
---|
| 822 | c[self.session_cookie_name]['path'] = self.session_cookie_path |
---|
| 823 | if self.session_cookie_domain: |
---|
| 824 | c[self.session_cookie_name]['domain'] = self.session_cookie_domain |
---|
| 825 | # if self.session_cookie_lifetime: |
---|
| 826 | # c[self.session_cookie_name]['expires'] = TODO date localtime or not, datetime.datetime(1970, 1, 1) |
---|
| 827 | h.append(("Set-Cookie", c[self.session_cookie_name].OutputString())) |
---|
| 828 | if self.session_limit_cache: |
---|
| 829 | h.append(('Cache-Control','no-store, no-cache, must-revalidate, post-check=0, pre-check=0')) |
---|
| 830 | h.append(('Expires','Thu, 19 Nov 1981 08:52:00 GMT')) |
---|
| 831 | h.append(('Pragma','no-cache')) |
---|
| 832 | return h |
---|
| 833 | def session_load(self,sid): |
---|
| 834 | fname=os.path.join(self.session_path,'qweb_sess_%s'%sid) |
---|
| 835 | try: |
---|
| 836 | orig=file(fname).read() |
---|
| 837 | d=pickle.loads(orig) |
---|
| 838 | except: |
---|
| 839 | return |
---|
| 840 | self.session_orig=orig |
---|
| 841 | self.update(d) |
---|
| 842 | return 1 |
---|
| 843 | def session_save(self): |
---|
| 844 | if not os.path.isdir(self.session_path): |
---|
| 845 | os.makedirs(self.session_path) |
---|
| 846 | fname=os.path.join(self.session_path,'qweb_sess_%s'%self.session_id) |
---|
| 847 | try: |
---|
| 848 | oldtime=os.path.getmtime(fname) |
---|
| 849 | except OSError,IOError: |
---|
| 850 | oldtime=0 |
---|
| 851 | dump=pickle.dumps(self.copy()) |
---|
| 852 | if (dump != self.session_orig) or (time.time() > oldtime+self.session_maxlifetime/4): |
---|
| 853 | tmpname=os.path.join(self.session_path,'qweb_sess_%s_%x'%(self.session_id,random.randint(1,2**32))) |
---|
| 854 | f=file(tmpname,'wb') |
---|
| 855 | f.write(dump) |
---|
| 856 | f.close() |
---|
| 857 | if sys.platform=='win32' and os.path.isfile(fname): |
---|
| 858 | os.remove(fname) |
---|
| 859 | os.rename(tmpname,fname) |
---|
| 860 | def session_clean(self): |
---|
| 861 | t=time.time() |
---|
| 862 | try: |
---|
| 863 | for i in [os.path.join(self.session_path,i) for i in os.listdir(self.session_path) if i.startswith('qweb_sess_')]: |
---|
| 864 | if (t > os.path.getmtime(i)+self.session_maxlifetime): |
---|
| 865 | os.unlink(i) |
---|
| 866 | except OSError,IOError: |
---|
| 867 | pass |
---|
| 868 | class QWebSessionMem(QWebSession): |
---|
| 869 | def session_load(self,sid): |
---|
| 870 | global _qweb_sessions |
---|
| 871 | if not "_qweb_sessions" in globals(): |
---|
| 872 | _qweb_sessions={} |
---|
| 873 | if _qweb_sessions.has_key(sid): |
---|
| 874 | self.session_orig=_qweb_sessions[sid] |
---|
| 875 | self.update(self.session_orig) |
---|
| 876 | return 1 |
---|
| 877 | def session_save(self): |
---|
| 878 | global _qweb_sessions |
---|
| 879 | if not "_qweb_sessions" in globals(): |
---|
| 880 | _qweb_sessions={} |
---|
| 881 | _qweb_sessions[self.session_id]=self.copy() |
---|
| 882 | class QWebSessionService: |
---|
| 883 | def __init__(self, wsgiapp, url_rewrite=0): |
---|
| 884 | self.wsgiapp=wsgiapp |
---|
| 885 | self.url_rewrite_tags="a=href,area=href,frame=src,form=,fieldset=" |
---|
| 886 | def __call__(self, environ, start_response): |
---|
| 887 | # TODO |
---|
| 888 | # use QWebSession to provide environ["qweb.session"] |
---|
| 889 | return self.wsgiapp(environ,start_response) |
---|
| 890 | class QWebDict(dict): |
---|
| 891 | def __init__(self,*p): |
---|
| 892 | dict.__init__(self,*p) |
---|
| 893 | def __getitem__(self,key): |
---|
| 894 | return self.get(key,"") |
---|
| 895 | def int(self,key): |
---|
| 896 | try: |
---|
| 897 | return int(self.get(key,"0")) |
---|
| 898 | except ValueError: |
---|
| 899 | return 0 |
---|
| 900 | class QWebListDict(dict): |
---|
| 901 | def __init__(self,*p): |
---|
| 902 | dict.__init__(self,*p) |
---|
| 903 | def __getitem__(self,key): |
---|
| 904 | return self.get(key,[]) |
---|
| 905 | def appendlist(self,key,val): |
---|
| 906 | if self.has_key(key): |
---|
| 907 | self[key].append(val) |
---|
| 908 | else: |
---|
| 909 | self[key]=[val] |
---|
| 910 | def get_qwebdict(self): |
---|
| 911 | d=QWebDict() |
---|
| 912 | for k,v in self.items(): |
---|
| 913 | d[k]=v[-1] |
---|
| 914 | return d |
---|
| 915 | class QWebRequest: |
---|
| 916 | """QWebRequest a WSGI request handler. |
---|
| 917 | |
---|
| 918 | QWebRequest is a WSGI request handler that feature GET, POST and POST |
---|
| 919 | multipart methods, handles cookies and headers and provide a dict-like |
---|
| 920 | SESSION Object (either on the filesystem or in memory). |
---|
| 921 | |
---|
| 922 | It is constructed with the environ and start_response WSGI arguments: |
---|
| 923 | |
---|
| 924 | req=qweb.QWebRequest(environ, start_response) |
---|
| 925 | |
---|
| 926 | req has the folowing attributes : |
---|
| 927 | |
---|
| 928 | req.environ standard WSGI dict (CGI and wsgi ones) |
---|
| 929 | |
---|
| 930 | Some CGI vars as attributes from environ for convenience: |
---|
| 931 | |
---|
| 932 | req.SCRIPT_NAME |
---|
| 933 | req.PATH_INFO |
---|
| 934 | req.REQUEST_URI |
---|
| 935 | |
---|
| 936 | Some computed value (also for convenience) |
---|
| 937 | |
---|
| 938 | req.FULL_URL full URL recontructed (http://host/query) |
---|
| 939 | req.FULL_PATH (URL path before ?querystring) |
---|
| 940 | |
---|
| 941 | Dict constructed from querystring and POST datas, PHP-like. |
---|
| 942 | |
---|
| 943 | req.GET contains GET vars |
---|
| 944 | req.POST contains POST vars |
---|
| 945 | req.REQUEST contains merge of GET and POST |
---|
| 946 | req.FILES contains uploaded files |
---|
| 947 | req.GET_LIST req.POST_LIST req.REQUEST_LIST req.FILES_LIST multiple arguments versions |
---|
| 948 | req.debug() returns an HTML dump of those vars |
---|
| 949 | |
---|
| 950 | A dict-like session object. |
---|
| 951 | |
---|
| 952 | req.SESSION the session start when the dict is not empty. |
---|
| 953 | |
---|
| 954 | Attribute for handling the response |
---|
| 955 | |
---|
| 956 | req.response_headers dict-like to set headers |
---|
| 957 | req.response_cookies a SimpleCookie to set cookies |
---|
| 958 | req.response_status a string to set the status like '200 OK' |
---|
| 959 | |
---|
| 960 | req.write() to write to the buffer |
---|
| 961 | |
---|
| 962 | req itselfs is an iterable object with the buffer, it will also also call |
---|
| 963 | start_response automatically before returning anything via the iterator. |
---|
| 964 | |
---|
| 965 | To make it short, it means that you may use |
---|
| 966 | |
---|
| 967 | return req |
---|
| 968 | |
---|
| 969 | at the end of your request handling to return the reponse to any WSGI |
---|
| 970 | application server. |
---|
| 971 | """ |
---|
| 972 | # |
---|
| 973 | # This class contains part ripped from colubrid (with the permission of |
---|
| 974 | # mitsuhiko) see http://wsgiarea.pocoo.org/colubrid/ |
---|
| 975 | # |
---|
| 976 | # - the class HttpHeaders |
---|
| 977 | # - the method load_post_data (tuned version) |
---|
| 978 | # |
---|
| 979 | class HttpHeaders(object): |
---|
| 980 | def __init__(self): |
---|
| 981 | self.data = [('Content-Type', 'text/html')] |
---|
| 982 | def __setitem__(self, key, value): |
---|
| 983 | self.set(key, value) |
---|
| 984 | def __delitem__(self, key): |
---|
| 985 | self.remove(key) |
---|
| 986 | def __contains__(self, key): |
---|
| 987 | key = key.lower() |
---|
| 988 | for k, v in self.data: |
---|
| 989 | if k.lower() == key: |
---|
| 990 | return True |
---|
| 991 | return False |
---|
| 992 | def add(self, key, value): |
---|
| 993 | self.data.append((key, value)) |
---|
| 994 | def remove(self, key, count=-1): |
---|
| 995 | removed = 0 |
---|
| 996 | data = [] |
---|
| 997 | for _key, _value in self.data: |
---|
| 998 | if _key.lower() != key.lower(): |
---|
| 999 | if count > -1: |
---|
| 1000 | if removed >= count: |
---|
| 1001 | break |
---|
| 1002 | else: |
---|
| 1003 | removed += 1 |
---|
| 1004 | data.append((_key, _value)) |
---|
| 1005 | self.data = data |
---|
| 1006 | def clear(self): |
---|
| 1007 | self.data = [] |
---|
| 1008 | def set(self, key, value): |
---|
| 1009 | self.remove(key) |
---|
| 1010 | self.add(key, value) |
---|
| 1011 | def get(self, key=False, httpformat=False): |
---|
| 1012 | if not key: |
---|
| 1013 | result = self.data |
---|
| 1014 | else: |
---|
| 1015 | result = [] |
---|
| 1016 | for _key, _value in self.data: |
---|
| 1017 | if _key.lower() == key.lower(): |
---|
| 1018 | result.append((_key, _value)) |
---|
| 1019 | if httpformat: |
---|
| 1020 | return '\n'.join(['%s: %s' % item for item in result]) |
---|
| 1021 | return result |
---|
| 1022 | def load_post_data(self,environ,POST,FILES): |
---|
| 1023 | length = int(environ['CONTENT_LENGTH']) |
---|
| 1024 | DATA = environ['wsgi.input'].read(length) |
---|
| 1025 | if environ.get('CONTENT_TYPE', '').startswith('multipart'): |
---|
| 1026 | lines = ['Content-Type: %s' % environ.get('CONTENT_TYPE', '')] |
---|
| 1027 | for key, value in environ.items(): |
---|
| 1028 | if key.startswith('HTTP_'): |
---|
| 1029 | lines.append('%s: %s' % (key, value)) |
---|
| 1030 | raw = '\r\n'.join(lines) + '\r\n\r\n' + DATA |
---|
| 1031 | msg = email.message_from_string(raw) |
---|
| 1032 | for sub in msg.get_payload(): |
---|
| 1033 | if not isinstance(sub, email.Message.Message): |
---|
| 1034 | continue |
---|
| 1035 | name_dict = cgi.parse_header(sub['Content-Disposition'])[1] |
---|
| 1036 | if 'filename' in name_dict: |
---|
| 1037 | # Nested MIME Messages are not supported' |
---|
| 1038 | if type([]) == type(sub.get_payload()): |
---|
| 1039 | continue |
---|
| 1040 | if not name_dict['filename'].strip(): |
---|
| 1041 | continue |
---|
| 1042 | filename = name_dict['filename'] |
---|
| 1043 | # why not keep all the filename? because IE always send 'C:\documents and settings\blub\blub.png' |
---|
| 1044 | filename = filename[filename.rfind('\\') + 1:] |
---|
| 1045 | if 'Content-Type' in sub: |
---|
| 1046 | content_type = sub['Content-Type'] |
---|
| 1047 | else: |
---|
| 1048 | content_type = None |
---|
| 1049 | s = { "name":filename, "type":content_type, "data":sub.get_payload() } |
---|
| 1050 | FILES.appendlist(name_dict['name'], s) |
---|
| 1051 | else: |
---|
| 1052 | POST.appendlist(name_dict['name'], sub.get_payload()) |
---|
| 1053 | else: |
---|
| 1054 | POST.update(cgi.parse_qs(DATA,keep_blank_values=1)) |
---|
| 1055 | return DATA |
---|
| 1056 | |
---|
| 1057 | def __init__(self,environ,start_response,session=QWebSession): |
---|
| 1058 | self.environ=environ |
---|
| 1059 | self.start_response=start_response |
---|
| 1060 | self.buffer=[] |
---|
| 1061 | |
---|
| 1062 | self.SCRIPT_NAME = environ.get('SCRIPT_NAME', '') |
---|
| 1063 | self.PATH_INFO = environ.get('PATH_INFO', '') |
---|
| 1064 | # extensions: |
---|
| 1065 | self.FULL_URL = environ['FULL_URL'] = self.get_full_url(environ) |
---|
| 1066 | # REQUEST_URI is optional, fake it if absent |
---|
| 1067 | if not environ.has_key("REQUEST_URI"): |
---|
| 1068 | environ["REQUEST_URI"]=urllib.quote(self.SCRIPT_NAME+self.PATH_INFO) |
---|
| 1069 | if environ.get('QUERY_STRING'): |
---|
| 1070 | environ["REQUEST_URI"]+='?'+environ['QUERY_STRING'] |
---|
| 1071 | self.REQUEST_URI = environ["REQUEST_URI"] |
---|
| 1072 | # full quote url path before the ? |
---|
| 1073 | self.FULL_PATH = environ['FULL_PATH'] = self.REQUEST_URI.split('?')[0] |
---|
| 1074 | |
---|
| 1075 | self.request_cookies=Cookie.SimpleCookie() |
---|
| 1076 | self.request_cookies.load(environ.get('HTTP_COOKIE', '')) |
---|
| 1077 | |
---|
| 1078 | self.response_started=False |
---|
| 1079 | self.response_gzencode=False |
---|
| 1080 | self.response_cookies=Cookie.SimpleCookie() |
---|
| 1081 | # to delete a cookie use: c[key]['expires'] = datetime.datetime(1970, 1, 1) |
---|
| 1082 | self.response_headers=self.HttpHeaders() |
---|
| 1083 | self.response_status="200 OK" |
---|
| 1084 | |
---|
| 1085 | self.php=None |
---|
| 1086 | if self.environ.has_key("php"): |
---|
| 1087 | self.php=environ["php"] |
---|
| 1088 | self.SESSION=self.php._SESSION |
---|
| 1089 | self.GET=self.php._GET |
---|
| 1090 | self.POST=self.php._POST |
---|
| 1091 | self.REQUEST=self.php._ARG |
---|
| 1092 | self.FILES=self.php._FILES |
---|
| 1093 | else: |
---|
| 1094 | if isinstance(session,QWebSession): |
---|
| 1095 | self.SESSION=session |
---|
| 1096 | elif session: |
---|
| 1097 | self.SESSION=session(environ) |
---|
| 1098 | else: |
---|
| 1099 | self.SESSION=None |
---|
| 1100 | self.GET_LIST=QWebListDict(cgi.parse_qs(environ.get('QUERY_STRING', ''),keep_blank_values=1)) |
---|
| 1101 | self.POST_LIST=QWebListDict() |
---|
| 1102 | self.FILES_LIST=QWebListDict() |
---|
| 1103 | self.REQUEST_LIST=QWebListDict(self.GET_LIST) |
---|
| 1104 | if environ['REQUEST_METHOD'] == 'POST': |
---|
| 1105 | self.DATA=self.load_post_data(environ,self.POST_LIST,self.FILES_LIST) |
---|
| 1106 | self.REQUEST_LIST.update(self.POST_LIST) |
---|
| 1107 | self.GET=self.GET_LIST.get_qwebdict() |
---|
| 1108 | self.POST=self.POST_LIST.get_qwebdict() |
---|
| 1109 | self.FILES=self.FILES_LIST.get_qwebdict() |
---|
| 1110 | self.REQUEST=self.REQUEST_LIST.get_qwebdict() |
---|
| 1111 | def get_full_url(environ): |
---|
| 1112 | # taken from PEP 333 |
---|
| 1113 | if 'FULL_URL' in environ: |
---|
| 1114 | return environ['FULL_URL'] |
---|
| 1115 | url = environ['wsgi.url_scheme']+'://' |
---|
| 1116 | if environ.get('HTTP_HOST'): |
---|
| 1117 | url += environ['HTTP_HOST'] |
---|
| 1118 | else: |
---|
| 1119 | url += environ['SERVER_NAME'] |
---|
| 1120 | if environ['wsgi.url_scheme'] == 'https': |
---|
| 1121 | if environ['SERVER_PORT'] != '443': |
---|
| 1122 | url += ':' + environ['SERVER_PORT'] |
---|
| 1123 | else: |
---|
| 1124 | if environ['SERVER_PORT'] != '80': |
---|
| 1125 | url += ':' + environ['SERVER_PORT'] |
---|
| 1126 | if environ.has_key('REQUEST_URI'): |
---|
| 1127 | url += environ['REQUEST_URI'] |
---|
| 1128 | else: |
---|
| 1129 | url += urllib.quote(environ.get('SCRIPT_NAME', '')) |
---|
| 1130 | url += urllib.quote(environ.get('PATH_INFO', '')) |
---|
| 1131 | if environ.get('QUERY_STRING'): |
---|
| 1132 | url += '?' + environ['QUERY_STRING'] |
---|
| 1133 | return url |
---|
| 1134 | get_full_url=staticmethod(get_full_url) |
---|
| 1135 | def save_files(self): |
---|
| 1136 | for k,v in self.FILES.items(): |
---|
| 1137 | if not v.has_key("tmp_file"): |
---|
| 1138 | f=tempfile.NamedTemporaryFile() |
---|
| 1139 | f.write(v["data"]) |
---|
| 1140 | f.flush() |
---|
| 1141 | v["tmp_file"]=f |
---|
| 1142 | v["tmp_name"]=f.name |
---|
| 1143 | def debug(self): |
---|
| 1144 | body='' |
---|
| 1145 | for name,d in [ |
---|
| 1146 | ("GET",self.GET), ("POST",self.POST), ("REQUEST",self.REQUEST), ("FILES",self.FILES), |
---|
| 1147 | ("GET_LIST",self.GET_LIST), ("POST_LIST",self.POST_LIST), ("REQUEST_LIST",self.REQUEST_LIST), ("FILES_LIST",self.FILES_LIST), |
---|
| 1148 | ("SESSION",self.SESSION), ("environ",self.environ), |
---|
| 1149 | ]: |
---|
| 1150 | body+='<table border="1" width="100%" align="center">\n' |
---|
| 1151 | body+='<tr><th colspan="2" align="center">%s</th></tr>\n'%name |
---|
| 1152 | keys=d.keys() |
---|
| 1153 | keys.sort() |
---|
| 1154 | body+=''.join(['<tr><td>%s</td><td>%s</td></tr>\n'%(k,cgi.escape(repr(d[k]))) for k in keys]) |
---|
| 1155 | body+='</table><br><br>\n\n' |
---|
| 1156 | return body |
---|
| 1157 | def write(self,s): |
---|
| 1158 | self.buffer.append(s) |
---|
| 1159 | def echo(self,*s): |
---|
| 1160 | self.buffer.extend([str(i) for i in s]) |
---|
| 1161 | def response(self): |
---|
| 1162 | if not self.response_started: |
---|
| 1163 | if not self.php: |
---|
| 1164 | for k,v in self.FILES.items(): |
---|
| 1165 | if v.has_key("tmp_file"): |
---|
| 1166 | try: |
---|
| 1167 | v["tmp_file"].close() |
---|
| 1168 | except OSError: |
---|
| 1169 | pass |
---|
| 1170 | if self.response_gzencode and self.environ.get('HTTP_ACCEPT_ENCODING','').find('gzip')!=-1: |
---|
| 1171 | zbuf=StringIO.StringIO() |
---|
| 1172 | zfile=gzip.GzipFile(mode='wb', fileobj=zbuf) |
---|
| 1173 | zfile.write(''.join(self.buffer)) |
---|
| 1174 | zfile.close() |
---|
| 1175 | zbuf=zbuf.getvalue() |
---|
| 1176 | self.buffer=[zbuf] |
---|
| 1177 | self.response_headers['Content-Encoding']="gzip" |
---|
| 1178 | self.response_headers['Content-Length']=str(len(zbuf)) |
---|
| 1179 | headers = self.response_headers.get() |
---|
| 1180 | if isinstance(self.SESSION, QWebSession): |
---|
| 1181 | headers.extend(self.SESSION.session_get_headers()) |
---|
| 1182 | headers.extend([('Set-Cookie', self.response_cookies[i].OutputString()) for i in self.response_cookies]) |
---|
| 1183 | self.start_response(self.response_status, headers) |
---|
| 1184 | self.response_started=True |
---|
| 1185 | return self.buffer |
---|
| 1186 | def __iter__(self): |
---|
| 1187 | return self.response().__iter__() |
---|
| 1188 | def http_redirect(self,url,permanent=1): |
---|
| 1189 | if permanent: |
---|
| 1190 | self.response_status="301 Moved Permanently" |
---|
| 1191 | else: |
---|
| 1192 | self.response_status="302 Found" |
---|
| 1193 | self.response_headers["Location"]=url |
---|
| 1194 | def http_404(self,msg="<h1>404 Not Found</h1>"): |
---|
| 1195 | self.response_status="404 Not Found" |
---|
| 1196 | if msg: |
---|
| 1197 | self.write(msg) |
---|
| 1198 | def http_download(self,fname,fstr,partial=0): |
---|
| 1199 | # allow fstr to be a file-like object |
---|
| 1200 | # if parital: |
---|
| 1201 | # say accept ranages |
---|
| 1202 | # parse range headers... |
---|
| 1203 | # if range: |
---|
| 1204 | # header("HTTP/1.1 206 Partial Content"); |
---|
| 1205 | # header("Content-Range: bytes $offset-".($fsize-1)."/".$fsize); |
---|
| 1206 | # header("Content-Length: ".($fsize-$offset)); |
---|
| 1207 | # fseek($fd,$offset); |
---|
| 1208 | # else: |
---|
| 1209 | self.response_headers["Content-Type"]="application/octet-stream" |
---|
| 1210 | self.response_headers["Content-Disposition"]="attachment; filename=\"%s\""%fname |
---|
| 1211 | self.response_headers["Content-Transfer-Encoding"]="binary" |
---|
| 1212 | self.response_headers["Content-Length"]="%d"%len(fstr) |
---|
| 1213 | self.write(fstr) |
---|
| 1214 | |
---|
| 1215 | #---------------------------------------------------------- |
---|
| 1216 | # QWeb WSGI HTTP Server to run any WSGI app |
---|
| 1217 | # autorun, run an app as FCGI or CGI otherwise launch the server |
---|
| 1218 | #---------------------------------------------------------- |
---|
| 1219 | class QWebWSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
---|
| 1220 | def log_message(self,*p): |
---|
| 1221 | if self.server.log: |
---|
| 1222 | return BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,*p) |
---|
| 1223 | def address_string(self): |
---|
| 1224 | return self.client_address[0] |
---|
| 1225 | def start_response(self,status,headers): |
---|
| 1226 | l=status.split(' ',1) |
---|
| 1227 | self.send_response(int(l[0]),l[1]) |
---|
| 1228 | ctype_sent=0 |
---|
| 1229 | for i in headers: |
---|
| 1230 | if i[0].lower()=="content-type": |
---|
| 1231 | ctype_sent=1 |
---|
| 1232 | self.send_header(*i) |
---|
| 1233 | if not ctype_sent: |
---|
| 1234 | self.send_header("Content-type", "text/html") |
---|
| 1235 | self.end_headers() |
---|
| 1236 | return self.write |
---|
| 1237 | def write(self,data): |
---|
| 1238 | try: |
---|
| 1239 | self.wfile.write(data) |
---|
| 1240 | except (socket.error, socket.timeout),e: |
---|
| 1241 | print e |
---|
| 1242 | def bufferon(self): |
---|
| 1243 | if not getattr(self,'wfile_buf',0): |
---|
| 1244 | self.wfile_buf=1 |
---|
| 1245 | self.wfile_bak=self.wfile |
---|
| 1246 | self.wfile=StringIO.StringIO() |
---|
| 1247 | def bufferoff(self): |
---|
| 1248 | if self.wfile_buf: |
---|
| 1249 | buf=self.wfile |
---|
| 1250 | self.wfile=self.wfile_bak |
---|
| 1251 | self.write(buf.getvalue()) |
---|
| 1252 | self.wfile_buf=0 |
---|
| 1253 | def serve(self,type): |
---|
| 1254 | path_info, parameters, query = urlparse.urlparse(self.path)[2:5] |
---|
| 1255 | environ = { |
---|
| 1256 | 'wsgi.version': (1,0), |
---|
| 1257 | 'wsgi.url_scheme': 'http', |
---|
| 1258 | 'wsgi.input': self.rfile, |
---|
| 1259 | 'wsgi.errors': sys.stderr, |
---|
| 1260 | 'wsgi.multithread': 0, |
---|
| 1261 | 'wsgi.multiprocess': 0, |
---|
| 1262 | 'wsgi.run_once': 0, |
---|
| 1263 | 'REQUEST_METHOD': self.command, |
---|
| 1264 | 'SCRIPT_NAME': '', |
---|
| 1265 | 'QUERY_STRING': query, |
---|
| 1266 | 'CONTENT_TYPE': self.headers.get('Content-Type', ''), |
---|
| 1267 | 'CONTENT_LENGTH': self.headers.get('Content-Length', ''), |
---|
| 1268 | 'REMOTE_ADDR': self.client_address[0], |
---|
| 1269 | 'REMOTE_PORT': str(self.client_address[1]), |
---|
| 1270 | 'SERVER_NAME': self.server.server_address[0], |
---|
| 1271 | 'SERVER_PORT': str(self.server.server_address[1]), |
---|
| 1272 | 'SERVER_PROTOCOL': self.request_version, |
---|
| 1273 | # extention |
---|
| 1274 | 'FULL_PATH': self.path, |
---|
| 1275 | 'qweb.mode': 'standalone', |
---|
| 1276 | } |
---|
| 1277 | if path_info: |
---|
| 1278 | environ['PATH_INFO'] = urllib.unquote(path_info) |
---|
| 1279 | for key, value in self.headers.items(): |
---|
| 1280 | environ['HTTP_' + key.upper().replace('-', '_')] = value |
---|
| 1281 | # Hack to avoid may TCP packets |
---|
| 1282 | self.bufferon() |
---|
| 1283 | appiter=self.server.wsgiapp(environ, self.start_response) |
---|
| 1284 | for data in appiter: |
---|
| 1285 | self.write(data) |
---|
| 1286 | self.bufferoff() |
---|
| 1287 | self.bufferoff() |
---|
| 1288 | def do_GET(self): |
---|
| 1289 | self.serve('GET') |
---|
| 1290 | def do_POST(self): |
---|
| 1291 | self.serve('GET') |
---|
| 1292 | class QWebWSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): |
---|
| 1293 | """ QWebWSGIServer |
---|
| 1294 | qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1) |
---|
| 1295 | A WSGI HTTP server threaded or not and a function to automatically run your |
---|
| 1296 | app according to the environement (either standalone, CGI or FastCGI). |
---|
| 1297 | |
---|
| 1298 | This feature is called QWeb autorun. If you want to To use it on your |
---|
| 1299 | application use the following lines at the end of the main application |
---|
| 1300 | python file: |
---|
| 1301 | |
---|
| 1302 | if __name__ == '__main__': |
---|
| 1303 | qweb.qweb_wsgi_autorun(your_wsgi_app) |
---|
| 1304 | |
---|
| 1305 | this function will select the approriate running mode according to the |
---|
| 1306 | calling environement (http-server, FastCGI or CGI). |
---|
| 1307 | """ |
---|
| 1308 | def __init__(self, wsgiapp, ip, port, threaded=1, log=1): |
---|
| 1309 | BaseHTTPServer.HTTPServer.__init__(self, (ip, port), QWebWSGIHandler) |
---|
| 1310 | self.wsgiapp = wsgiapp |
---|
| 1311 | self.threaded = threaded |
---|
| 1312 | self.log = log |
---|
| 1313 | def process_request(self,*p): |
---|
| 1314 | if self.threaded: |
---|
| 1315 | return SocketServer.ThreadingMixIn.process_request(self,*p) |
---|
| 1316 | else: |
---|
| 1317 | return BaseHTTPServer.HTTPServer.process_request(self,*p) |
---|
| 1318 | def qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1,log=1,callback_ready=None): |
---|
| 1319 | if sys.platform=='win32': |
---|
| 1320 | fcgi=0 |
---|
| 1321 | else: |
---|
| 1322 | fcgi=1 |
---|
| 1323 | sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM) |
---|
| 1324 | try: |
---|
| 1325 | sock.getpeername() |
---|
| 1326 | except socket.error, e: |
---|
| 1327 | if e[0] == errno.ENOTSOCK: |
---|
| 1328 | fcgi=0 |
---|
| 1329 | if fcgi or os.environ.has_key('REQUEST_METHOD'): |
---|
| 1330 | import fcgi |
---|
| 1331 | fcgi.WSGIServer(wsgiapp,multithreaded=False).run() |
---|
| 1332 | else: |
---|
| 1333 | if log: |
---|
| 1334 | print 'Serving on %s:%d'%(ip,port) |
---|
| 1335 | s=QWebWSGIServer(wsgiapp,ip=ip,port=port,threaded=threaded,log=log) |
---|
| 1336 | if callback_ready: |
---|
| 1337 | callback_ready() |
---|
| 1338 | try: |
---|
| 1339 | s.serve_forever() |
---|
| 1340 | except KeyboardInterrupt,e: |
---|
| 1341 | sys.excepthook(*sys.exc_info()) |
---|
| 1342 | |
---|
| 1343 | #---------------------------------------------------------- |
---|
| 1344 | # Qweb Documentation |
---|
| 1345 | #---------------------------------------------------------- |
---|
| 1346 | def qweb_doc(): |
---|
| 1347 | body=__doc__ |
---|
| 1348 | for i in [QWebXml ,QWebHtml ,QWebForm ,QWebURL ,qweb_control ,QWebRequest ,QWebSession ,QWebWSGIServer ,qweb_wsgi_autorun]: |
---|
| 1349 | n=i.__name__ |
---|
| 1350 | d=i.__doc__ |
---|
| 1351 | body+='\n\n%s\n%s\n\n%s'%(n,'-'*len(n),d) |
---|
| 1352 | return body |
---|
| 1353 | |
---|
| 1354 | print qweb_doc() |
---|
| 1355 | |
---|
| 1356 | # |
---|