Package gluon :: Module tools
[hide private]
[frames] | no frames]

Source Code for Module gluon.tools

   1  #!/bin/python 
   2  # -*- coding: utf-8 -*- 
   3   
   4  """ 
   5  This file is part of the web2py Web Framework Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
   6  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
   7  """ 
   8   
   9  import base64 
  10  import cPickle 
  11  import datetime 
  12  import thread 
  13  import logging 
  14  import sys 
  15  import glob 
  16  import os 
  17  import re 
  18  import time 
  19  import traceback 
  20  import smtplib 
  21  import urllib 
  22  import urllib2 
  23  import Cookie 
  24  import cStringIO 
  25  from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset 
  26   
  27  from gluon.contenttype import contenttype 
  28  from gluon.storage import Storage, StorageList, Settings, Messages 
  29  from gluon.utils import web2py_uuid 
  30  from gluon.fileutils import read_file, check_credentials 
  31  from gluon import * 
  32  from gluon.contrib.autolinks import expand_one 
  33  from gluon.contrib.markmin.markmin2html import \ 
  34      replace_at_urls, replace_autolinks, replace_components 
  35  from gluon.dal import Row, Set, Query 
  36   
  37  import gluon.serializers as serializers 
  38   
  39  try: 
  40      # try stdlib (Python 2.6) 
  41      import json as json_parser 
  42  except ImportError: 
  43      try: 
  44          # try external module 
  45          import simplejson as json_parser 
  46      except: 
  47          # fallback to pure-Python module 
  48          import contrib.simplejson as json_parser 
  49   
  50  __all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki', 
  51             'PluginManager', 'fetch', 'geocode', 'prettydate'] 
  52   
  53  ### mind there are two loggers here (logger and crud.settings.logger)! 
  54  logger = logging.getLogger("web2py") 
  55   
  56  DEFAULT = lambda: None 
57 58 59 -def getarg(position, default=None):
60 args = current.request.args 61 if position < 0 and len(args) >= -position: 62 return args[position] 63 elif position >= 0 and len(args) > position: 64 return args[position] 65 else: 66 return default
67
68 69 -def callback(actions, form, tablename=None):
70 if actions: 71 if tablename and isinstance(actions, dict): 72 actions = actions.get(tablename, []) 73 if not isinstance(actions, (list, tuple)): 74 actions = [actions] 75 [action(form) for action in actions]
76
77 78 -def validators(*a):
79 b = [] 80 for item in a: 81 if isinstance(item, (list, tuple)): 82 b = b + list(item) 83 else: 84 b.append(item) 85 return b
86
87 88 -def call_or_redirect(f, *args):
89 if callable(f): 90 redirect(f(*args)) 91 else: 92 redirect(f)
93
94 95 -def replace_id(url, form):
96 if url: 97 url = url.replace('[id]', str(form.vars.id)) 98 if url[0] == '/' or url[:4] == 'http': 99 return url 100 return URL(url)
101
102 103 -class Mail(object):
104 """ 105 Class for configuring and sending emails with alternative text / html 106 body, multiple attachments and encryption support 107 108 Works with SMTP and Google App Engine. 109 """ 110
111 - class Attachment(MIMEBase.MIMEBase):
112 """ 113 Email attachment 114 115 Arguments: 116 117 payload: path to file or file-like object with read() method 118 filename: name of the attachment stored in message; if set to 119 None, it will be fetched from payload path; file-like 120 object payload must have explicit filename specified 121 content_id: id of the attachment; automatically contained within 122 < and > 123 content_type: content type of the attachment; if set to None, 124 it will be fetched from filename using gluon.contenttype 125 module 126 encoding: encoding of all strings passed to this function (except 127 attachment body) 128 129 Content ID is used to identify attachments within the html body; 130 in example, attached image with content ID 'photo' may be used in 131 html message as a source of img tag <img src="cid:photo" />. 132 133 Examples: 134 135 #Create attachment from text file: 136 attachment = Mail.Attachment('/path/to/file.txt') 137 138 Content-Type: text/plain 139 MIME-Version: 1.0 140 Content-Disposition: attachment; filename="file.txt" 141 Content-Transfer-Encoding: base64 142 143 SOMEBASE64CONTENT= 144 145 #Create attachment from image file with custom filename and cid: 146 attachment = Mail.Attachment('/path/to/file.png', 147 filename='photo.png', 148 content_id='photo') 149 150 Content-Type: image/png 151 MIME-Version: 1.0 152 Content-Disposition: attachment; filename="photo.png" 153 Content-Id: <photo> 154 Content-Transfer-Encoding: base64 155 156 SOMEOTHERBASE64CONTENT= 157 """ 158
159 - def __init__( 160 self, 161 payload, 162 filename=None, 163 content_id=None, 164 content_type=None, 165 encoding='utf-8'):
166 if isinstance(payload, str): 167 if filename is None: 168 filename = os.path.basename(payload) 169 payload = read_file(payload, 'rb') 170 else: 171 if filename is None: 172 raise Exception('Missing attachment name') 173 payload = payload.read() 174 filename = filename.encode(encoding) 175 if content_type is None: 176 content_type = contenttype(filename) 177 self.my_filename = filename 178 self.my_payload = payload 179 MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1)) 180 self.set_payload(payload) 181 self['Content-Disposition'] = 'attachment; filename="%s"' % filename 182 if not content_id is None: 183 self['Content-Id'] = '<%s>' % content_id.encode(encoding) 184 Encoders.encode_base64(self)
185
186 - def __init__(self, server=None, sender=None, login=None, tls=True):
187 """ 188 Main Mail object 189 190 Arguments: 191 192 server: SMTP server address in address:port notation 193 sender: sender email address 194 login: sender login name and password in login:password notation 195 or None if no authentication is required 196 tls: enables/disables encryption (True by default) 197 198 In Google App Engine use: 199 200 server='gae' 201 202 For sake of backward compatibility all fields are optional and default 203 to None, however, to be able to send emails at least server and sender 204 must be specified. They are available under following fields: 205 206 mail.settings.server 207 mail.settings.sender 208 mail.settings.login 209 210 When server is 'logging', email is logged but not sent (debug mode) 211 212 Optionally you can use PGP encryption or X509: 213 214 mail.settings.cipher_type = None 215 mail.settings.gpg_home = None 216 mail.settings.sign = True 217 mail.settings.sign_passphrase = None 218 mail.settings.encrypt = True 219 mail.settings.x509_sign_keyfile = None 220 mail.settings.x509_sign_certfile = None 221 mail.settings.x509_nocerts = False 222 mail.settings.x509_crypt_certfiles = None 223 224 cipher_type : None 225 gpg - need a python-pyme package and gpgme lib 226 x509 - smime 227 gpg_home : you can set a GNUPGHOME environment variable 228 to specify home of gnupg 229 sign : sign the message (True or False) 230 sign_passphrase : passphrase for key signing 231 encrypt : encrypt the message 232 ... x509 only ... 233 x509_sign_keyfile : the signers private key filename (PEM format) 234 x509_sign_certfile: the signers certificate filename (PEM format) 235 x509_nocerts : if True then no attached certificate in mail 236 x509_crypt_certfiles: the certificates file to encrypt the messages 237 with can be a file name or a list of 238 file names (PEM format) 239 240 Examples: 241 242 #Create Mail object with authentication data for remote server: 243 mail = Mail('example.com:25', 'me@example.com', 'me:password') 244 """ 245 246 settings = self.settings = Settings() 247 settings.server = server 248 settings.sender = sender 249 settings.login = login 250 settings.tls = tls 251 settings.hostname = None 252 settings.ssl = False 253 settings.cipher_type = None 254 settings.gpg_home = None 255 settings.sign = True 256 settings.sign_passphrase = None 257 settings.encrypt = True 258 settings.x509_sign_keyfile = None 259 settings.x509_sign_certfile = None 260 settings.x509_nocerts = False 261 settings.x509_crypt_certfiles = None 262 settings.debug = False 263 settings.lock_keys = True 264 self.result = {} 265 self.error = None
266
267 - def send( 268 self, 269 to, 270 subject = '[no subject]', 271 message = '[no message]', 272 attachments=None, 273 cc=None, 274 bcc=None, 275 reply_to=None, 276 sender=None, 277 encoding='utf-8', 278 raw=False, 279 headers={} 280 ):
281 """ 282 Sends an email using data specified in constructor 283 284 Arguments: 285 286 to: list or tuple of receiver addresses; will also accept single 287 object 288 subject: subject of the email 289 message: email body text; depends on type of passed object: 290 if 2-list or 2-tuple is passed: first element will be 291 source of plain text while second of html text; 292 otherwise: object will be the only source of plain text 293 and html source will be set to None; 294 If text or html source is: 295 None: content part will be ignored, 296 string: content part will be set to it, 297 file-like object: content part will be fetched from 298 it using it's read() method 299 attachments: list or tuple of Mail.Attachment objects; will also 300 accept single object 301 cc: list or tuple of carbon copy receiver addresses; will also 302 accept single object 303 bcc: list or tuple of blind carbon copy receiver addresses; will 304 also accept single object 305 reply_to: address to which reply should be composed 306 encoding: encoding of all strings passed to this method (including 307 message bodies) 308 headers: dictionary of headers to refine the headers just before 309 sending mail, e.g. {'Return-Path' : 'bounces@example.org'} 310 311 Examples: 312 313 #Send plain text message to single address: 314 mail.send('you@example.com', 315 'Message subject', 316 'Plain text body of the message') 317 318 #Send html message to single address: 319 mail.send('you@example.com', 320 'Message subject', 321 '<html>Plain text body of the message</html>') 322 323 #Send text and html message to three addresses (two in cc): 324 mail.send('you@example.com', 325 'Message subject', 326 ('Plain text body', '<html>html body</html>'), 327 cc=['other1@example.com', 'other2@example.com']) 328 329 #Send html only message with image attachment available from 330 the message by 'photo' content id: 331 mail.send('you@example.com', 332 'Message subject', 333 (None, '<html><img src="cid:photo" /></html>'), 334 Mail.Attachment('/path/to/photo.jpg' 335 content_id='photo')) 336 337 #Send email with two attachments and no body text 338 mail.send('you@example.com, 339 'Message subject', 340 None, 341 [Mail.Attachment('/path/to/fist.file'), 342 Mail.Attachment('/path/to/second.file')]) 343 344 Returns True on success, False on failure. 345 346 Before return, method updates two object's fields: 347 self.result: return value of smtplib.SMTP.sendmail() or GAE's 348 mail.send_mail() method 349 self.error: Exception message or None if above was successful 350 """ 351 352 # We don't want to use base64 encoding for unicode mail 353 Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8') 354 355 def encode_header(key): 356 if [c for c in key if 32 > ord(c) or ord(c) > 127]: 357 return Header.Header(key.encode('utf-8'), 'utf-8') 358 else: 359 return key
360 361 # encoded or raw text 362 def encoded_or_raw(text): 363 if raw: 364 text = encode_header(text) 365 return text
366 367 sender = sender or self.settings.sender 368 369 if not isinstance(self.settings.server, str): 370 raise Exception('Server address not specified') 371 if not isinstance(sender, str): 372 raise Exception('Sender address not specified') 373 374 if not raw and attachments: 375 # Use multipart/mixed if there is attachments 376 payload_in = MIMEMultipart.MIMEMultipart('mixed') 377 elif raw: 378 # no encoding configuration for raw messages 379 if not isinstance(message, basestring): 380 message = message.read() 381 if isinstance(message, unicode): 382 text = message.encode('utf-8') 383 elif not encoding == 'utf-8': 384 text = message.decode(encoding).encode('utf-8') 385 else: 386 text = message 387 # No charset passed to avoid transport encoding 388 # NOTE: some unicode encoded strings will produce 389 # unreadable mail contents. 390 payload_in = MIMEText.MIMEText(text) 391 if to: 392 if not isinstance(to, (list, tuple)): 393 to = [to] 394 else: 395 raise Exception('Target receiver address not specified') 396 if cc: 397 if not isinstance(cc, (list, tuple)): 398 cc = [cc] 399 if bcc: 400 if not isinstance(bcc, (list, tuple)): 401 bcc = [bcc] 402 if message is None: 403 text = html = None 404 elif isinstance(message, (list, tuple)): 405 text, html = message 406 elif message.strip().startswith('<html') and \ 407 message.strip().endswith('</html>'): 408 text = self.settings.server == 'gae' and message or None 409 html = message 410 else: 411 text = message 412 html = None 413 414 if (not text is None or not html is None) and (not raw): 415 416 if not text is None: 417 if not isinstance(text, basestring): 418 text = text.read() 419 if isinstance(text, unicode): 420 text = text.encode('utf-8') 421 elif not encoding == 'utf-8': 422 text = text.decode(encoding).encode('utf-8') 423 if not html is None: 424 if not isinstance(html, basestring): 425 html = html.read() 426 if isinstance(html, unicode): 427 html = html.encode('utf-8') 428 elif not encoding == 'utf-8': 429 html = html.decode(encoding).encode('utf-8') 430 431 # Construct mime part only if needed 432 if text and html: 433 # We have text and html we need multipart/alternative 434 attachment = MIMEMultipart.MIMEMultipart('alternative') 435 attachment.attach(MIMEText.MIMEText(text, _charset='utf-8')) 436 attachment.attach( 437 MIMEText.MIMEText(html, 'html', _charset='utf-8')) 438 elif text: 439 attachment = MIMEText.MIMEText(text, _charset='utf-8') 440 elif html: 441 attachment = \ 442 MIMEText.MIMEText(html, 'html', _charset='utf-8') 443 444 if attachments: 445 # If there is attachments put text and html into 446 # multipart/mixed 447 payload_in.attach(attachment) 448 else: 449 # No attachments no multipart/mixed 450 payload_in = attachment 451 452 if (attachments is None) or raw: 453 pass 454 elif isinstance(attachments, (list, tuple)): 455 for attachment in attachments: 456 payload_in.attach(attachment) 457 else: 458 payload_in.attach(attachments) 459 460 ####################################################### 461 # CIPHER # 462 ####################################################### 463 cipher_type = self.settings.cipher_type 464 sign = self.settings.sign 465 sign_passphrase = self.settings.sign_passphrase 466 encrypt = self.settings.encrypt 467 ####################################################### 468 # GPGME # 469 ####################################################### 470 if cipher_type == 'gpg': 471 if self.settings.gpg_home: 472 # Set GNUPGHOME environment variable to set home of gnupg 473 import os 474 os.environ['GNUPGHOME'] = self.settings.gpg_home 475 if not sign and not encrypt: 476 self.error = "No sign and no encrypt is set but cipher type to gpg" 477 return False 478 479 # need a python-pyme package and gpgme lib 480 from pyme import core, errors 481 from pyme.constants.sig import mode 482 ############################################ 483 # sign # 484 ############################################ 485 if sign: 486 import string 487 core.check_version(None) 488 pin = string.replace(payload_in.as_string(), '\n', '\r\n') 489 plain = core.Data(pin) 490 sig = core.Data() 491 c = core.Context() 492 c.set_armor(1) 493 c.signers_clear() 494 # search for signing key for From: 495 for sigkey in c.op_keylist_all(sender, 1): 496 if sigkey.can_sign: 497 c.signers_add(sigkey) 498 if not c.signers_enum(0): 499 self.error = 'No key for signing [%s]' % sender 500 return False 501 c.set_passphrase_cb(lambda x, y, z: sign_passphrase) 502 try: 503 # make a signature 504 c.op_sign(plain, sig, mode.DETACH) 505 sig.seek(0, 0) 506 # make it part of the email 507 payload = MIMEMultipart.MIMEMultipart('signed', 508 boundary=None, 509 _subparts=None, 510 **dict( 511 micalg="pgp-sha1", 512 protocol="application/pgp-signature")) 513 # insert the origin payload 514 payload.attach(payload_in) 515 # insert the detached signature 516 p = MIMEBase.MIMEBase("application", 'pgp-signature') 517 p.set_payload(sig.read()) 518 payload.attach(p) 519 # it's just a trick to handle the no encryption case 520 payload_in = payload 521 except errors.GPGMEError, ex: 522 self.error = "GPG error: %s" % ex.getstring() 523 return False 524 ############################################ 525 # encrypt # 526 ############################################ 527 if encrypt: 528 core.check_version(None) 529 plain = core.Data(payload_in.as_string()) 530 cipher = core.Data() 531 c = core.Context() 532 c.set_armor(1) 533 # collect the public keys for encryption 534 recipients = [] 535 rec = to[:] 536 if cc: 537 rec.extend(cc) 538 if bcc: 539 rec.extend(bcc) 540 for addr in rec: 541 c.op_keylist_start(addr, 0) 542 r = c.op_keylist_next() 543 if r is None: 544 self.error = 'No key for [%s]' % addr 545 return False 546 recipients.append(r) 547 try: 548 # make the encryption 549 c.op_encrypt(recipients, 1, plain, cipher) 550 cipher.seek(0, 0) 551 # make it a part of the email 552 payload = MIMEMultipart.MIMEMultipart('encrypted', 553 boundary=None, 554 _subparts=None, 555 **dict(protocol="application/pgp-encrypted")) 556 p = MIMEBase.MIMEBase("application", 'pgp-encrypted') 557 p.set_payload("Version: 1\r\n") 558 payload.attach(p) 559 p = MIMEBase.MIMEBase("application", 'octet-stream') 560 p.set_payload(cipher.read()) 561 payload.attach(p) 562 except errors.GPGMEError, ex: 563 self.error = "GPG error: %s" % ex.getstring() 564 return False 565 ####################################################### 566 # X.509 # 567 ####################################################### 568 elif cipher_type == 'x509': 569 if not sign and not encrypt: 570 self.error = "No sign and no encrypt is set but cipher type to x509" 571 return False 572 x509_sign_keyfile = self.settings.x509_sign_keyfile 573 if self.settings.x509_sign_certfile: 574 x509_sign_certfile = self.settings.x509_sign_certfile 575 else: 576 # if there is no sign certfile we'll assume the 577 # cert is in keyfile 578 x509_sign_certfile = self.settings.x509_sign_keyfile 579 # crypt certfiles could be a string or a list 580 x509_crypt_certfiles = self.settings.x509_crypt_certfiles 581 x509_nocerts = self.settings.x509_nocerts 582 583 # need m2crypto 584 try: 585 from M2Crypto import BIO, SMIME, X509 586 except Exception, e: 587 self.error = "Can't load M2Crypto module" 588 return False 589 msg_bio = BIO.MemoryBuffer(payload_in.as_string()) 590 s = SMIME.SMIME() 591 592 # SIGN 593 if sign: 594 #key for signing 595 try: 596 s.load_key(x509_sign_keyfile, x509_sign_certfile, 597 callback=lambda x: sign_passphrase) 598 except Exception, e: 599 self.error = "Something went wrong on certificate / private key loading: <%s>" % str(e) 600 return False 601 try: 602 if x509_nocerts: 603 flags = SMIME.PKCS7_NOCERTS 604 else: 605 flags = 0 606 if not encrypt: 607 flags += SMIME.PKCS7_DETACHED 608 p7 = s.sign(msg_bio, flags=flags) 609 msg_bio = BIO.MemoryBuffer(payload_in.as_string( 610 )) # Recreate coz sign() has consumed it. 611 except Exception, e: 612 self.error = "Something went wrong on signing: <%s> %s" % ( 613 str(e), str(flags)) 614 return False 615 616 # ENCRYPT 617 if encrypt: 618 try: 619 sk = X509.X509_Stack() 620 if not isinstance(x509_crypt_certfiles, (list, tuple)): 621 x509_crypt_certfiles = [x509_crypt_certfiles] 622 623 # make an encryption cert's stack 624 for x in x509_crypt_certfiles: 625 sk.push(X509.load_cert(x)) 626 s.set_x509_stack(sk) 627 628 s.set_cipher(SMIME.Cipher('des_ede3_cbc')) 629 tmp_bio = BIO.MemoryBuffer() 630 if sign: 631 s.write(tmp_bio, p7) 632 else: 633 tmp_bio.write(payload_in.as_string()) 634 p7 = s.encrypt(tmp_bio) 635 except Exception, e: 636 self.error = "Something went wrong on encrypting: <%s>" % str(e) 637 return False 638 639 # Final stage in sign and encryption 640 out = BIO.MemoryBuffer() 641 if encrypt: 642 s.write(out, p7) 643 else: 644 if sign: 645 s.write(out, p7, msg_bio, SMIME.PKCS7_DETACHED) 646 else: 647 out.write('\r\n') 648 out.write(payload_in.as_string()) 649 out.close() 650 st = str(out.read()) 651 payload = message_from_string(st) 652 else: 653 # no cryptography process as usual 654 payload = payload_in 655 656 payload['From'] = encoded_or_raw(sender.decode(encoding)) 657 origTo = to[:] 658 if to: 659 payload['To'] = encoded_or_raw(', '.join(to).decode(encoding)) 660 if reply_to: 661 payload['Reply-To'] = encoded_or_raw(reply_to.decode(encoding)) 662 if cc: 663 payload['Cc'] = encoded_or_raw(', '.join(cc).decode(encoding)) 664 to.extend(cc) 665 if bcc: 666 to.extend(bcc) 667 payload['Subject'] = encoded_or_raw(subject.decode(encoding)) 668 payload['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", 669 time.gmtime()) 670 for k, v in headers.iteritems(): 671 payload[k] = encoded_or_raw(v.decode(encoding)) 672 result = {} 673 try: 674 if self.settings.server == 'logging': 675 logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % 676 ('-' * 40, sender, 677 ', '.join(to), subject, 678 text or html, '-' * 40)) 679 elif self.settings.server == 'gae': 680 xcc = dict() 681 if cc: 682 xcc['cc'] = cc 683 if bcc: 684 xcc['bcc'] = bcc 685 if reply_to: 686 xcc['reply_to'] = reply_to 687 from google.appengine.api import mail 688 attachments = attachments and [(a.my_filename, a.my_payload) for a in attachments if not raw] 689 if attachments: 690 result = mail.send_mail( 691 sender=sender, to=origTo, 692 subject=subject, body=text, html=html, 693 attachments=attachments, **xcc) 694 elif html and (not raw): 695 result = mail.send_mail( 696 sender=sender, to=origTo, 697 subject=subject, body=text, html=html, **xcc) 698 else: 699 result = mail.send_mail( 700 sender=sender, to=origTo, 701 subject=subject, body=text, **xcc) 702 else: 703 smtp_args = self.settings.server.split(':') 704 if self.settings.ssl: 705 server = smtplib.SMTP_SSL(*smtp_args) 706 else: 707 server = smtplib.SMTP(*smtp_args) 708 if self.settings.tls and not self.settings.ssl: 709 server.ehlo(self.settings.hostname) 710 server.starttls() 711 server.ehlo(self.settings.hostname) 712 if self.settings.login: 713 server.login(*self.settings.login.split(':', 1)) 714 result = server.sendmail( 715 sender, to, payload.as_string()) 716 server.quit() 717 except Exception, e: 718 logger.warn('Mail.send failure:%s' % e) 719 self.result = result 720 self.error = e 721 return False 722 self.result = result 723 self.error = None 724 return True 725
726 727 -class Recaptcha(DIV):
728 729 """ 730 Usage: 731 732 form = FORM(Recaptcha(public_key='...',private_key='...')) 733 734 or 735 736 form = SQLFORM(...) 737 form.append(Recaptcha(public_key='...',private_key='...')) 738 """ 739 740 API_SSL_SERVER = 'https://www.google.com/recaptcha/api' 741 API_SERVER = 'http://www.google.com/recaptcha/api' 742 VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify' 743
744 - def __init__( 745 self, 746 request=None, 747 public_key='', 748 private_key='', 749 use_ssl=False, 750 error=None, 751 error_message='invalid', 752 label='Verify:', 753 options='' 754 ):
755 self.request_vars = request and request.vars or current.request.vars 756 self.remote_addr = request.env.remote_addr 757 self.public_key = public_key 758 self.private_key = private_key 759 self.use_ssl = use_ssl 760 self.error = error 761 self.errors = Storage() 762 self.error_message = error_message 763 self.components = [] 764 self.attributes = {} 765 self.label = label 766 self.options = options 767 self.comment = ''
768
769 - def _validate(self):
770 771 # for local testing: 772 773 recaptcha_challenge_field = \ 774 self.request_vars.recaptcha_challenge_field 775 recaptcha_response_field = \ 776 self.request_vars.recaptcha_response_field 777 private_key = self.private_key 778 remoteip = self.remote_addr 779 if not (recaptcha_response_field and recaptcha_challenge_field 780 and len(recaptcha_response_field) 781 and len(recaptcha_challenge_field)): 782 self.errors['captcha'] = self.error_message 783 return False 784 params = urllib.urlencode({ 785 'privatekey': private_key, 786 'remoteip': remoteip, 787 'challenge': recaptcha_challenge_field, 788 'response': recaptcha_response_field, 789 }) 790 request = urllib2.Request( 791 url=self.VERIFY_SERVER, 792 data=params, 793 headers={'Content-type': 'application/x-www-form-urlencoded', 794 'User-agent': 'reCAPTCHA Python'}) 795 httpresp = urllib2.urlopen(request) 796 return_values = httpresp.read().splitlines() 797 httpresp.close() 798 return_code = return_values[0] 799 if return_code == 'true': 800 del self.request_vars.recaptcha_challenge_field 801 del self.request_vars.recaptcha_response_field 802 self.request_vars.captcha = '' 803 return True 804 else: 805 # In case we get an error code, store it so we can get an error message 806 # from the /api/challenge URL as described in the reCAPTCHA api docs. 807 self.error = return_values[1] 808 self.errors['captcha'] = self.error_message 809 return False
810
811 - def xml(self):
812 public_key = self.public_key 813 use_ssl = self.use_ssl 814 error_param = '' 815 if self.error: 816 error_param = '&error=%s' % self.error 817 if use_ssl: 818 server = self.API_SSL_SERVER 819 else: 820 server = self.API_SERVER 821 captcha = DIV( 822 SCRIPT("var RecaptchaOptions = {%s};" % self.options), 823 SCRIPT(_type="text/javascript", 824 _src="%s/challenge?k=%s%s" % (server, public_key, error_param)), 825 TAG.noscript( 826 IFRAME( 827 _src="%s/noscript?k=%s%s" % ( 828 server, public_key, error_param), 829 _height="300", _width="500", _frameborder="0"), BR(), 830 INPUT( 831 _type='hidden', _name='recaptcha_response_field', 832 _value='manual_challenge')), _id='recaptcha') 833 if not self.errors.captcha: 834 return XML(captcha).xml() 835 else: 836 captcha.append(DIV(self.errors['captcha'], _class='error')) 837 return XML(captcha).xml()
838
839 840 -def addrow(form, a, b, c, style, _id, position=-1):
841 if style == "divs": 842 form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'), 843 DIV(b, _class='w2p_fw'), 844 DIV(c, _class='w2p_fc'), 845 _id=_id)) 846 elif style == "table2cols": 847 form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'), 848 TD(c, _class='w2p_fc'))) 849 form[0].insert(position + 1, TR(TD(b, _class='w2p_fw'), 850 _colspan=2, _id=_id)) 851 elif style == "ul": 852 form[0].insert(position, LI(DIV(LABEL(a), _class='w2p_fl'), 853 DIV(b, _class='w2p_fw'), 854 DIV(c, _class='w2p_fc'), 855 _id=_id)) 856 elif style == "bootstrap": 857 form[0].insert(position, DIV(LABEL(a, _class='control-label'), 858 DIV(b, SPAN(c, _class='inline-help'), 859 _class='controls'), 860 _class='control-group', _id=_id)) 861 else: 862 form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'), 863 TD(b, _class='w2p_fw'), 864 TD(c, _class='w2p_fc'), _id=_id))
865
866 867 -class Auth(object):
868 869 default_settings = dict( 870 hideerror=False, 871 password_min_length=4, 872 cas_maps=None, 873 reset_password_requires_verification=False, 874 registration_requires_verification=False, 875 registration_requires_approval=False, 876 login_after_registration=False, 877 login_after_password_change=True, 878 alternate_requires_registration=False, 879 create_user_groups="user_%(id)s", 880 everybody_group_id=None, 881 manager_group_role=None, 882 login_captcha=None, 883 register_captcha=None, 884 retrieve_username_captcha=None, 885 retrieve_password_captcha=None, 886 captcha=None, 887 expiration=3600, # one hour 888 long_expiration=3600 * 30 * 24, # one month 889 remember_me_form=True, 890 allow_basic_login=False, 891 allow_basic_login_only=False, 892 on_failed_authentication=lambda x: redirect(x), 893 formstyle="table3cols", 894 label_separator=": ", 895 allow_delete_accounts=False, 896 password_field='password', 897 table_user_name='auth_user', 898 table_group_name='auth_group', 899 table_membership_name='auth_membership', 900 table_permission_name='auth_permission', 901 table_event_name='auth_event', 902 table_cas_name='auth_cas', 903 table_user=None, 904 table_group=None, 905 table_membership=None, 906 table_permission=None, 907 table_event=None, 908 table_cas=None, 909 showid=False, 910 use_username=False, 911 login_email_validate=True, 912 login_userfield=None, 913 logout_onlogout=None, 914 register_fields=None, 915 register_verify_password=True, 916 profile_fields=None, 917 email_case_sensitive=True, 918 username_case_sensitive=True, 919 update_fields = ['email'], 920 ondelete="CASCADE", 921 client_side = True, 922 wiki = Settings(), 923 ) 924 # ## these are messages that can be customized 925 default_messages = dict( 926 login_button='Login', 927 register_button='Register', 928 password_reset_button='Request reset password', 929 password_change_button='Change password', 930 profile_save_button='Apply changes', 931 submit_button='Submit', 932 verify_password='Verify Password', 933 delete_label='Check to delete', 934 function_disabled='Function disabled', 935 access_denied='Insufficient privileges', 936 registration_verifying='Registration needs verification', 937 registration_pending='Registration is pending approval', 938 login_disabled='Login disabled by administrator', 939 logged_in='Logged in', 940 email_sent='Email sent', 941 unable_to_send_email='Unable to send email', 942 email_verified='Email verified', 943 logged_out='Logged out', 944 registration_successful='Registration successful', 945 invalid_email='Invalid email', 946 unable_send_email='Unable to send email', 947 invalid_login='Invalid login', 948 invalid_user='Invalid user', 949 invalid_password='Invalid password', 950 is_empty="Cannot be empty", 951 mismatched_password="Password fields don't match", 952 verify_email='Click on the link %(link)s to verify your email', 953 verify_email_subject='Email verification', 954 username_sent='Your username was emailed to you', 955 new_password_sent='A new password was emailed to you', 956 password_changed='Password changed', 957 retrieve_username='Your username is: %(username)s', 958 retrieve_username_subject='Username retrieve', 959 retrieve_password='Your password is: %(password)s', 960 retrieve_password_subject='Password retrieve', 961 reset_password= 962 'Click on the link %(link)s to reset your password', 963 reset_password_subject='Password reset', 964 invalid_reset_password='Invalid reset password', 965 profile_updated='Profile updated', 966 new_password='New password', 967 old_password='Old password', 968 group_description='Group uniquely assigned to user %(id)s', 969 register_log='User %(id)s Registered', 970 login_log='User %(id)s Logged-in', 971 login_failed_log=None, 972 logout_log='User %(id)s Logged-out', 973 profile_log='User %(id)s Profile updated', 974 verify_email_log='User %(id)s Verification email sent', 975 retrieve_username_log='User %(id)s Username retrieved', 976 retrieve_password_log='User %(id)s Password retrieved', 977 reset_password_log='User %(id)s Password reset', 978 change_password_log='User %(id)s Password changed', 979 add_group_log='Group %(group_id)s created', 980 del_group_log='Group %(group_id)s deleted', 981 add_membership_log=None, 982 del_membership_log=None, 983 has_membership_log=None, 984 add_permission_log=None, 985 del_permission_log=None, 986 has_permission_log=None, 987 impersonate_log='User %(id)s is impersonating %(other_id)s', 988 label_first_name='First name', 989 label_last_name='Last name', 990 label_username='Username', 991 label_email='E-mail', 992 label_password='Password', 993 label_registration_key='Registration key', 994 label_reset_password_key='Reset Password key', 995 label_registration_id='Registration identifier', 996 label_role='Role', 997 label_description='Description', 998 label_user_id='User ID', 999 label_group_id='Group ID', 1000 label_name='Name', 1001 label_table_name='Object or table name', 1002 label_record_id='Record ID', 1003 label_time_stamp='Timestamp', 1004 label_client_ip='Client IP', 1005 label_origin='Origin', 1006 label_remember_me="Remember me (for 30 days)", 1007 verify_password_comment='please input your password again', 1008 ) 1009 1010 """ 1011 Class for authentication, authorization, role based access control. 1012 1013 Includes: 1014 1015 - registration and profile 1016 - login and logout 1017 - username and password retrieval 1018 - event logging 1019 - role creation and assignment 1020 - user defined group/role based permission 1021 1022 Authentication Example: 1023 1024 from contrib.utils import * 1025 mail=Mail() 1026 mail.settings.server='smtp.gmail.com:587' 1027 mail.settings.sender='you@somewhere.com' 1028 mail.settings.login='username:password' 1029 auth=Auth(db) 1030 auth.settings.mailer=mail 1031 # auth.settings....=... 1032 auth.define_tables() 1033 def authentication(): 1034 return dict(form=auth()) 1035 1036 exposes: 1037 1038 - http://.../{application}/{controller}/authentication/login 1039 - http://.../{application}/{controller}/authentication/logout 1040 - http://.../{application}/{controller}/authentication/register 1041 - http://.../{application}/{controller}/authentication/verify_email 1042 - http://.../{application}/{controller}/authentication/retrieve_username 1043 - http://.../{application}/{controller}/authentication/retrieve_password 1044 - http://.../{application}/{controller}/authentication/reset_password 1045 - http://.../{application}/{controller}/authentication/profile 1046 - http://.../{application}/{controller}/authentication/change_password 1047 1048 On registration a group with role=new_user.id is created 1049 and user is given membership of this group. 1050 1051 You can create a group with: 1052 1053 group_id=auth.add_group('Manager', 'can access the manage action') 1054 auth.add_permission(group_id, 'access to manage') 1055 1056 Here \"access to manage\" is just a user defined string. 1057 You can give access to a user: 1058 1059 auth.add_membership(group_id, user_id) 1060 1061 If user id is omitted, the logged in user is assumed 1062 1063 Then you can decorate any action: 1064 1065 @auth.requires_permission('access to manage') 1066 def manage(): 1067 return dict() 1068 1069 You can restrict a permission to a specific table: 1070 1071 auth.add_permission(group_id, 'edit', db.sometable) 1072 @auth.requires_permission('edit', db.sometable) 1073 1074 Or to a specific record: 1075 1076 auth.add_permission(group_id, 'edit', db.sometable, 45) 1077 @auth.requires_permission('edit', db.sometable, 45) 1078 1079 If authorization is not granted calls: 1080 1081 auth.settings.on_failed_authorization 1082 1083 Other options: 1084 1085 auth.settings.mailer=None 1086 auth.settings.expiration=3600 # seconds 1087 1088 ... 1089 1090 ### these are messages that can be customized 1091 ... 1092 """ 1093 1094 @staticmethod
1095 - def get_or_create_key(filename=None, alg='sha512'):
1096 request = current.request 1097 if not filename: 1098 filename = os.path.join(request.folder, 'private', 'auth.key') 1099 if os.path.exists(filename): 1100 key = open(filename, 'r').read().strip() 1101 else: 1102 key = alg + ':' + web2py_uuid() 1103 open(filename, 'w').write(key) 1104 return key
1105
1106 - def url(self, f=None, args=None, vars=None, scheme=False):
1107 if args is None: 1108 args = [] 1109 if vars is None: 1110 vars = {} 1111 return URL(c=self.settings.controller, 1112 f=f, args=args, vars=vars, scheme=scheme)
1113
1114 - def here(self):
1115 return URL(args=current.request.args, vars=current.request.vars)
1116
1117 - def __init__(self, environment=None, db=None, mailer=True, 1118 hmac_key=None, controller='default', function='user', 1119 cas_provider=None, signature=True, secure=False):
1120 """ 1121 auth=Auth(db) 1122 1123 - environment is there for legacy but unused (awful) 1124 - db has to be the database where to create tables for authentication 1125 - mailer=Mail(...) or None (no mailed) or True (make a mailer) 1126 - hmac_key can be a hmac_key or hmac_key=Auth.get_or_create_key() 1127 - controller (where is the user action?) 1128 - cas_provider (delegate authentication to the URL, CAS2) 1129 """ 1130 ## next two lines for backward compatibility 1131 if not db and environment and isinstance(environment, DAL): 1132 db = environment 1133 self.db = db 1134 self.environment = current 1135 request = current.request 1136 session = current.session 1137 auth = session.auth 1138 self.user_groups = auth and auth.user_groups or {} 1139 if secure: 1140 request.requires_https() 1141 if auth and auth.last_visit and auth.last_visit + \ 1142 datetime.timedelta(days=0, seconds=auth.expiration) > request.now: 1143 self.user = auth.user 1144 # this is a trick to speed up sessions 1145 if (request.now - auth.last_visit).seconds > (auth.expiration / 10): 1146 auth.last_visit = request.now 1147 else: 1148 self.user = None 1149 if session.auth: 1150 del session.auth 1151 # ## what happens after login? 1152 1153 self.next = current.request.vars._next 1154 if isinstance(self.next, (list, tuple)): 1155 self.next = self.next[0] 1156 url_index = URL(controller, 'index') 1157 url_login = URL(controller, function, args='login') 1158 # ## what happens after registration? 1159 1160 settings = self.settings = Settings() 1161 settings.update(Auth.default_settings) 1162 settings.update( 1163 cas_domains=[request.env.http_host], 1164 cas_provider=cas_provider, 1165 cas_actions=dict(login='login', 1166 validate='validate', 1167 servicevalidate='serviceValidate', 1168 proxyvalidate='proxyValidate', 1169 logout='logout'), 1170 extra_fields={}, 1171 actions_disabled=[], 1172 controller=controller, 1173 function=function, 1174 login_url=url_login, 1175 logged_url=URL(controller, function, args='profile'), 1176 download_url=URL(controller, 'download'), 1177 mailer=(mailer == True) and Mail() or mailer, 1178 on_failed_authorization = 1179 URL(controller, function, args='not_authorized'), 1180 login_next = url_index, 1181 login_onvalidation = [], 1182 login_onaccept = [], 1183 login_onfail = [], 1184 login_methods = [self], 1185 login_form = self, 1186 logout_next = url_index, 1187 logout_onlogout = None, 1188 register_next = url_index, 1189 register_onvalidation = [], 1190 register_onaccept = [], 1191 verify_email_next = url_login, 1192 verify_email_onaccept = [], 1193 profile_next = url_index, 1194 profile_onvalidation = [], 1195 profile_onaccept = [], 1196 retrieve_username_next = url_index, 1197 retrieve_password_next = url_index, 1198 request_reset_password_next = url_login, 1199 reset_password_next = url_index, 1200 change_password_next = url_index, 1201 change_password_onvalidation = [], 1202 change_password_onaccept = [], 1203 retrieve_password_onvalidation = [], 1204 reset_password_onvalidation = [], 1205 reset_password_onaccept = [], 1206 hmac_key = hmac_key, 1207 ) 1208 settings.lock_keys = True 1209 1210 # ## these are messages that can be customized 1211 messages = self.messages = Messages(current.T) 1212 messages.update(Auth.default_messages) 1213 messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'), 1214 'Please ', 1215 A('login', 1216 _href=self.settings.login_url + 1217 ('?_next=' + urllib.quote(current.request.env.http_web2py_component_location)) 1218 if current.request.env.http_web2py_component_location else ''), 1219 ' to view this content.', 1220 _class='not-authorized alert alert-block')) 1221 messages.lock_keys = True 1222 1223 # for "remember me" option 1224 response = current.response 1225 if auth and auth.remember: 1226 # when user wants to be logged in for longer 1227 response.cookies[response.session_id_name]["expires"] = \ 1228 auth.expiration 1229 if signature: 1230 self.define_signature() 1231 else: 1232 self.signature = None
1233
1234 - def _get_user_id(self):
1235 "accessor for auth.user_id" 1236 return self.user and self.user.id or None
1237 1238 user_id = property(_get_user_id, doc="user.id or None") 1239
1240 - def table_user(self):
1241 return self.db[self.settings.table_user_name]
1242
1243 - def table_group(self):
1244 return self.db[self.settings.table_group_name]
1245
1246 - def table_membership(self):
1247 return self.db[self.settings.table_membership_name]
1248
1249 - def table_permission(self):
1250 return self.db[self.settings.table_permission_name]
1251
1252 - def table_event(self):
1253 return self.db[self.settings.table_event_name]
1254
1255 - def table_cas(self):
1256 return self.db[self.settings.table_cas_name]
1257
1258 - def _HTTP(self, *a, **b):
1259 """ 1260 only used in lambda: self._HTTP(404) 1261 """ 1262 1263 raise HTTP(*a, **b)
1264
1265 - def __call__(self):
1266 """ 1267 usage: 1268 1269 def authentication(): return dict(form=auth()) 1270 """ 1271 1272 request = current.request 1273 args = request.args 1274 if not args: 1275 redirect(self.url(args='login', vars=request.vars)) 1276 elif args[0] in self.settings.actions_disabled: 1277 raise HTTP(404) 1278 if args[0] in ('login', 'logout', 'register', 'verify_email', 1279 'retrieve_username', 'retrieve_password', 1280 'reset_password', 'request_reset_password', 1281 'change_password', 'profile', 'groups', 1282 'impersonate', 'not_authorized'): 1283 if len(request.args) >= 2 and args[0] == 'impersonate': 1284 return getattr(self, args[0])(request.args[1]) 1285 else: 1286 return getattr(self, args[0])() 1287 elif args[0] == 'cas' and not self.settings.cas_provider: 1288 if args(1) == self.settings.cas_actions['login']: 1289 return self.cas_login(version=2) 1290 elif args(1) == self.settings.cas_actions['validate']: 1291 return self.cas_validate(version=1) 1292 elif args(1) == self.settings.cas_actions['servicevalidate']: 1293 return self.cas_validate(version=2, proxy=False) 1294 elif args(1) == self.settings.cas_actions['proxyvalidate']: 1295 return self.cas_validate(version=2, proxy=True) 1296 elif args(1) == self.settings.cas_actions['logout']: 1297 return self.logout(next=request.vars.service or DEFAULT) 1298 else: 1299 raise HTTP(404)
1300
1301 - def navbar(self, prefix='Welcome', action=None, 1302 separators=(' [ ', ' | ', ' ] '), user_identifier=DEFAULT, 1303 referrer_actions=DEFAULT, mode='default'):
1304 def Anr(*a,**b): 1305 b['_rel']='nofollow' 1306 return A(*a,**b)
1307 referrer_actions = [] if not referrer_actions else referrer_actions 1308 request = current.request 1309 asdropdown = (mode == 'dropdown') 1310 T = current.T 1311 if isinstance(prefix, str): 1312 prefix = T(prefix) 1313 if prefix: 1314 prefix = prefix.strip() + ' ' 1315 if not action: 1316 action = self.url(self.settings.function) 1317 s1, s2, s3 = separators 1318 if URL() == action: 1319 next = '' 1320 else: 1321 next = '?_next=' + urllib.quote(URL(args=request.args, 1322 vars=request.get_vars)) 1323 href = lambda function: '%s/%s%s' % (action, function, 1324 next if referrer_actions is DEFAULT or function in referrer_actions else '') 1325 1326 if self.user_id: 1327 if user_identifier is DEFAULT: 1328 user_identifier = '%(first_name)s' 1329 if callable(user_identifier): 1330 user_identifier = user_identifier(self.user) 1331 elif ((isinstance(user_identifier, str) or 1332 type(user_identifier).__name__ == 'lazyT') and 1333 re.search(r'%\(.+\)s', user_identifier)): 1334 user_identifier = user_identifier % self.user 1335 if not user_identifier: 1336 user_identifier = '' 1337 logout = Anr(T('Logout'), _href='%s/logout?_next=%s' % 1338 (action, urllib.quote(self.settings.logout_next))) 1339 profile = Anr(T('Profile'), _href=href('profile')) 1340 password = Anr(T('Password'), _href=href('change_password')) 1341 bar = SPAN( 1342 prefix, user_identifier, s1, logout, s3, _class='auth_navbar') 1343 1344 if asdropdown: 1345 logout = LI(Anr(I(_class='icon-off'), ' ' + T('Logout'), _href='%s/logout?_next=%s' % 1346 (action, urllib.quote(self.settings.logout_next)))) # the space before T('Logout') is intentional. It creates a gap between icon and text 1347 profile = LI(Anr(I(_class='icon-user'), ' ' + 1348 T('Profile'), _href=href('profile'))) 1349 password = LI(Anr(I(_class='icon-lock'), ' ' + 1350 T('Password'), _href=href('change_password'))) 1351 bar = UL(logout, _class='dropdown-menu') 1352 # logout will be the last item in list 1353 1354 if not 'profile' in self.settings.actions_disabled: 1355 if not asdropdown: 1356 bar.insert(-1, s2) 1357 bar.insert(-1, profile) 1358 if not 'change_password' in self.settings.actions_disabled: 1359 if not asdropdown: 1360 bar.insert(-1, s2) 1361 bar.insert(-1, password) 1362 else: 1363 login = Anr(T('Login'), _href=href('login')) 1364 register = Anr(T('Register'), _href=href('register')) 1365 retrieve_username = Anr( 1366 T('Forgot username?'), _href=href('retrieve_username')) 1367 lost_password = Anr( 1368 T('Lost password?'), _href=href('request_reset_password')) 1369 bar = SPAN(s1, login, s3, _class='auth_navbar') 1370 1371 if asdropdown: 1372 login = LI(Anr(I(_class='icon-off'), ' ' + T('Login'), _href=href('login'))) # the space before T('Login') is intentional. It creates a gap between icon and text 1373 register = LI(Anr(I(_class='icon-user'), 1374 ' ' + T('Register'), _href=href('register'))) 1375 retrieve_username = LI(Anr(I(_class='icon-edit'), ' ' + T( 1376 'Forgot username?'), _href=href('retrieve_username'))) 1377 lost_password = LI(Anr(I(_class='icon-lock'), ' ' + T( 1378 'Lost password?'), _href=href('request_reset_password'))) 1379 bar = UL(login, _class='dropdown-menu') 1380 # login will be the last item in list 1381 1382 if not 'register' in self.settings.actions_disabled: 1383 if not asdropdown: 1384 bar.insert(-1, s2) 1385 bar.insert(-1, register) 1386 if self.settings.use_username and not 'retrieve_username' \ 1387 in self.settings.actions_disabled: 1388 if not asdropdown: 1389 bar.insert(-1, s2) 1390 bar.insert(-1, retrieve_username) 1391 if not 'request_reset_password' \ 1392 in self.settings.actions_disabled: 1393 if not asdropdown: 1394 bar.insert(-1, s2) 1395 bar.insert(-1, lost_password) 1396 1397 if asdropdown: 1398 bar.insert(-1, LI('', _class='divider')) 1399 if self.user_id: 1400 bar = LI(Anr(prefix, user_identifier, _href='#'), 1401 bar, _class='dropdown') 1402 else: 1403 bar = LI(Anr(T('Login'), _href='#'), 1404 bar, _class='dropdown') 1405 return bar
1406
1407 - def __get_migrate(self, tablename, migrate=True):
1408 1409 if type(migrate).__name__ == 'str': 1410 return (migrate + tablename + '.table') 1411 elif migrate == False: 1412 return False 1413 else: 1414 return True
1415
1416 - def enable_record_versioning(self, 1417 tables, 1418 archive_db=None, 1419 archive_names='%(tablename)s_archive', 1420 current_record='current_record'):
1421 """ 1422 to enable full record versioning (including auth tables): 1423 1424 auth = Auth(db) 1425 auth.define_tables(signature=True) 1426 # define our own tables 1427 db.define_table('mything',Field('name'),auth.signature) 1428 auth.enable_record_versioning(tables=db) 1429 1430 tables can be the db (all table) or a list of tables. 1431 only tables with modified_by and modified_on fiels (as created 1432 by auth.signature) will have versioning. Old record versions will be 1433 in table 'mything_archive' automatically defined. 1434 1435 when you enable enable_record_versioning, records are never 1436 deleted but marked with is_active=False. 1437 1438 enable_record_versioning enables a common_filter for 1439 every table that filters out records with is_active = False 1440 1441 Important: If you use auth.enable_record_versioning, 1442 do not use auth.archive or you will end up with duplicates. 1443 auth.archive does explicitly what enable_record_versioning 1444 does automatically. 1445 1446 """ 1447 tables = [table for table in tables] 1448 for table in tables: 1449 if 'modified_on' in table.fields() and not current_record in table.fields(): 1450 table._enable_record_versioning( 1451 archive_db=archive_db, 1452 archive_name=archive_names, 1453 current_record=current_record)
1454
1455 - def define_signature(self):
1456 db = self.db 1457 settings = self.settings 1458 request = current.request 1459 T = current.T 1460 reference_user = 'reference %s' % settings.table_user_name 1461 1462 def lazy_user(auth=self): 1463 return auth.user_id
1464 1465 def represent(id, record=None, s=settings): 1466 try: 1467 user = s.table_user(id) 1468 return '%s %s' % (user.get("first_name", user.get("email")), 1469 user.get("last_name", '')) 1470 except: 1471 return id 1472 ondelete = self.settings.ondelete 1473 self.signature = db.Table( 1474 self.db, 'auth_signature', 1475 Field('is_active', 'boolean', 1476 default=True, 1477 readable=False, writable=False, 1478 label=T('Is Active')), 1479 Field('created_on', 'datetime', 1480 default=request.now, 1481 writable=False, readable=False, 1482 label=T('Created On')), 1483 Field('created_by', 1484 reference_user, 1485 default=lazy_user, represent=represent, 1486 writable=False, readable=False, 1487 label=T('Created By'), ondelete=ondelete), 1488 Field('modified_on', 'datetime', 1489 update=request.now, default=request.now, 1490 writable=False, readable=False, 1491 label=T('Modified On')), 1492 Field('modified_by', 1493 reference_user, represent=represent, 1494 default=lazy_user, update=lazy_user, 1495 writable=False, readable=False, 1496 label=T('Modified By'), ondelete=ondelete)) 1497
1498 - def define_tables(self, username=None, signature=None, 1499 migrate=True, fake_migrate=False):
1500 """ 1501 to be called unless tables are defined manually 1502 1503 usages: 1504 1505 # defines all needed tables and table files 1506 # 'myprefix_auth_user.table', ... 1507 auth.define_tables(migrate='myprefix_') 1508 1509 # defines all needed tables without migration/table files 1510 auth.define_tables(migrate=False) 1511 1512 """ 1513 1514 db = self.db 1515 settings = self.settings 1516 if username is None: 1517 username = settings.use_username 1518 else: 1519 settings.use_username = username 1520 if not self.signature: 1521 self.define_signature() 1522 if signature == True: 1523 signature_list = [self.signature] 1524 elif not signature: 1525 signature_list = [] 1526 elif isinstance(signature, self.db.Table): 1527 signature_list = [signature] 1528 else: 1529 signature_list = signature 1530 is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1531 is_crypted = CRYPT(key=settings.hmac_key, 1532 min_length=settings.password_min_length) 1533 is_unique_email = [ 1534 IS_EMAIL(error_message=self.messages.invalid_email), 1535 IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name)] 1536 if not settings.email_case_sensitive: 1537 is_unique_email.insert(1, IS_LOWER()) 1538 if not settings.table_user_name in db.tables: 1539 passfield = settings.password_field 1540 extra_fields = settings.extra_fields.get( 1541 settings.table_user_name, []) + signature_list 1542 if username or settings.cas_provider: 1543 is_unique_username = \ 1544 [IS_MATCH('[\w\.\-]+'), 1545 IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name)] 1546 if not settings.username_case_sensitive: 1547 is_unique_username.insert(1, IS_LOWER()) 1548 db.define_table( 1549 settings.table_user_name, 1550 Field('first_name', length=128, default='', 1551 label=self.messages.label_first_name, 1552 requires=is_not_empty), 1553 Field('last_name', length=128, default='', 1554 label=self.messages.label_last_name, 1555 requires=is_not_empty), 1556 Field('email', length=512, default='', 1557 label=self.messages.label_email, 1558 requires=is_unique_email), 1559 Field('username', length=128, default='', 1560 label=self.messages.label_username, 1561 requires=is_unique_username), 1562 Field(passfield, 'password', length=512, 1563 readable=False, label=self.messages.label_password, 1564 requires=[is_crypted]), 1565 Field('registration_key', length=512, 1566 writable=False, readable=False, default='', 1567 label=self.messages.label_registration_key), 1568 Field('reset_password_key', length=512, 1569 writable=False, readable=False, default='', 1570 label=self.messages.label_reset_password_key), 1571 Field('registration_id', length=512, 1572 writable=False, readable=False, default='', 1573 label=self.messages.label_registration_id), 1574 *extra_fields, 1575 **dict( 1576 migrate=self.__get_migrate(settings.table_user_name, 1577 migrate), 1578 fake_migrate=fake_migrate, 1579 format='%(username)s')) 1580 else: 1581 db.define_table( 1582 settings.table_user_name, 1583 Field('first_name', length=128, default='', 1584 label=self.messages.label_first_name, 1585 requires=is_not_empty), 1586 Field('last_name', length=128, default='', 1587 label=self.messages.label_last_name, 1588 requires=is_not_empty), 1589 Field('email', length=512, default='', 1590 label=self.messages.label_email, 1591 requires=is_unique_email), 1592 Field(passfield, 'password', length=512, 1593 readable=False, label=self.messages.label_password, 1594 requires=[is_crypted]), 1595 Field('registration_key', length=512, 1596 writable=False, readable=False, default='', 1597 label=self.messages.label_registration_key), 1598 Field('reset_password_key', length=512, 1599 writable=False, readable=False, default='', 1600 label=self.messages.label_reset_password_key), 1601 Field('registration_id', length=512, 1602 writable=False, readable=False, default='', 1603 label=self.messages.label_registration_id), 1604 *extra_fields, 1605 **dict( 1606 migrate=self.__get_migrate(settings.table_user_name, 1607 migrate), 1608 fake_migrate=fake_migrate, 1609 format='%(first_name)s %(last_name)s (%(id)s)')) 1610 reference_table_user = 'reference %s' % settings.table_user_name 1611 if not settings.table_group_name in db.tables: 1612 extra_fields = settings.extra_fields.get( 1613 settings.table_group_name, []) + signature_list 1614 db.define_table( 1615 settings.table_group_name, 1616 Field('role', length=512, default='', 1617 label=self.messages.label_role, 1618 requires=IS_NOT_IN_DB( 1619 db, '%s.role' % settings.table_group_name)), 1620 Field('description', 'text', 1621 label=self.messages.label_description), 1622 *extra_fields, 1623 **dict( 1624 migrate=self.__get_migrate( 1625 settings.table_group_name, migrate), 1626 fake_migrate=fake_migrate, 1627 format='%(role)s (%(id)s)')) 1628 reference_table_group = 'reference %s' % settings.table_group_name 1629 if not settings.table_membership_name in db.tables: 1630 extra_fields = settings.extra_fields.get( 1631 settings.table_membership_name, []) + signature_list 1632 db.define_table( 1633 settings.table_membership_name, 1634 Field('user_id', reference_table_user, 1635 label=self.messages.label_user_id), 1636 Field('group_id', reference_table_group, 1637 label=self.messages.label_group_id), 1638 *extra_fields, 1639 **dict( 1640 migrate=self.__get_migrate( 1641 settings.table_membership_name, migrate), 1642 fake_migrate=fake_migrate)) 1643 if not settings.table_permission_name in db.tables: 1644 extra_fields = settings.extra_fields.get( 1645 settings.table_permission_name, []) + signature_list 1646 db.define_table( 1647 settings.table_permission_name, 1648 Field('group_id', reference_table_group, 1649 label=self.messages.label_group_id), 1650 Field('name', default='default', length=512, 1651 label=self.messages.label_name, 1652 requires=is_not_empty), 1653 Field('table_name', length=512, 1654 label=self.messages.label_table_name), 1655 Field('record_id', 'integer', default=0, 1656 label=self.messages.label_record_id, 1657 requires=IS_INT_IN_RANGE(0, 10 ** 9)), 1658 *extra_fields, 1659 **dict( 1660 migrate=self.__get_migrate( 1661 settings.table_permission_name, migrate), 1662 fake_migrate=fake_migrate)) 1663 if not settings.table_event_name in db.tables: 1664 db.define_table( 1665 settings.table_event_name, 1666 Field('time_stamp', 'datetime', 1667 default=current.request.now, 1668 label=self.messages.label_time_stamp), 1669 Field('client_ip', 1670 default=current.request.client, 1671 label=self.messages.label_client_ip), 1672 Field('user_id', reference_table_user, default=None, 1673 label=self.messages.label_user_id), 1674 Field('origin', default='auth', length=512, 1675 label=self.messages.label_origin, 1676 requires=is_not_empty), 1677 Field('description', 'text', default='', 1678 label=self.messages.label_description, 1679 requires=is_not_empty), 1680 *settings.extra_fields.get(settings.table_event_name, []), 1681 **dict( 1682 migrate=self.__get_migrate( 1683 settings.table_event_name, migrate), 1684 fake_migrate=fake_migrate)) 1685 now = current.request.now 1686 if settings.cas_domains: 1687 if not settings.table_cas_name in db.tables: 1688 db.define_table( 1689 settings.table_cas_name, 1690 Field('user_id', reference_table_user, default=None, 1691 label=self.messages.label_user_id), 1692 Field('created_on', 'datetime', default=now), 1693 Field('service', requires=IS_URL()), 1694 Field('ticket'), 1695 Field('renew', 'boolean', default=False), 1696 *settings.extra_fields.get(settings.table_cas_name, []), 1697 **dict( 1698 migrate=self.__get_migrate( 1699 settings.table_cas_name, migrate), 1700 fake_migrate=fake_migrate)) 1701 if not db._lazy_tables: 1702 settings.table_user = db[settings.table_user_name] 1703 settings.table_group = db[settings.table_group_name] 1704 settings.table_membership = db[settings.table_membership_name] 1705 settings.table_permission = db[settings.table_permission_name] 1706 settings.table_event = db[settings.table_event_name] 1707 if settings.cas_domains: 1708 settings.table_cas = db[settings.table_cas_name] 1709 1710 if settings.cas_provider: # THIS IS NOT LAZY 1711 settings.actions_disabled = \ 1712 ['profile', 'register', 'change_password', 1713 'request_reset_password', 'retrieve_username'] 1714 from gluon.contrib.login_methods.cas_auth import CasAuth 1715 maps = settings.cas_maps 1716 if not maps: 1717 table_user = self.table_user() 1718 maps = dict((name, lambda v, n=name: v.get(n, None)) for name in 1719 table_user.fields if name != 'id' 1720 and table_user[name].readable) 1721 maps['registration_id'] = \ 1722 lambda v, p=settings.cas_provider: '%s/%s' % (p, v['user']) 1723 actions = [settings.cas_actions['login'], 1724 settings.cas_actions['servicevalidate'], 1725 settings.cas_actions['logout']] 1726 settings.login_form = CasAuth( 1727 casversion=2, 1728 urlbase=settings.cas_provider, 1729 actions=actions, 1730 maps=maps) 1731 return self
1732
1733 - def log_event(self, description, vars=None, origin='auth'):
1734 """ 1735 usage: 1736 1737 auth.log_event(description='this happened', origin='auth') 1738 """ 1739 if not description: 1740 return 1741 elif self.is_logged_in(): 1742 user_id = self.user.id 1743 else: 1744 user_id = None # user unknown 1745 vars = vars or {} 1746 self.table_event().insert( 1747 description=str(description % vars), 1748 origin=origin, user_id=user_id)
1749
1750 - def get_or_create_user(self, keys, update_fields=['email']):
1751 """ 1752 Used for alternate login methods: 1753 If the user exists already then password is updated. 1754 If the user doesn't yet exist, then they are created. 1755 """ 1756 table_user = self.table_user() 1757 user = None 1758 checks = [] 1759 # make a guess about who this user is 1760 for fieldname in ['registration_id', 'username', 'email']: 1761 if fieldname in table_user.fields() and \ 1762 keys.get(fieldname, None): 1763 checks.append(fieldname) 1764 value = keys[fieldname] 1765 user = table_user(**{fieldname: value}) 1766 if user: 1767 break 1768 if not checks: 1769 return None 1770 if not 'registration_id' in keys: 1771 keys['registration_id'] = keys[checks[0]] 1772 # if we think we found the user but registration_id does not match, 1773 # make new user 1774 if 'registration_id' in checks \ 1775 and user \ 1776 and user.registration_id \ 1777 and ('registration_id' not in keys or user.registration_id != str(keys['registration_id'])): 1778 user = None # THINK MORE ABOUT THIS? DO WE TRUST OPENID PROVIDER? 1779 if user: 1780 update_keys = dict(registration_id=keys['registration_id']) 1781 for key in update_fields: 1782 if key in keys: 1783 update_keys[key] = keys[key] 1784 user.update_record(**update_keys) 1785 elif checks: 1786 if not 'first_name' in keys and 'first_name' in table_user.fields: 1787 guess = keys.get('email', 'anonymous').split('@')[0] 1788 keys['first_name'] = keys.get('username', guess) 1789 user_id = table_user.insert(**table_user._filter_fields(keys)) 1790 user = self.user = table_user[user_id] 1791 if self.settings.create_user_groups: 1792 group_id = self.add_group( 1793 self.settings.create_user_groups % user) 1794 self.add_membership(group_id, user_id) 1795 if self.settings.everybody_group_id: 1796 self.add_membership(self.settings.everybody_group_id, user_id) 1797 return user
1798
1799 - def basic(self, basic_auth_realm=False):
1800 """ 1801 perform basic login. 1802 1803 :param basic_auth_realm: optional basic http authentication realm. 1804 :type basic_auth_realm: str or unicode or function or callable or boolean. 1805 1806 reads current.request.env.http_authorization 1807 and returns basic_allowed,basic_accepted,user. 1808 1809 if basic_auth_realm is defined is a callable it's return value 1810 is used to set the basic authentication realm, if it's a string 1811 its content is used instead. Otherwise basic authentication realm 1812 is set to the application name. 1813 If basic_auth_realm is None or False (the default) the behavior 1814 is to skip sending any challenge. 1815 1816 """ 1817 if not self.settings.allow_basic_login: 1818 return (False, False, False) 1819 basic = current.request.env.http_authorization 1820 if basic_auth_realm: 1821 if callable(basic_auth_realm): 1822 basic_auth_realm = basic_auth_auth() 1823 elif isinstance(basic_auth_realm, (unicode, str)): 1824 basic_realm = unicode(basic_auth_realm) 1825 elif basic_auth_realm is True: 1826 basic_realm = u'' + current.request.application 1827 http_401 = HTTP(401, u'Not Authorized', 1828 **{'WWW-Authenticate': u'Basic realm="' + basic_realm + '"'}) 1829 if not basic or not basic[:6].lower() == 'basic ': 1830 if basic_auth_realm: 1831 raise http_401 1832 return (True, False, False) 1833 (username, sep, password) = base64.b64decode(basic[6:]).partition(':') 1834 is_valid_user = sep and self.login_bare(username, password) 1835 if not is_valid_user and basic_auth_realm: 1836 raise http_401 1837 return (True, True, is_valid_user)
1838
1839 - def login_user(self, user):
1840 """ 1841 login the user = db.auth_user(id) 1842 """ 1843 from gluon.settings import global_settings 1844 if global_settings.web2py_runtime_gae: 1845 user = Row(self.db.auth_user._filter_fields(user, id=True)) 1846 delattr(user,'password') 1847 else: 1848 user = Row(user) 1849 for key,value in user.items(): 1850 if callable(value) or key=='password': 1851 delattr(user,key) 1852 current.session.auth = Storage( 1853 user = user, 1854 last_visit=current.request.now, 1855 expiration=self.settings.expiration, 1856 hmac_key=web2py_uuid()) 1857 self.user = user 1858 self.update_groups()
1859
1860 - def login_bare(self, username, password):
1861 """ 1862 logins user as specified by usernname (or email) and password 1863 """ 1864 table_user = self.table_user() 1865 if self.settings.login_userfield: 1866 userfield = self.settings.login_userfield 1867 elif 'username' in table_user.fields: 1868 userfield = 'username' 1869 else: 1870 userfield = 'email' 1871 passfield = self.settings.password_field 1872 user = self.db(table_user[userfield] == username).select().first() 1873 if user and user.get(passfield, False): 1874 password = table_user[passfield].validate(password)[0] 1875 if not user.registration_key and password == user[passfield]: 1876 self.login_user(user) 1877 return user 1878 else: 1879 # user not in database try other login methods 1880 for login_method in self.settings.login_methods: 1881 if login_method != self and login_method(username, password): 1882 self.user = username 1883 return username 1884 return False
1885
1886 - def cas_login( 1887 self, 1888 next=DEFAULT, 1889 onvalidation=DEFAULT, 1890 onaccept=DEFAULT, 1891 log=DEFAULT, 1892 version=2, 1893 ):
1894 request = current.request 1895 response = current.response 1896 session = current.session 1897 db, table = self.db, self.table_cas() 1898 session._cas_service = request.vars.service or session._cas_service 1899 if not request.env.http_host in self.settings.cas_domains or \ 1900 not session._cas_service: 1901 raise HTTP(403, 'not authorized') 1902 1903 def allow_access(interactivelogin=False): 1904 row = table(service=session._cas_service, user_id=self.user.id) 1905 if row: 1906 ticket = row.ticket 1907 else: 1908 ticket = 'ST-' + web2py_uuid() 1909 table.insert(service=session._cas_service, 1910 user_id=self.user.id, 1911 ticket=ticket, 1912 created_on=request.now, 1913 renew=interactivelogin) 1914 service = session._cas_service 1915 query_sep = '&' if '?' in service else '?' 1916 del session._cas_service 1917 if 'warn' in request.vars and not interactivelogin: 1918 response.headers[ 1919 'refresh'] = "5;URL=%s" % service + query_sep + "ticket=" + ticket 1920 return A("Continue to %s" % service, 1921 _href=service + query_sep + "ticket=" + ticket) 1922 else: 1923 redirect(service + query_sep + "ticket=" + ticket)
1924 if self.is_logged_in() and not 'renew' in request.vars: 1925 return allow_access() 1926 elif not self.is_logged_in() and 'gateway' in request.vars: 1927 redirect(service) 1928 1929 def cas_onaccept(form, onaccept=onaccept): 1930 if not onaccept is DEFAULT: 1931 onaccept(form) 1932 return allow_access(interactivelogin=True) 1933 return self.login(next, onvalidation, cas_onaccept, log) 1934
1935 - def cas_validate(self, version=2, proxy=False):
1936 request = current.request 1937 db, table = self.db, self.table_cas() 1938 current.response.headers['Content-Type'] = 'text' 1939 ticket = request.vars.ticket 1940 renew = 'renew' in request.vars 1941 row = table(ticket=ticket) 1942 success = False 1943 if row: 1944 if self.settings.login_userfield: 1945 userfield = self.settings.login_userfield 1946 elif 'username' in table.fields: 1947 userfield = 'username' 1948 else: 1949 userfield = 'email' 1950 # If ticket is a service Ticket and RENEW flag respected 1951 if ticket[0:3] == 'ST-' and \ 1952 not ((row.renew and renew) ^ renew): 1953 user = self.table_user()(row.user_id) 1954 row.delete_record() 1955 success = True 1956 1957 def build_response(body): 1958 return '<?xml version="1.0" encoding="UTF-8"?>\n' +\ 1959 TAG['cas:serviceResponse']( 1960 body, **{'_xmlns:cas': 'http://www.yale.edu/tp/cas'}).xml()
1961 if success: 1962 if version == 1: 1963 message = 'yes\n%s' % user[userfield] 1964 else: # assume version 2 1965 username = user.get('username', user[userfield]) 1966 message = build_response( 1967 TAG['cas:authenticationSuccess']( 1968 TAG['cas:user'](username), 1969 *[TAG['cas:' + field.name](user[field.name]) 1970 for field in self.table_user() 1971 if field.readable])) 1972 else: 1973 if version == 1: 1974 message = 'no\n' 1975 elif row: 1976 message = build_response(TAG['cas:authenticationFailure']()) 1977 else: 1978 message = build_response( 1979 TAG['cas:authenticationFailure']( 1980 'Ticket %s not recognized' % ticket, 1981 _code='INVALID TICKET')) 1982 raise HTTP(200, message) 1983
1984 - def login( 1985 self, 1986 next=DEFAULT, 1987 onvalidation=DEFAULT, 1988 onaccept=DEFAULT, 1989 log=DEFAULT, 1990 ):
1991 """ 1992 returns a login form 1993 1994 method: Auth.login([next=DEFAULT [, onvalidation=DEFAULT 1995 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1996 1997 """ 1998 1999 table_user = self.table_user() 2000 if self.settings.login_userfield: 2001 username = self.settings.login_userfield 2002 elif 'username' in table_user.fields: 2003 username = 'username' 2004 else: 2005 username = 'email' 2006 if 'username' in table_user.fields or \ 2007 not self.settings.login_email_validate: 2008 tmpvalidator = IS_NOT_EMPTY(error_message=self.messages.is_empty) 2009 else: 2010 tmpvalidator = IS_EMAIL(error_message=self.messages.invalid_email) 2011 old_requires = table_user[username].requires 2012 table_user[username].requires = tmpvalidator 2013 2014 request = current.request 2015 response = current.response 2016 session = current.session 2017 2018 passfield = self.settings.password_field 2019 try: 2020 table_user[passfield].requires[-1].min_length = 0 2021 except: 2022 pass 2023 2024 ### use session for federated login 2025 if self.next: 2026 session._auth_next = self.next 2027 elif session._auth_next: 2028 self.next = session._auth_next 2029 ### pass 2030 2031 if next is DEFAULT: 2032 next = self.next or self.settings.login_next 2033 if onvalidation is DEFAULT: 2034 onvalidation = self.settings.login_onvalidation 2035 if onaccept is DEFAULT: 2036 onaccept = self.settings.login_onaccept 2037 if log is DEFAULT: 2038 log = self.messages.login_log 2039 2040 onfail = self.settings.login_onfail 2041 2042 user = None # default 2043 2044 # do we use our own login form, or from a central source? 2045 if self.settings.login_form == self: 2046 form = SQLFORM( 2047 table_user, 2048 fields=[username, passfield], 2049 hidden=dict(_next=next), 2050 showid=self.settings.showid, 2051 submit_button=self.messages.login_button, 2052 delete_label=self.messages.delete_label, 2053 formstyle=self.settings.formstyle, 2054 separator=self.settings.label_separator 2055 ) 2056 2057 if self.settings.remember_me_form: 2058 ## adds a new input checkbox "remember me for longer" 2059 if self.settings.formstyle != 'bootstrap': 2060 addrow(form, XML("&nbsp;"), 2061 DIV(XML("&nbsp;"), 2062 INPUT(_type='checkbox', 2063 _class='checkbox', 2064 _id="auth_user_remember", 2065 _name="remember", 2066 ), 2067 XML("&nbsp;&nbsp;"), 2068 LABEL( 2069 self.messages.label_remember_me, 2070 _for="auth_user_remember", 2071 )), "", 2072 self.settings.formstyle, 2073 'auth_user_remember__row') 2074 elif self.settings.formstyle == 'bootstrap': 2075 addrow(form, 2076 "", 2077 LABEL( 2078 INPUT(_type='checkbox', 2079 _id="auth_user_remember", 2080 _name="remember"), 2081 self.messages.label_remember_me, 2082 _class="checkbox"), 2083 "", 2084 self.settings.formstyle, 2085 'auth_user_remember__row') 2086 2087 captcha = self.settings.login_captcha or \ 2088 (self.settings.login_captcha != False and self.settings.captcha) 2089 if captcha: 2090 addrow(form, captcha.label, captcha, captcha.comment, 2091 self.settings.formstyle, 'captcha__row') 2092 accepted_form = False 2093 2094 if form.accepts(request, session, 2095 formname='login', dbio=False, 2096 onvalidation=onvalidation, 2097 hideerror=self.settings.hideerror): 2098 2099 accepted_form = True 2100 # check for username in db 2101 user = self.db(table_user[username] 2102 == form.vars[username]).select().first() 2103 if user: 2104 # user in db, check if registration pending or disabled 2105 temp_user = user 2106 if temp_user.registration_key == 'pending': 2107 response.flash = self.messages.registration_pending 2108 return form 2109 elif temp_user.registration_key in ('disabled', 'blocked'): 2110 response.flash = self.messages.login_disabled 2111 return form 2112 elif not temp_user.registration_key is None and \ 2113 temp_user.registration_key.strip(): 2114 response.flash = \ 2115 self.messages.registration_verifying 2116 return form 2117 # try alternate logins 1st as these have the 2118 # current version of the password 2119 user = None 2120 for login_method in self.settings.login_methods: 2121 if login_method != self and \ 2122 login_method(request.vars[username], 2123 request.vars[passfield]): 2124 if not self in self.settings.login_methods: 2125 # do not store password in db 2126 form.vars[passfield] = None 2127 user = self.get_or_create_user( 2128 form.vars, self.settings.update_fields) 2129 break 2130 if not user: 2131 # alternates have failed, maybe because service inaccessible 2132 if self.settings.login_methods[0] == self: 2133 # try logging in locally using cached credentials 2134 if form.vars.get(passfield, '') == temp_user[passfield]: 2135 # success 2136 user = temp_user 2137 else: 2138 # user not in db 2139 if not self.settings.alternate_requires_registration: 2140 # we're allowed to auto-register users from external systems 2141 for login_method in self.settings.login_methods: 2142 if login_method != self and \ 2143 login_method(request.vars[username], 2144 request.vars[passfield]): 2145 if not self in self.settings.login_methods: 2146 # do not store password in db 2147 form.vars[passfield] = None 2148 user = self.get_or_create_user( 2149 form.vars, self.settings.update_fields) 2150 break 2151 if not user: 2152 self.log_event(self.messages.login_failed_log, 2153 request.post_vars) 2154 # invalid login 2155 session.flash = self.messages.invalid_login 2156 callback(onfail, None) 2157 redirect( 2158 self.url(args=request.args, vars=request.get_vars), 2159 client_side=self.settings.client_side) 2160 2161 else: 2162 # use a central authentication server 2163 cas = self.settings.login_form 2164 cas_user = cas.get_user() 2165 2166 if cas_user: 2167 cas_user[passfield] = None 2168 user = self.get_or_create_user( 2169 table_user._filter_fields(cas_user), 2170 self.settings.update_fields) 2171 elif hasattr(cas, 'login_form'): 2172 return cas.login_form() 2173 else: 2174 # we need to pass through login again before going on 2175 next = self.url(self.settings.function, args='login') 2176 redirect(cas.login_url(next), 2177 client_side=self.settings.client_side) 2178 2179 # process authenticated users 2180 if user: 2181 user = Row(table_user._filter_fields(user, id=True)) 2182 # process authenticated users 2183 # user wants to be logged in for longer 2184 self.login_user(user) 2185 session.auth.expiration = \ 2186 request.vars.get('remember', False) and \ 2187 self.settings.long_expiration or \ 2188 self.settings.expiration 2189 session.auth.remember = 'remember' in request.vars 2190 self.log_event(log, user) 2191 session.flash = self.messages.logged_in 2192 2193 # how to continue 2194 if self.settings.login_form == self: 2195 if accepted_form: 2196 callback(onaccept, form) 2197 if next == session._auth_next: 2198 session._auth_next = None 2199 next = replace_id(next, form) 2200 redirect(next, client_side=self.settings.client_side) 2201 2202 table_user[username].requires = old_requires 2203 return form 2204 elif user: 2205 callback(onaccept, None) 2206 2207 if next == session._auth_next: 2208 del session._auth_next 2209 redirect(next, client_side=self.settings.client_side)
2210
2211 - def logout(self, next=DEFAULT, onlogout=DEFAULT, log=DEFAULT):
2212 """ 2213 logout and redirects to login 2214 2215 method: Auth.logout ([next=DEFAULT[, onlogout=DEFAULT[, 2216 log=DEFAULT]]]) 2217 2218 """ 2219 2220 if next is DEFAULT: 2221 next = self.settings.logout_next 2222 if onlogout is DEFAULT: 2223 onlogout = self.settings.logout_onlogout 2224 if onlogout: 2225 onlogout(self.user) 2226 if log is DEFAULT: 2227 log = self.messages.logout_log 2228 if self.user: 2229 self.log_event(log, self.user) 2230 if self.settings.login_form != self: 2231 cas = self.settings.login_form 2232 cas_user = cas.get_user() 2233 if cas_user: 2234 next = cas.logout_url(next) 2235 2236 current.session.auth = None 2237 current.session.flash = self.messages.logged_out 2238 if not next is None: 2239 redirect(next)
2240
2241 - def register( 2242 self, 2243 next=DEFAULT, 2244 onvalidation=DEFAULT, 2245 onaccept=DEFAULT, 2246 log=DEFAULT, 2247 ):
2248 """ 2249 returns a registration form 2250 2251 method: Auth.register([next=DEFAULT [, onvalidation=DEFAULT 2252 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2253 2254 """ 2255 2256 table_user = self.table_user() 2257 request = current.request 2258 response = current.response 2259 session = current.session 2260 if self.is_logged_in(): 2261 redirect(self.settings.logged_url, 2262 client_side=self.settings.client_side) 2263 if next is DEFAULT: 2264 next = self.next or self.settings.register_next 2265 if onvalidation is DEFAULT: 2266 onvalidation = self.settings.register_onvalidation 2267 if onaccept is DEFAULT: 2268 onaccept = self.settings.register_onaccept 2269 if log is DEFAULT: 2270 log = self.messages.register_log 2271 2272 table_user = self.table_user() 2273 if self.settings.login_userfield: 2274 username = self.settings.login_userfield 2275 elif 'username' in table_user.fields: 2276 username = 'username' 2277 else: 2278 username = 'email' 2279 2280 # Ensure the username field is unique. 2281 unique_validator = IS_NOT_IN_DB(self.db, table_user[username]) 2282 if not table_user[username].requires: 2283 table_user[username].requires = unique_validator 2284 elif isinstance(table_user[username].requires, (list, tuple)): 2285 if not any([isinstance(validator, IS_NOT_IN_DB) for validator in 2286 table_user[username].requires]): 2287 if isinstance(table_user[username].requires, list): 2288 table_user[username].requires.append(unique_validator) 2289 else: 2290 table_user[username].requires += (unique_validator, ) 2291 elif not isinstance(table_user[username].requires, IS_NOT_IN_DB): 2292 table_user[username].requires = [table_user[username].requires, 2293 unique_validator] 2294 2295 passfield = self.settings.password_field 2296 formstyle = self.settings.formstyle 2297 form = SQLFORM(table_user, 2298 fields=self.settings.register_fields, 2299 hidden=dict(_next=next), 2300 showid=self.settings.showid, 2301 submit_button=self.messages.register_button, 2302 delete_label=self.messages.delete_label, 2303 formstyle=formstyle, 2304 separator=self.settings.label_separator 2305 ) 2306 if self.settings.register_verify_password: 2307 for i, row in enumerate(form[0].components): 2308 item = row.element('input', _name=passfield) 2309 if item: 2310 form.custom.widget.password_two = \ 2311 INPUT(_name="password_two", _type="password", 2312 requires=IS_EXPR( 2313 'value==%s' % 2314 repr(request.vars.get(passfield, None)), 2315 error_message=self.messages.mismatched_password)) 2316 2317 if formstyle == 'bootstrap': 2318 form.custom.widget.password_two[ 2319 '_class'] = 'input-xlarge' 2320 2321 addrow( 2322 form, self.messages.verify_password + 2323 self.settings.label_separator, 2324 form.custom.widget.password_two, 2325 self.messages.verify_password_comment, 2326 formstyle, 2327 '%s_%s__row' % (table_user, 'password_two'), 2328 position=i + 1) 2329 break 2330 captcha = self.settings.register_captcha or self.settings.captcha 2331 if captcha: 2332 addrow(form, captcha.label, captcha, 2333 captcha.comment, self.settings.formstyle, 'captcha__row') 2334 2335 table_user.registration_key.default = key = web2py_uuid() 2336 if form.accepts(request, session, formname='register', 2337 onvalidation=onvalidation, hideerror=self.settings.hideerror): 2338 description = self.messages.group_description % form.vars 2339 if self.settings.create_user_groups: 2340 group_id = self.add_group( 2341 self.settings.create_user_groups % form.vars, description) 2342 self.add_membership(group_id, form.vars.id) 2343 if self.settings.everybody_group_id: 2344 self.add_membership( 2345 self.settings.everybody_group_id, form.vars.id) 2346 if self.settings.registration_requires_verification: 2347 link = self.url( 2348 self.settings.function, args=('verify_email', key), scheme=True) 2349 2350 if not self.settings.mailer or \ 2351 not self.settings.mailer.send( 2352 to=form.vars.email, 2353 subject=self.messages.verify_email_subject, 2354 message=self.messages.verify_email 2355 % dict(key=key, link=link)): 2356 self.db.rollback() 2357 response.flash = self.messages.unable_send_email 2358 return form 2359 session.flash = self.messages.email_sent 2360 if self.settings.registration_requires_approval and \ 2361 not self.settings.registration_requires_verification: 2362 table_user[form.vars.id] = dict(registration_key='pending') 2363 session.flash = self.messages.registration_pending 2364 elif (not self.settings.registration_requires_verification or 2365 self.settings.login_after_registration): 2366 if not self.settings.registration_requires_verification: 2367 table_user[form.vars.id] = dict(registration_key='') 2368 session.flash = self.messages.registration_successful 2369 user = self.db( 2370 table_user[username] == form.vars[username] 2371 ).select().first() 2372 self.login_user(user) 2373 session.flash = self.messages.logged_in 2374 self.log_event(log, form.vars) 2375 callback(onaccept, form) 2376 if not next: 2377 next = self.url(args=request.args) 2378 else: 2379 next = replace_id(next, form) 2380 redirect(next, client_side=self.settings.client_side) 2381 return form
2382
2383 - def is_logged_in(self):
2384 """ 2385 checks if the user is logged in and returns True/False. 2386 if so user is in auth.user as well as in session.auth.user 2387 """ 2388 2389 if self.user: 2390 return True 2391 return False
2392
2393 - def verify_email( 2394 self, 2395 next=DEFAULT, 2396 onaccept=DEFAULT, 2397 log=DEFAULT, 2398 ):
2399 """ 2400 action user to verify the registration email, XXXXXXXXXXXXXXXX 2401 2402 method: Auth.verify_email([next=DEFAULT [, onvalidation=DEFAULT 2403 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2404 2405 """ 2406 2407 key = getarg(-1) 2408 table_user = self.table_user() 2409 user = table_user(registration_key=key) 2410 if not user: 2411 redirect(self.settings.login_url) 2412 if self.settings.registration_requires_approval: 2413 user.update_record(registration_key='pending') 2414 current.session.flash = self.messages.registration_pending 2415 else: 2416 user.update_record(registration_key='') 2417 current.session.flash = self.messages.email_verified 2418 # make sure session has same user.registrato_key as db record 2419 if current.session.auth and current.session.auth.user: 2420 current.session.auth.user.registration_key = user.registration_key 2421 if log is DEFAULT: 2422 log = self.messages.verify_email_log 2423 if next is DEFAULT: 2424 next = self.settings.verify_email_next 2425 if onaccept is DEFAULT: 2426 onaccept = self.settings.verify_email_onaccept 2427 self.log_event(log, user) 2428 callback(onaccept, user) 2429 redirect(next)
2430
2431 - def retrieve_username( 2432 self, 2433 next=DEFAULT, 2434 onvalidation=DEFAULT, 2435 onaccept=DEFAULT, 2436 log=DEFAULT, 2437 ):
2438 """ 2439 returns a form to retrieve the user username 2440 (only if there is a username field) 2441 2442 method: Auth.retrieve_username([next=DEFAULT 2443 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2444 2445 """ 2446 2447 table_user = self.table_user() 2448 if not 'username' in table_user.fields: 2449 raise HTTP(404) 2450 request = current.request 2451 response = current.response 2452 session = current.session 2453 captcha = self.settings.retrieve_username_captcha or \ 2454 (self.settings.retrieve_username_captcha != False and self.settings.captcha) 2455 if not self.settings.mailer: 2456 response.flash = self.messages.function_disabled 2457 return '' 2458 if next is DEFAULT: 2459 next = self.next or self.settings.retrieve_username_next 2460 if onvalidation is DEFAULT: 2461 onvalidation = self.settings.retrieve_username_onvalidation 2462 if onaccept is DEFAULT: 2463 onaccept = self.settings.retrieve_username_onaccept 2464 if log is DEFAULT: 2465 log = self.messages.retrieve_username_log 2466 old_requires = table_user.email.requires 2467 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 2468 error_message=self.messages.invalid_email)] 2469 form = SQLFORM(table_user, 2470 fields=['email'], 2471 hidden=dict(_next=next), 2472 showid=self.settings.showid, 2473 submit_button=self.messages.submit_button, 2474 delete_label=self.messages.delete_label, 2475 formstyle=self.settings.formstyle, 2476 separator=self.settings.label_separator 2477 ) 2478 if captcha: 2479 addrow(form, captcha.label, captcha, 2480 captcha.comment, self.settings.formstyle, 'captcha__row') 2481 2482 if form.accepts(request, session, 2483 formname='retrieve_username', dbio=False, 2484 onvalidation=onvalidation, hideerror=self.settings.hideerror): 2485 user = table_user(email=form.vars.email) 2486 if not user: 2487 current.session.flash = \ 2488 self.messages.invalid_email 2489 redirect(self.url(args=request.args)) 2490 username = user.username 2491 self.settings.mailer.send(to=form.vars.email, 2492 subject=self.messages.retrieve_username_subject, 2493 message=self.messages.retrieve_username 2494 % dict(username=username)) 2495 session.flash = self.messages.email_sent 2496 self.log_event(log, user) 2497 callback(onaccept, form) 2498 if not next: 2499 next = self.url(args=request.args) 2500 else: 2501 next = replace_id(next, form) 2502 redirect(next) 2503 table_user.email.requires = old_requires 2504 return form
2505
2506 - def random_password(self):
2507 import string 2508 import random 2509 password = '' 2510 specials = r'!#$*' 2511 for i in range(0, 3): 2512 password += random.choice(string.lowercase) 2513 password += random.choice(string.uppercase) 2514 password += random.choice(string.digits) 2515 password += random.choice(specials) 2516 return ''.join(random.sample(password, len(password)))
2517
2518 - def reset_password_deprecated( 2519 self, 2520 next=DEFAULT, 2521 onvalidation=DEFAULT, 2522 onaccept=DEFAULT, 2523 log=DEFAULT, 2524 ):
2525 """ 2526 returns a form to reset the user password (deprecated) 2527 2528 method: Auth.reset_password_deprecated([next=DEFAULT 2529 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2530 2531 """ 2532 2533 table_user = self.table_user() 2534 request = current.request 2535 response = current.response 2536 session = current.session 2537 if not self.settings.mailer: 2538 response.flash = self.messages.function_disabled 2539 return '' 2540 if next is DEFAULT: 2541 next = self.next or self.settings.retrieve_password_next 2542 if onvalidation is DEFAULT: 2543 onvalidation = self.settings.retrieve_password_onvalidation 2544 if onaccept is DEFAULT: 2545 onaccept = self.settings.retrieve_password_onaccept 2546 if log is DEFAULT: 2547 log = self.messages.retrieve_password_log 2548 old_requires = table_user.email.requires 2549 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 2550 error_message=self.messages.invalid_email)] 2551 form = SQLFORM(table_user, 2552 fields=['email'], 2553 hidden=dict(_next=next), 2554 showid=self.settings.showid, 2555 submit_button=self.messages.submit_button, 2556 delete_label=self.messages.delete_label, 2557 formstyle=self.settings.formstyle, 2558 separator=self.settings.label_separator 2559 ) 2560 if form.accepts(request, session, 2561 formname='retrieve_password', dbio=False, 2562 onvalidation=onvalidation, hideerror=self.settings.hideerror): 2563 user = table_user(email=form.vars.email) 2564 if not user: 2565 current.session.flash = \ 2566 self.messages.invalid_email 2567 redirect(self.url(args=request.args)) 2568 elif user.registration_key in ('pending', 'disabled', 'blocked'): 2569 current.session.flash = \ 2570 self.messages.registration_pending 2571 redirect(self.url(args=request.args)) 2572 password = self.random_password() 2573 passfield = self.settings.password_field 2574 d = dict( 2575 passfield=str(table_user[passfield].validate(password)[0]), 2576 registration_key='') 2577 user.update_record(**d) 2578 if self.settings.mailer and \ 2579 self.settings.mailer.send(to=form.vars.email, 2580 subject=self.messages.retrieve_password_subject, 2581 message=self.messages.retrieve_password 2582 % dict(password=password)): 2583 session.flash = self.messages.email_sent 2584 else: 2585 session.flash = self.messages.unable_to_send_email 2586 self.log_event(log, user) 2587 callback(onaccept, form) 2588 if not next: 2589 next = self.url(args=request.args) 2590 else: 2591 next = replace_id(next, form) 2592 redirect(next) 2593 table_user.email.requires = old_requires 2594 return form
2595
2596 - def reset_password( 2597 self, 2598 next=DEFAULT, 2599 onvalidation=DEFAULT, 2600 onaccept=DEFAULT, 2601 log=DEFAULT, 2602 ):
2603 """ 2604 returns a form to reset the user password 2605 2606 method: Auth.reset_password([next=DEFAULT 2607 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2608 2609 """ 2610 2611 table_user = self.table_user() 2612 request = current.request 2613 # response = current.response 2614 session = current.session 2615 2616 if next is DEFAULT: 2617 next = self.next or self.settings.reset_password_next 2618 try: 2619 key = request.vars.key or getarg(-1) 2620 t0 = int(key.split('-')[0]) 2621 if time.time() - t0 > 60 * 60 * 24: 2622 raise Exception 2623 user = table_user(reset_password_key=key) 2624 if not user: 2625 raise Exception 2626 except Exception: 2627 session.flash = self.messages.invalid_reset_password 2628 redirect(next, client_side=self.settings.client_side) 2629 passfield = self.settings.password_field 2630 form = SQLFORM.factory( 2631 Field('new_password', 'password', 2632 label=self.messages.new_password, 2633 requires=self.table_user()[passfield].requires), 2634 Field('new_password2', 'password', 2635 label=self.messages.verify_password, 2636 requires=[IS_EXPR( 2637 'value==%s' % repr(request.vars.new_password), 2638 self.messages.mismatched_password)]), 2639 submit_button=self.messages.password_reset_button, 2640 hidden=dict(_next=next), 2641 formstyle=self.settings.formstyle, 2642 separator=self.settings.label_separator 2643 ) 2644 if form.accepts(request, session, 2645 hideerror=self.settings.hideerror): 2646 user.update_record( 2647 **{passfield: str(form.vars.new_password), 2648 'registration_key': '', 2649 'reset_password_key': ''}) 2650 session.flash = self.messages.password_changed 2651 if self.settings.login_after_password_change: 2652 self.login_user(user) 2653 redirect(next, client_side=self.settings.client_side) 2654 return form
2655
2656 - def request_reset_password( 2657 self, 2658 next=DEFAULT, 2659 onvalidation=DEFAULT, 2660 onaccept=DEFAULT, 2661 log=DEFAULT, 2662 ):
2663 """ 2664 returns a form to reset the user password 2665 2666 method: Auth.reset_password([next=DEFAULT 2667 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2668 2669 """ 2670 table_user = self.table_user() 2671 request = current.request 2672 response = current.response 2673 session = current.session 2674 captcha = self.settings.retrieve_password_captcha or \ 2675 (self.settings.retrieve_password_captcha != False and self.settings.captcha) 2676 2677 if next is DEFAULT: 2678 next = self.next or self.settings.request_reset_password_next 2679 if not self.settings.mailer: 2680 response.flash = self.messages.function_disabled 2681 return '' 2682 if onvalidation is DEFAULT: 2683 onvalidation = self.settings.reset_password_onvalidation 2684 if onaccept is DEFAULT: 2685 onaccept = self.settings.reset_password_onaccept 2686 if log is DEFAULT: 2687 log = self.messages.reset_password_log 2688 table_user.email.requires = [ 2689 IS_EMAIL(error_message=self.messages.invalid_email), 2690 IS_IN_DB(self.db, table_user.email, 2691 error_message=self.messages.invalid_email)] 2692 form = SQLFORM(table_user, 2693 fields=['email'], 2694 hidden=dict(_next=next), 2695 showid=self.settings.showid, 2696 submit_button=self.messages.password_reset_button, 2697 delete_label=self.messages.delete_label, 2698 formstyle=self.settings.formstyle, 2699 separator=self.settings.label_separator 2700 ) 2701 if captcha: 2702 addrow(form, captcha.label, captcha, 2703 captcha.comment, self.settings.formstyle, 'captcha__row') 2704 if form.accepts(request, session, 2705 formname='reset_password', dbio=False, 2706 onvalidation=onvalidation, 2707 hideerror=self.settings.hideerror): 2708 user = table_user(email=form.vars.email) 2709 if not user: 2710 session.flash = self.messages.invalid_email 2711 redirect(self.url(args=request.args), 2712 client_side=self.settings.client_side) 2713 elif user.registration_key in ('pending', 'disabled', 'blocked'): 2714 session.flash = self.messages.registration_pending 2715 redirect(self.url(args=request.args), 2716 client_side=self.settings.client_side) 2717 if self.email_reset_password(user): 2718 session.flash = self.messages.email_sent 2719 else: 2720 session.flash = self.messages.unable_to_send_email 2721 self.log_event(log, user) 2722 callback(onaccept, form) 2723 if not next: 2724 next = self.url(args=request.args) 2725 else: 2726 next = replace_id(next, form) 2727 redirect(next, client_side=self.settings.client_side) 2728 # old_requires = table_user.email.requires 2729 return form
2730
2731 - def email_reset_password(self, user):
2732 reset_password_key = str(int(time.time())) + '-' + web2py_uuid() 2733 link = self.url(self.settings.function, 2734 args=('reset_password', reset_password_key), 2735 scheme=True) 2736 if self.settings.mailer.send( 2737 to=user.email, 2738 subject=self.messages.reset_password_subject, 2739 message=self.messages.reset_password % 2740 dict(key=reset_password_key, link=link)): 2741 user.update_record(reset_password_key=reset_password_key) 2742 return True 2743 return False
2744
2745 - def retrieve_password( 2746 self, 2747 next=DEFAULT, 2748 onvalidation=DEFAULT, 2749 onaccept=DEFAULT, 2750 log=DEFAULT, 2751 ):
2752 if self.settings.reset_password_requires_verification: 2753 return self.request_reset_password(next, onvalidation, onaccept, log) 2754 else: 2755 return self.reset_password_deprecated(next, onvalidation, onaccept, log)
2756
2757 - def change_password( 2758 self, 2759 next=DEFAULT, 2760 onvalidation=DEFAULT, 2761 onaccept=DEFAULT, 2762 log=DEFAULT, 2763 ):
2764 """ 2765 returns a form that lets the user change password 2766 2767 method: Auth.change_password([next=DEFAULT[, onvalidation=DEFAULT[, 2768 onaccept=DEFAULT[, log=DEFAULT]]]]) 2769 """ 2770 2771 if not self.is_logged_in(): 2772 redirect(self.settings.login_url, 2773 client_side=self.settings.client_side) 2774 db = self.db 2775 table_user = self.table_user() 2776 s = db(table_user.id == self.user.id) 2777 2778 request = current.request 2779 session = current.session 2780 if next is DEFAULT: 2781 next = self.next or self.settings.change_password_next 2782 if onvalidation is DEFAULT: 2783 onvalidation = self.settings.change_password_onvalidation 2784 if onaccept is DEFAULT: 2785 onaccept = self.settings.change_password_onaccept 2786 if log is DEFAULT: 2787 log = self.messages.change_password_log 2788 passfield = self.settings.password_field 2789 form = SQLFORM.factory( 2790 Field('old_password', 'password', 2791 label=self.messages.old_password, 2792 requires=table_user[passfield].requires), 2793 Field('new_password', 'password', 2794 label=self.messages.new_password, 2795 requires=table_user[passfield].requires), 2796 Field('new_password2', 'password', 2797 label=self.messages.verify_password, 2798 requires=[IS_EXPR( 2799 'value==%s' % repr(request.vars.new_password), 2800 self.messages.mismatched_password)]), 2801 submit_button=self.messages.password_change_button, 2802 hidden=dict(_next=next), 2803 formstyle=self.settings.formstyle, 2804 separator=self.settings.label_separator 2805 ) 2806 if form.accepts(request, session, 2807 formname='change_password', 2808 onvalidation=onvalidation, 2809 hideerror=self.settings.hideerror): 2810 2811 if not form.vars['old_password'] == s.select().first()[passfield]: 2812 form.errors['old_password'] = self.messages.invalid_password 2813 else: 2814 d = {passfield: str(form.vars.new_password)} 2815 s.update(**d) 2816 session.flash = self.messages.password_changed 2817 self.log_event(log, self.user) 2818 callback(onaccept, form) 2819 if not next: 2820 next = self.url(args=request.args) 2821 else: 2822 next = replace_id(next, form) 2823 redirect(next, client_side=self.settings.client_side) 2824 return form
2825
2826 - def profile( 2827 self, 2828 next=DEFAULT, 2829 onvalidation=DEFAULT, 2830 onaccept=DEFAULT, 2831 log=DEFAULT, 2832 ):
2833 """ 2834 returns a form that lets the user change his/her profile 2835 2836 method: Auth.profile([next=DEFAULT [, onvalidation=DEFAULT 2837 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2838 2839 """ 2840 2841 table_user = self.table_user() 2842 if not self.is_logged_in(): 2843 redirect(self.settings.login_url, 2844 client_side=self.settings.client_side) 2845 passfield = self.settings.password_field 2846 table_user[passfield].writable = False 2847 request = current.request 2848 session = current.session 2849 if next is DEFAULT: 2850 next = self.next or self.settings.profile_next 2851 if onvalidation is DEFAULT: 2852 onvalidation = self.settings.profile_onvalidation 2853 if onaccept is DEFAULT: 2854 onaccept = self.settings.profile_onaccept 2855 if log is DEFAULT: 2856 log = self.messages.profile_log 2857 form = SQLFORM( 2858 table_user, 2859 self.user.id, 2860 fields=self.settings.profile_fields, 2861 hidden=dict(_next=next), 2862 showid=self.settings.showid, 2863 submit_button=self.messages.profile_save_button, 2864 delete_label=self.messages.delete_label, 2865 upload=self.settings.download_url, 2866 formstyle=self.settings.formstyle, 2867 separator=self.settings.label_separator, 2868 deletable=self.settings.allow_delete_accounts, 2869 ) 2870 if form.accepts(request, session, 2871 formname='profile', 2872 onvalidation=onvalidation, 2873 hideerror=self.settings.hideerror): 2874 self.user.update(table_user._filter_fields(form.vars)) 2875 session.flash = self.messages.profile_updated 2876 self.log_event(log, self.user) 2877 callback(onaccept, form) 2878 if form.deleted: 2879 return self.logout() 2880 if not next: 2881 next = self.url(args=request.args) 2882 else: 2883 next = replace_id(next, form) 2884 redirect(next, client_side=self.settings.client_side) 2885 return form
2886
2887 - def is_impersonating(self):
2888 return self.is_logged_in() and 'impersonator' in current.session.auth
2889
2890 - def impersonate(self, user_id=DEFAULT):
2891 """ 2892 usage: POST TO http://..../impersonate request.post_vars.user_id=<id> 2893 set request.post_vars.user_id to 0 to restore original user. 2894 2895 requires impersonator is logged in and 2896 has_permission('impersonate', 'auth_user', user_id) 2897 """ 2898 request = current.request 2899 session = current.session 2900 auth = session.auth 2901 table_user = self.table_user() 2902 if not self.is_logged_in(): 2903 raise HTTP(401, "Not Authorized") 2904 current_id = auth.user.id 2905 requested_id = user_id 2906 if user_id is DEFAULT: 2907 user_id = current.request.post_vars.user_id 2908 if user_id and user_id != self.user.id and user_id != '0': 2909 if not self.has_permission('impersonate', 2910 self.settings.table_user_name, 2911 user_id): 2912 raise HTTP(403, "Forbidden") 2913 user = table_user(user_id) 2914 if not user: 2915 raise HTTP(401, "Not Authorized") 2916 auth.impersonator = cPickle.dumps(session) 2917 auth.user.update( 2918 table_user._filter_fields(user, True)) 2919 self.user = auth.user 2920 onaccept = self.settings.login_onaccept 2921 if onaccept: 2922 form = Storage(dict(vars=self.user)) 2923 if not isinstance(onaccept,(list, tuple)): 2924 onaccept = [onaccept] 2925 for callback in onaccept: 2926 callback(form) 2927 log = self.messages.impersonate_log 2928 self.log_event(log, dict(id=current_id, other_id=auth.user.id)) 2929 elif user_id in (0, '0'): 2930 if self.is_impersonating(): 2931 session.clear() 2932 session.update(cPickle.loads(auth.impersonator)) 2933 self.user = session.auth.user 2934 return None 2935 if requested_id is DEFAULT and not request.post_vars: 2936 return SQLFORM.factory(Field('user_id', 'integer')) 2937 return SQLFORM(table_user, user.id, readonly=True)
2938
2939 - def update_groups(self):
2940 if not self.user: 2941 return 2942 user_groups = self.user_groups = {} 2943 if current.session.auth: 2944 current.session.auth.user_groups = self.user_groups 2945 table_group = self.table_group() 2946 table_membership = self.table_membership() 2947 memberships = self.db( 2948 table_membership.user_id == self.user.id).select() 2949 for membership in memberships: 2950 group = table_group(membership.group_id) 2951 if group: 2952 user_groups[membership.group_id] = group.role
2953
2954 - def groups(self):
2955 """ 2956 displays the groups and their roles for the logged in user 2957 """ 2958 2959 if not self.is_logged_in(): 2960 redirect(self.settings.login_url) 2961 table_membership = self.table_membership() 2962 memberships = self.db( 2963 table_membership.user_id == self.user.id).select() 2964 table = TABLE() 2965 for membership in memberships: 2966 table_group = self.db[self.settings.table_group_name] 2967 groups = self.db(table_group.id == membership.group_id).select() 2968 if groups: 2969 group = groups[0] 2970 table.append(TR(H3(group.role, '(%s)' % group.id))) 2971 table.append(TR(P(group.description))) 2972 if not memberships: 2973 return None 2974 return table
2975
2976 - def not_authorized(self):
2977 """ 2978 you can change the view for this page to make it look as you like 2979 """ 2980 if current.request.ajax: 2981 raise HTTP(403, 'ACCESS DENIED') 2982 return 'ACCESS DENIED'
2983
2984 - def requires(self, condition, requires_login=True, otherwise=None):
2985 """ 2986 decorator that prevents access to action if not logged in 2987 """ 2988 2989 def decorator(action): 2990 2991 def f(*a, **b): 2992 2993 basic_allowed, basic_accepted, user = self.basic() 2994 user = user or self.user 2995 if requires_login: 2996 if not user: 2997 if current.request.ajax: 2998 raise HTTP(401, self.messages.ajax_failed_authentication) 2999 elif not otherwise is None: 3000 if callable(otherwise): 3001 return otherwise() 3002 redirect(otherwise) 3003 elif self.settings.allow_basic_login_only or \ 3004 basic_accepted or current.request.is_restful: 3005 raise HTTP(403, "Not authorized") 3006 else: 3007 next = self.here() 3008 current.session.flash = current.response.flash 3009 return call_or_redirect( 3010 self.settings.on_failed_authentication, 3011 self.settings.login_url + 3012 '?_next=' + urllib.quote(next)) 3013 3014 if callable(condition): 3015 flag = condition() 3016 else: 3017 flag = condition 3018 if not flag: 3019 current.session.flash = self.messages.access_denied 3020 return call_or_redirect( 3021 self.settings.on_failed_authorization) 3022 return action(*a, **b)
3023 f.__doc__ = action.__doc__ 3024 f.__name__ = action.__name__ 3025 f.__dict__.update(action.__dict__) 3026 return f 3027 3028 return decorator 3029
3030 - def requires_login(self, otherwise=None):
3031 """ 3032 decorator that prevents access to action if not logged in 3033 """ 3034 return self.requires(True, otherwise=otherwise)
3035
3036 - def requires_membership(self, role=None, group_id=None, otherwise=None):
3037 """ 3038 decorator that prevents access to action if not logged in or 3039 if user logged in is not a member of group_id. 3040 If role is provided instead of group_id then the 3041 group_id is calculated. 3042 """ 3043 def has_membership(self=self, group_id=group_id, role=role): 3044 return self.has_membership(group_id=group_id, role=role)
3045 return self.requires(has_membership, otherwise=otherwise) 3046
3047 - def requires_permission(self, name, table_name='', record_id=0, 3048 otherwise=None):
3049 """ 3050 decorator that prevents access to action if not logged in or 3051 if user logged in is not a member of any group (role) that 3052 has 'name' access to 'table_name', 'record_id'. 3053 """ 3054 def has_permission(self=self, name=name, table_name=table_name, record_id=record_id): 3055 return self.has_permission(name, table_name, record_id)
3056 return self.requires(has_permission, otherwise=otherwise) 3057
3058 - def requires_signature(self, otherwise=None, hash_vars=True):
3059 """ 3060 decorator that prevents access to action if not logged in or 3061 if user logged in is not a member of group_id. 3062 If role is provided instead of group_id then the 3063 group_id is calculated. 3064 """ 3065 def verify(): 3066 return URL.verify(current.request, user_signature=True, hash_vars=hash_vars)
3067 return self.requires(verify, otherwise) 3068
3069 - def add_group(self, role, description=''):
3070 """ 3071 creates a group associated to a role 3072 """ 3073 3074 group_id = self.table_group().insert( 3075 role=role, description=description) 3076 self.log_event(self.messages.add_group_log, 3077 dict(group_id=group_id, role=role)) 3078 return group_id
3079
3080 - def del_group(self, group_id):
3081 """ 3082 deletes a group 3083 """ 3084 self.db(self.table_group().id == group_id).delete() 3085 self.db(self.table_membership().group_id == group_id).delete() 3086 self.db(self.table_permission().group_id == group_id).delete() 3087 self.update_groups() 3088 self.log_event(self.messages.del_group_log, dict(group_id=group_id))
3089
3090 - def id_group(self, role):
3091 """ 3092 returns the group_id of the group specified by the role 3093 """ 3094 rows = self.db(self.table_group().role == role).select() 3095 if not rows: 3096 return None 3097 return rows[0].id
3098
3099 - def user_group(self, user_id=None):
3100 """ 3101 returns the group_id of the group uniquely associated to this user 3102 i.e. role=user:[user_id] 3103 """ 3104 return self.id_group(self.user_group_role(user_id))
3105
3106 - def user_group_role(self, user_id=None):
3107 if not self.settings.create_user_groups: 3108 return None 3109 if user_id: 3110 user = self.table_user()[user_id] 3111 else: 3112 user = self.user 3113 return self.settings.create_user_groups % user
3114
3115 - def has_membership(self, group_id=None, user_id=None, role=None):
3116 """ 3117 checks if user is member of group_id or role 3118 """ 3119 3120 group_id = group_id or self.id_group(role) 3121 try: 3122 group_id = int(group_id) 3123 except: 3124 group_id = self.id_group(group_id) # interpret group_id as a role 3125 if not user_id and self.user: 3126 user_id = self.user.id 3127 membership = self.table_membership() 3128 if group_id and user_id and self.db((membership.user_id == user_id) 3129 & (membership.group_id == group_id)).select(): 3130 r = True 3131 else: 3132 r = False 3133 self.log_event(self.messages.has_membership_log, 3134 dict(user_id=user_id, group_id=group_id, check=r)) 3135 return r
3136
3137 - def add_membership(self, group_id=None, user_id=None, role=None):
3138 """ 3139 gives user_id membership of group_id or role 3140 if user is None than user_id is that of current logged in user 3141 """ 3142 3143 group_id = group_id or self.id_group(role) 3144 try: 3145 group_id = int(group_id) 3146 except: 3147 group_id = self.id_group(group_id) # interpret group_id as a role 3148 if not user_id and self.user: 3149 user_id = self.user.id 3150 membership = self.table_membership() 3151 record = membership(user_id=user_id, group_id=group_id) 3152 if record: 3153 return record.id 3154 else: 3155 id = membership.insert(group_id=group_id, user_id=user_id) 3156 self.update_groups() 3157 self.log_event(self.messages.add_membership_log, 3158 dict(user_id=user_id, group_id=group_id)) 3159 return id
3160
3161 - def del_membership(self, group_id=None, user_id=None, role=None):
3162 """ 3163 revokes membership from group_id to user_id 3164 if user_id is None than user_id is that of current logged in user 3165 """ 3166 3167 group_id = group_id or self.id_group(role) 3168 if not user_id and self.user: 3169 user_id = self.user.id 3170 membership = self.table_membership() 3171 self.log_event(self.messages.del_membership_log, 3172 dict(user_id=user_id, group_id=group_id)) 3173 ret = self.db(membership.user_id 3174 == user_id)(membership.group_id 3175 == group_id).delete() 3176 self.update_groups() 3177 return ret
3178
3179 - def has_permission( 3180 self, 3181 name='any', 3182 table_name='', 3183 record_id=0, 3184 user_id=None, 3185 group_id=None, 3186 ):
3187 """ 3188 checks if user_id or current logged in user is member of a group 3189 that has 'name' permission on 'table_name' and 'record_id' 3190 if group_id is passed, it checks whether the group has the permission 3191 """ 3192 3193 if not group_id and self.settings.everybody_group_id and \ 3194 self.has_permission( 3195 name, table_name, record_id, user_id=None, 3196 group_id=self.settings.everybody_group_id): 3197 return True 3198 3199 if not user_id and not group_id and self.user: 3200 user_id = self.user.id 3201 if user_id: 3202 membership = self.table_membership() 3203 rows = self.db(membership.user_id 3204 == user_id).select(membership.group_id) 3205 groups = set([row.group_id for row in rows]) 3206 if group_id and not group_id in groups: 3207 return False 3208 else: 3209 groups = set([group_id]) 3210 permission = self.table_permission() 3211 rows = self.db(permission.name == name)(permission.table_name 3212 == str(table_name))(permission.record_id 3213 == record_id).select(permission.group_id) 3214 groups_required = set([row.group_id for row in rows]) 3215 if record_id: 3216 rows = self.db(permission.name 3217 == name)(permission.table_name 3218 == str(table_name))(permission.record_id 3219 == 0).select(permission.group_id) 3220 groups_required = groups_required.union(set([row.group_id 3221 for row in rows])) 3222 if groups.intersection(groups_required): 3223 r = True 3224 else: 3225 r = False 3226 if user_id: 3227 self.log_event(self.messages.has_permission_log, 3228 dict(user_id=user_id, name=name, 3229 table_name=table_name, record_id=record_id)) 3230 return r
3231
3232 - def add_permission( 3233 self, 3234 group_id, 3235 name='any', 3236 table_name='', 3237 record_id=0, 3238 ):
3239 """ 3240 gives group_id 'name' access to 'table_name' and 'record_id' 3241 """ 3242 3243 permission = self.table_permission() 3244 if group_id == 0: 3245 group_id = self.user_group() 3246 record = self.db(permission.group_id == group_id)(permission.name == name)(permission.table_name == str(table_name))( 3247 permission.record_id == long(record_id)).select().first() 3248 if record: 3249 id = record.id 3250 else: 3251 id = permission.insert(group_id=group_id, name=name, 3252 table_name=str(table_name), 3253 record_id=long(record_id)) 3254 self.log_event(self.messages.add_permission_log, 3255 dict(permission_id=id, group_id=group_id, 3256 name=name, table_name=table_name, 3257 record_id=record_id)) 3258 return id
3259
3260 - def del_permission( 3261 self, 3262 group_id, 3263 name='any', 3264 table_name='', 3265 record_id=0, 3266 ):
3267 """ 3268 revokes group_id 'name' access to 'table_name' and 'record_id' 3269 """ 3270 3271 permission = self.table_permission() 3272 self.log_event(self.messages.del_permission_log, 3273 dict(group_id=group_id, name=name, 3274 table_name=table_name, record_id=record_id)) 3275 return self.db(permission.group_id == group_id)(permission.name 3276 == name)(permission.table_name 3277 == str(table_name))(permission.record_id 3278 == long(record_id)).delete()
3279
3280 - def accessible_query(self, name, table, user_id=None):
3281 """ 3282 returns a query with all accessible records for user_id or 3283 the current logged in user 3284 this method does not work on GAE because uses JOIN and IN 3285 3286 example: 3287 3288 db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL) 3289 3290 """ 3291 if not user_id: 3292 user_id = self.user_id 3293 db = self.db 3294 if isinstance(table, str) and table in self.db.tables(): 3295 table = self.db[table] 3296 elif isinstance(table, (Set, Query)): 3297 # experimental: build a chained query for all tables 3298 if isinstance(table, Set): 3299 cquery = table.query 3300 else: 3301 cquery = table 3302 tablenames = db._adapter.tables(cquery) 3303 for tablename in tablenames: 3304 cquery &= self.accessible_query(name, tablename, 3305 user_id=user_id) 3306 return cquery 3307 if not isinstance(table, str) and\ 3308 self.has_permission(name, table, 0, user_id): 3309 return table.id > 0 3310 membership = self.table_membership() 3311 permission = self.table_permission() 3312 query = table.id.belongs( 3313 db(membership.user_id == user_id) 3314 (membership.group_id == permission.group_id) 3315 (permission.name == name) 3316 (permission.table_name == table) 3317 ._select(permission.record_id)) 3318 if self.settings.everybody_group_id: 3319 query |= table.id.belongs( 3320 db(permission.group_id == self.settings.everybody_group_id) 3321 (permission.name == name) 3322 (permission.table_name == table) 3323 ._select(permission.record_id)) 3324 return query
3325 3326 @staticmethod
3327 - def archive(form, 3328 archive_table=None, 3329 current_record='current_record', 3330 archive_current=False, 3331 fields=None):
3332 """ 3333 If you have a table (db.mytable) that needs full revision history you can just do: 3334 3335 form=crud.update(db.mytable,myrecord,onaccept=auth.archive) 3336 3337 or 3338 3339 form=SQLFORM(db.mytable,myrecord).process(onaccept=auth.archive) 3340 3341 crud.archive will define a new table "mytable_archive" and store 3342 a copy of the current record (if archive_current=True) 3343 or a copy of the previous record (if archive_current=False) 3344 in the newly created table including a reference 3345 to the current record. 3346 3347 fields allows to specify extra fields that need to be archived. 3348 3349 If you want to access such table you need to define it yourself 3350 in a model: 3351 3352 db.define_table('mytable_archive', 3353 Field('current_record',db.mytable), 3354 db.mytable) 3355 3356 Notice such table includes all fields of db.mytable plus one: current_record. 3357 crud.archive does not timestamp the stored record unless your original table 3358 has a fields like: 3359 3360 db.define_table(..., 3361 Field('saved_on','datetime', 3362 default=request.now,update=request.now,writable=False), 3363 Field('saved_by',auth.user, 3364 default=auth.user_id,update=auth.user_id,writable=False), 3365 3366 there is nothing special about these fields since they are filled before 3367 the record is archived. 3368 3369 If you want to change the archive table name and the name of the reference field 3370 you can do, for example: 3371 3372 db.define_table('myhistory', 3373 Field('parent_record',db.mytable), 3374 db.mytable) 3375 3376 and use it as: 3377 3378 form=crud.update(db.mytable,myrecord, 3379 onaccept=lambda form:crud.archive(form, 3380 archive_table=db.myhistory, 3381 current_record='parent_record')) 3382 3383 """ 3384 if not archive_current and not form.record: 3385 return None 3386 table = form.table 3387 if not archive_table: 3388 archive_table_name = '%s_archive' % table 3389 if not archive_table_name in table._db: 3390 table._db.define_table( 3391 archive_table_name, 3392 Field(current_record, table), 3393 *[field.clone(unique=False) for field in table]) 3394 archive_table = table._db[archive_table_name] 3395 new_record = {current_record: form.vars.id} 3396 for fieldname in archive_table.fields: 3397 if not fieldname in ['id', current_record]: 3398 if archive_current and fieldname in form.vars: 3399 new_record[fieldname] = form.vars[fieldname] 3400 elif form.record and fieldname in form.record: 3401 new_record[fieldname] = form.record[fieldname] 3402 if fields: 3403 new_record.update(fields) 3404 id = archive_table.insert(**new_record) 3405 return id
3406
3407 - def wiki(self, 3408 slug=None, 3409 env=None, 3410 render='markmin', 3411 manage_permissions=False, 3412 force_prefix='', 3413 restrict_search=False, 3414 resolve=True, 3415 extra=None, 3416 menu_groups=None, 3417 templates=None, 3418 migrate=True, 3419 controller=None, 3420 function=None):
3421 3422 if controller and function: resolve = False 3423 3424 if not hasattr(self, '_wiki'): 3425 self._wiki = Wiki(self, render=render, 3426 manage_permissions=manage_permissions, 3427 force_prefix=force_prefix, 3428 restrict_search=restrict_search, 3429 env=env, extra=extra or {}, 3430 menu_groups=menu_groups, 3431 templates=templates, 3432 migrate=migrate, 3433 controller=controller, 3434 function=function) 3435 else: 3436 self._wiki.env.update(env or {}) 3437 3438 # if resolve is set to True, process request as wiki call 3439 # resolve=False allows initial setup without wiki redirection 3440 wiki = None 3441 if resolve: 3442 action = str(current.request.args(0)).startswith("_") 3443 if slug and not action: 3444 wiki = self._wiki.read(slug) 3445 if isinstance(wiki, dict) and wiki.has_key('content'): 3446 # We don't want to return a dict object, just the wiki 3447 wiki = wiki['content'] 3448 else: 3449 wiki = self._wiki() 3450 if isinstance(wiki, basestring): 3451 wiki = XML(wiki) 3452 return wiki
3453
3454 - def wikimenu(self):
3455 """to be used in menu.py for app wide wiki menus""" 3456 if (hasattr(self, "_wiki") and 3457 self._wiki.settings.controller and 3458 self._wiki.settings.function): 3459 self._wiki.automenu()
3460
3461 3462 -class Crud(object):
3463
3464 - def url(self, f=None, args=None, vars=None):
3465 """ 3466 this should point to the controller that exposes 3467 download and crud 3468 """ 3469 if args is None: 3470 args = [] 3471 if vars is None: 3472 vars = {} 3473 return URL(c=self.settings.controller, f=f, args=args, vars=vars)
3474
3475 - def __init__(self, environment, db=None, controller='default'):
3476 self.db = db 3477 if not db and environment and isinstance(environment, DAL): 3478 self.db = environment 3479 elif not db: 3480 raise SyntaxError("must pass db as first or second argument") 3481 self.environment = current 3482 settings = self.settings = Settings() 3483 settings.auth = None 3484 settings.logger = None 3485 3486 settings.create_next = None 3487 settings.update_next = None 3488 settings.controller = controller 3489 settings.delete_next = self.url() 3490 settings.download_url = self.url('download') 3491 settings.create_onvalidation = StorageList() 3492 settings.update_onvalidation = StorageList() 3493 settings.delete_onvalidation = StorageList() 3494 settings.create_onaccept = StorageList() 3495 settings.update_onaccept = StorageList() 3496 settings.update_ondelete = StorageList() 3497 settings.delete_onaccept = StorageList() 3498 settings.update_deletable = True 3499 settings.showid = False 3500 settings.keepvalues = False 3501 settings.create_captcha = None 3502 settings.update_captcha = None 3503 settings.captcha = None 3504 settings.formstyle = 'table3cols' 3505 settings.label_separator = ': ' 3506 settings.hideerror = False 3507 settings.detect_record_change = True 3508 settings.hmac_key = None 3509 settings.lock_keys = True 3510 3511 messages = self.messages = Messages(current.T) 3512 messages.submit_button = 'Submit' 3513 messages.delete_label = 'Check to delete' 3514 messages.record_created = 'Record Created' 3515 messages.record_updated = 'Record Updated' 3516 messages.record_deleted = 'Record Deleted' 3517 3518 messages.update_log = 'Record %(id)s updated' 3519 messages.create_log = 'Record %(id)s created' 3520 messages.read_log = 'Record %(id)s read' 3521 messages.delete_log = 'Record %(id)s deleted' 3522 3523 messages.lock_keys = True
3524
3525 - def __call__(self):
3526 args = current.request.args 3527 if len(args) < 1: 3528 raise HTTP(404) 3529 elif args[0] == 'tables': 3530 return self.tables() 3531 elif len(args) > 1 and not args(1) in self.db.tables: 3532 raise HTTP(404) 3533 table = self.db[args(1)] 3534 if args[0] == 'create': 3535 return self.create(table) 3536 elif args[0] == 'select': 3537 return self.select(table, linkto=self.url(args='read')) 3538 elif args[0] == 'search': 3539 form, rows = self.search(table, linkto=self.url(args='read')) 3540 return DIV(form, SQLTABLE(rows)) 3541 elif args[0] == 'read': 3542 return self.read(table, args(2)) 3543 elif args[0] == 'update': 3544 return self.update(table, args(2)) 3545 elif args[0] == 'delete': 3546 return self.delete(table, args(2)) 3547 else: 3548 raise HTTP(404)
3549
3550 - def log_event(self, message, vars):
3551 if self.settings.logger: 3552 self.settings.logger.log_event(message, vars, origin='crud')
3553
3554 - def has_permission(self, name, table, record=0):
3555 if not self.settings.auth: 3556 return True 3557 try: 3558 record_id = record.id 3559 except: 3560 record_id = record 3561 return self.settings.auth.has_permission(name, str(table), record_id)
3562
3563 - def tables(self):
3564 return TABLE(*[TR(A(name, 3565 _href=self.url(args=('select', name)))) 3566 for name in self.db.tables])
3567 3568 @staticmethod
3569 - def archive(form, archive_table=None, current_record='current_record'):
3570 return Auth.archive(form, archive_table=archive_table, 3571 current_record=current_record)
3572
3573 - def update( 3574 self, 3575 table, 3576 record, 3577 next=DEFAULT, 3578 onvalidation=DEFAULT, 3579 onaccept=DEFAULT, 3580 ondelete=DEFAULT, 3581 log=DEFAULT, 3582 message=DEFAULT, 3583 deletable=DEFAULT, 3584 formname=DEFAULT, 3585 **attributes 3586 ):
3587 """ 3588 method: Crud.update(table, record, [next=DEFAULT 3589 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT 3590 [, message=DEFAULT[, deletable=DEFAULT]]]]]]) 3591 3592 """ 3593 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 3594 or (isinstance(record, str) and not str(record).isdigit()): 3595 raise HTTP(404) 3596 if not isinstance(table, self.db.Table): 3597 table = self.db[table] 3598 try: 3599 record_id = record.id 3600 except: 3601 record_id = record or 0 3602 if record_id and not self.has_permission('update', table, record_id): 3603 redirect(self.settings.auth.settings.on_failed_authorization) 3604 if not record_id and not self.has_permission('create', table, record_id): 3605 redirect(self.settings.auth.settings.on_failed_authorization) 3606 3607 request = current.request 3608 response = current.response 3609 session = current.session 3610 if request.extension == 'json' and request.vars.json: 3611 request.vars.update(json_parser.loads(request.vars.json)) 3612 if next is DEFAULT: 3613 next = request.get_vars._next \ 3614 or request.post_vars._next \ 3615 or self.settings.update_next 3616 if onvalidation is DEFAULT: 3617 onvalidation = self.settings.update_onvalidation 3618 if onaccept is DEFAULT: 3619 onaccept = self.settings.update_onaccept 3620 if ondelete is DEFAULT: 3621 ondelete = self.settings.update_ondelete 3622 if log is DEFAULT: 3623 log = self.messages.update_log 3624 if deletable is DEFAULT: 3625 deletable = self.settings.update_deletable 3626 if message is DEFAULT: 3627 message = self.messages.record_updated 3628 if not 'hidden' in attributes: 3629 attributes['hidden'] = {} 3630 attributes['hidden']['_next'] = next 3631 form = SQLFORM( 3632 table, 3633 record, 3634 showid=self.settings.showid, 3635 submit_button=self.messages.submit_button, 3636 delete_label=self.messages.delete_label, 3637 deletable=deletable, 3638 upload=self.settings.download_url, 3639 formstyle=self.settings.formstyle, 3640 separator=self.settings.label_separator, 3641 **attributes # contains hidden 3642 ) 3643 self.accepted = False 3644 self.deleted = False 3645 captcha = self.settings.update_captcha or self.settings.captcha 3646 if record and captcha: 3647 addrow(form, captcha.label, captcha, captcha.comment, 3648 self.settings.formstyle, 'captcha__row') 3649 captcha = self.settings.create_captcha or self.settings.captcha 3650 if not record and captcha: 3651 addrow(form, captcha.label, captcha, captcha.comment, 3652 self.settings.formstyle, 'captcha__row') 3653 if not request.extension in ('html', 'load'): 3654 (_session, _formname) = (None, None) 3655 else: 3656 (_session, _formname) = ( 3657 session, '%s/%s' % (table._tablename, form.record_id)) 3658 if not formname is DEFAULT: 3659 _formname = formname 3660 keepvalues = self.settings.keepvalues 3661 if request.vars.delete_this_record: 3662 keepvalues = False 3663 if isinstance(onvalidation, StorageList): 3664 onvalidation = onvalidation.get(table._tablename, []) 3665 if form.accepts(request, _session, formname=_formname, 3666 onvalidation=onvalidation, keepvalues=keepvalues, 3667 hideerror=self.settings.hideerror, 3668 detect_record_change=self.settings.detect_record_change): 3669 self.accepted = True 3670 response.flash = message 3671 if log: 3672 self.log_event(log, form.vars) 3673 if request.vars.delete_this_record: 3674 self.deleted = True 3675 message = self.messages.record_deleted 3676 callback(ondelete, form, table._tablename) 3677 response.flash = message 3678 callback(onaccept, form, table._tablename) 3679 if not request.extension in ('html', 'load'): 3680 raise HTTP(200, 'RECORD CREATED/UPDATED') 3681 if isinstance(next, (list, tuple)): # fix issue with 2.6 3682 next = next[0] 3683 if next: # Only redirect when explicit 3684 next = replace_id(next, form) 3685 session.flash = response.flash 3686 redirect(next) 3687 elif not request.extension in ('html', 'load'): 3688 raise HTTP(401, serializers.json(dict(errors=form.errors))) 3689 return form
3690
3691 - def create( 3692 self, 3693 table, 3694 next=DEFAULT, 3695 onvalidation=DEFAULT, 3696 onaccept=DEFAULT, 3697 log=DEFAULT, 3698 message=DEFAULT, 3699 formname=DEFAULT, 3700 **attributes 3701 ):
3702 """ 3703 method: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT 3704 [, onaccept=DEFAULT [, log=DEFAULT[, message=DEFAULT]]]]]) 3705 """ 3706 3707 if next is DEFAULT: 3708 next = self.settings.create_next 3709 if onvalidation is DEFAULT: 3710 onvalidation = self.settings.create_onvalidation 3711 if onaccept is DEFAULT: 3712 onaccept = self.settings.create_onaccept 3713 if log is DEFAULT: 3714 log = self.messages.create_log 3715 if message is DEFAULT: 3716 message = self.messages.record_created 3717 return self.update( 3718 table, 3719 None, 3720 next=next, 3721 onvalidation=onvalidation, 3722 onaccept=onaccept, 3723 log=log, 3724 message=message, 3725 deletable=False, 3726 formname=formname, 3727 **attributes 3728 )
3729
3730 - def read(self, table, record):
3731 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 3732 or (isinstance(record, str) and not str(record).isdigit()): 3733 raise HTTP(404) 3734 if not isinstance(table, self.db.Table): 3735 table = self.db[table] 3736 if not self.has_permission('read', table, record): 3737 redirect(self.settings.auth.settings.on_failed_authorization) 3738 form = SQLFORM( 3739 table, 3740 record, 3741 readonly=True, 3742 comments=False, 3743 upload=self.settings.download_url, 3744 showid=self.settings.showid, 3745 formstyle=self.settings.formstyle, 3746 separator=self.settings.label_separator 3747 ) 3748 if not current.request.extension in ('html', 'load'): 3749 return table._filter_fields(form.record, id=True) 3750 return form
3751
3752 - def delete( 3753 self, 3754 table, 3755 record_id, 3756 next=DEFAULT, 3757 message=DEFAULT, 3758 ):
3759 """ 3760 method: Crud.delete(table, record_id, [next=DEFAULT 3761 [, message=DEFAULT]]) 3762 """ 3763 if not (isinstance(table, self.db.Table) or table in self.db.tables): 3764 raise HTTP(404) 3765 if not isinstance(table, self.db.Table): 3766 table = self.db[table] 3767 if not self.has_permission('delete', table, record_id): 3768 redirect(self.settings.auth.settings.on_failed_authorization) 3769 request = current.request 3770 session = current.session 3771 if next is DEFAULT: 3772 next = request.get_vars._next \ 3773 or request.post_vars._next \ 3774 or self.settings.delete_next 3775 if message is DEFAULT: 3776 message = self.messages.record_deleted 3777 record = table[record_id] 3778 if record: 3779 callback(self.settings.delete_onvalidation, record) 3780 del table[record_id] 3781 callback(self.settings.delete_onaccept, record, table._tablename) 3782 session.flash = message 3783 redirect(next)
3784
3785 - def rows( 3786 self, 3787 table, 3788 query=None, 3789 fields=None, 3790 orderby=None, 3791 limitby=None, 3792 ):
3793 if not (isinstance(table, self.db.Table) or table in self.db.tables): 3794 raise HTTP(404) 3795 if not self.has_permission('select', table): 3796 redirect(self.settings.auth.settings.on_failed_authorization) 3797 #if record_id and not self.has_permission('select', table): 3798 # redirect(self.settings.auth.settings.on_failed_authorization) 3799 if not isinstance(table, self.db.Table): 3800 table = self.db[table] 3801 if not query: 3802 query = table.id > 0 3803 if not fields: 3804 fields = [field for field in table if field.readable] 3805 else: 3806 fields = [table[f] if isinstance(f, str) else f for f in fields] 3807 rows = self.db(query).select(*fields, **dict(orderby=orderby, 3808 limitby=limitby)) 3809 return rows
3810
3811 - def select( 3812 self, 3813 table, 3814 query=None, 3815 fields=None, 3816 orderby=None, 3817 limitby=None, 3818 headers=None, 3819 **attr 3820 ):
3821 headers = headers or {} 3822 rows = self.rows(table, query, fields, orderby, limitby) 3823 if not rows: 3824 return None # Nicer than an empty table. 3825 if not 'upload' in attr: 3826 attr['upload'] = self.url('download') 3827 if not current.request.extension in ('html', 'load'): 3828 return rows.as_list() 3829 if not headers: 3830 if isinstance(table, str): 3831 table = self.db[table] 3832 headers = dict((str(k), k.label) for k in table) 3833 return SQLTABLE(rows, headers=headers, **attr)
3834
3835 - def get_format(self, field):
3836 rtable = field._db[field.type[10:]] 3837 format = rtable.get('_format', None) 3838 if format and isinstance(format, str): 3839 return format[2:-2] 3840 return field.name
3841
3842 - def get_query(self, field, op, value, refsearch=False):
3843 try: 3844 if refsearch: 3845 format = self.get_format(field) 3846 if op == 'equals': 3847 if not refsearch: 3848 return field == value 3849 else: 3850 return lambda row: row[field.name][format] == value 3851 elif op == 'not equal': 3852 if not refsearch: 3853 return field != value 3854 else: 3855 return lambda row: row[field.name][format] != value 3856 elif op == 'greater than': 3857 if not refsearch: 3858 return field > value 3859 else: 3860 return lambda row: row[field.name][format] > value 3861 elif op == 'less than': 3862 if not refsearch: 3863 return field < value 3864 else: 3865 return lambda row: row[field.name][format] < value 3866 elif op == 'starts with': 3867 if not refsearch: 3868 return field.like(value + '%') 3869 else: 3870 return lambda row: str(row[field.name][format]).startswith(value) 3871 elif op == 'ends with': 3872 if not refsearch: 3873 return field.like('%' + value) 3874 else: 3875 return lambda row: str(row[field.name][format]).endswith(value) 3876 elif op == 'contains': 3877 if not refsearch: 3878 return field.like('%' + value + '%') 3879 else: 3880 return lambda row: value in row[field.name][format] 3881 except: 3882 return None
3883
3884 - def search(self, *tables, **args):
3885 """ 3886 Creates a search form and its results for a table 3887 Example usage: 3888 form, results = crud.search(db.test, 3889 queries = ['equals', 'not equal', 'contains'], 3890 query_labels={'equals':'Equals', 3891 'not equal':'Not equal'}, 3892 fields = ['id','children'], 3893 field_labels = { 3894 'id':'ID','children':'Children'}, 3895 zero='Please choose', 3896 query = (db.test.id > 0)&(db.test.id != 3) ) 3897 """ 3898 table = tables[0] 3899 fields = args.get('fields', table.fields) 3900 request = current.request 3901 db = self.db 3902 if not (isinstance(table, db.Table) or table in db.tables): 3903 raise HTTP(404) 3904 attributes = {} 3905 for key in ('orderby', 'groupby', 'left', 'distinct', 'limitby', 'cache'): 3906 if key in args: 3907 attributes[key] = args[key] 3908 tbl = TABLE() 3909 selected = [] 3910 refsearch = [] 3911 results = [] 3912 showall = args.get('showall', False) 3913 if showall: 3914 selected = fields 3915 chkall = args.get('chkall', False) 3916 if chkall: 3917 for f in fields: 3918 request.vars['chk%s' % f] = 'on' 3919 ops = args.get('queries', []) 3920 zero = args.get('zero', '') 3921 if not ops: 3922 ops = ['equals', 'not equal', 'greater than', 3923 'less than', 'starts with', 3924 'ends with', 'contains'] 3925 ops.insert(0, zero) 3926 query_labels = args.get('query_labels', {}) 3927 query = args.get('query', table.id > 0) 3928 field_labels = args.get('field_labels', {}) 3929 for field in fields: 3930 field = table[field] 3931 if not field.readable: 3932 continue 3933 fieldname = field.name 3934 chkval = request.vars.get('chk' + fieldname, None) 3935 txtval = request.vars.get('txt' + fieldname, None) 3936 opval = request.vars.get('op' + fieldname, None) 3937 row = TR(TD(INPUT(_type="checkbox", _name="chk" + fieldname, 3938 _disabled=(field.type == 'id'), 3939 value=(field.type == 'id' or chkval == 'on'))), 3940 TD(field_labels.get(fieldname, field.label)), 3941 TD(SELECT([OPTION(query_labels.get(op, op), 3942 _value=op) for op in ops], 3943 _name="op" + fieldname, 3944 value=opval)), 3945 TD(INPUT(_type="text", _name="txt" + fieldname, 3946 _value=txtval, _id='txt' + fieldname, 3947 _class=str(field.type)))) 3948 tbl.append(row) 3949 if request.post_vars and (chkval or field.type == 'id'): 3950 if txtval and opval != '': 3951 if field.type[0:10] == 'reference ': 3952 refsearch.append(self.get_query(field, 3953 opval, txtval, refsearch=True)) 3954 else: 3955 value, error = field.validate(txtval) 3956 if not error: 3957 ### TODO deal with 'starts with', 'ends with', 'contains' on GAE 3958 query &= self.get_query(field, opval, value) 3959 else: 3960 row[3].append(DIV(error, _class='error')) 3961 selected.append(field) 3962 form = FORM(tbl, INPUT(_type="submit")) 3963 if selected: 3964 try: 3965 results = db(query).select(*selected, **attributes) 3966 for r in refsearch: 3967 results = results.find(r) 3968 except: # hmmm, we should do better here 3969 results = None 3970 return form, results
3971 3972 3973 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor()))
3974 3975 3976 -def fetch(url, data=None, headers=None, 3977 cookie=Cookie.SimpleCookie(), 3978 user_agent='Mozilla/5.0'):
3979 headers = headers or {} 3980 if not data is None: 3981 data = urllib.urlencode(data) 3982 if user_agent: 3983 headers['User-agent'] = user_agent 3984 headers['Cookie'] = ' '.join( 3985 ['%s=%s;' % (c.key, c.value) for c in cookie.values()]) 3986 try: 3987 from google.appengine.api import urlfetch 3988 except ImportError: 3989 req = urllib2.Request(url, data, headers) 3990 html = urllib2.urlopen(req).read() 3991 else: 3992 method = ((data is None) and urlfetch.GET) or urlfetch.POST 3993 while url is not None: 3994 response = urlfetch.fetch(url=url, payload=data, 3995 method=method, headers=headers, 3996 allow_truncated=False, follow_redirects=False, 3997 deadline=10) 3998 # next request will be a get, so no need to send the data again 3999 data = None 4000 method = urlfetch.GET 4001 # load cookies from the response 4002 cookie.load(response.headers.get('set-cookie', '')) 4003 url = response.headers.get('location') 4004 html = response.content 4005 return html
4006 4007 regex_geocode = \ 4008 re.compile(r"""<geometry>[\W]*?<location>[\W]*?<lat>(?P<la>[^<]*)</lat>[\W]*?<lng>(?P<lo>[^<]*)</lng>[\W]*?</location>""")
4009 4010 4011 -def geocode(address):
4012 try: 4013 a = urllib.quote(address) 4014 txt = fetch('http://maps.googleapis.com/maps/api/geocode/xml?sensor=false&address=%s' 4015 % a) 4016 item = regex_geocode.search(txt) 4017 (la, lo) = (float(item.group('la')), float(item.group('lo'))) 4018 return (la, lo) 4019 except: 4020 return (0.0, 0.0)
4021
4022 4023 -def universal_caller(f, *a, **b):
4024 c = f.func_code.co_argcount 4025 n = f.func_code.co_varnames[:c] 4026 4027 defaults = f.func_defaults or [] 4028 pos_args = n[0:-len(defaults)] 4029 named_args = n[-len(defaults):] 4030 4031 arg_dict = {} 4032 4033 # Fill the arg_dict with name and value for the submitted, positional values 4034 for pos_index, pos_val in enumerate(a[:c]): 4035 arg_dict[n[pos_index] 4036 ] = pos_val # n[pos_index] is the name of the argument 4037 4038 # There might be pos_args left, that are sent as named_values. Gather them as well. 4039 # If a argument already is populated with values we simply replaces them. 4040 for arg_name in pos_args[len(arg_dict):]: 4041 if arg_name in b: 4042 arg_dict[arg_name] = b[arg_name] 4043 4044 if len(arg_dict) >= len(pos_args): 4045 # All the positional arguments is found. The function may now be called. 4046 # However, we need to update the arg_dict with the values from the named arguments as well. 4047 for arg_name in named_args: 4048 if arg_name in b: 4049 arg_dict[arg_name] = b[arg_name] 4050 4051 return f(**arg_dict) 4052 4053 # Raise an error, the function cannot be called. 4054 raise HTTP(404, "Object does not exist")
4055
4056 4057 -class Service(object):
4058
4059 - def __init__(self, environment=None):
4060 self.run_procedures = {} 4061 self.csv_procedures = {} 4062 self.xml_procedures = {} 4063 self.rss_procedures = {} 4064 self.json_procedures = {} 4065 self.jsonrpc_procedures = {} 4066 self.jsonrpc2_procedures = {} 4067 self.xmlrpc_procedures = {} 4068 self.amfrpc_procedures = {} 4069 self.amfrpc3_procedures = {} 4070 self.soap_procedures = {}
4071
4072 - def run(self, f):
4073 """ 4074 example: 4075 4076 service = Service() 4077 @service.run 4078 def myfunction(a, b): 4079 return a + b 4080 def call(): 4081 return service() 4082 4083 Then call it with: 4084 4085 wget http://..../app/default/call/run/myfunction?a=3&b=4 4086 4087 """ 4088 self.run_procedures[f.__name__] = f 4089 return f
4090
4091 - def csv(self, f):
4092 """ 4093 example: 4094 4095 service = Service() 4096 @service.csv 4097 def myfunction(a, b): 4098 return a + b 4099 def call(): 4100 return service() 4101 4102 Then call it with: 4103 4104 wget http://..../app/default/call/csv/myfunction?a=3&b=4 4105 4106 """ 4107 self.run_procedures[f.__name__] = f 4108 return f
4109
4110 - def xml(self, f):
4111 """ 4112 example: 4113 4114 service = Service() 4115 @service.xml 4116 def myfunction(a, b): 4117 return a + b 4118 def call(): 4119 return service() 4120 4121 Then call it with: 4122 4123 wget http://..../app/default/call/xml/myfunction?a=3&b=4 4124 4125 """ 4126 self.run_procedures[f.__name__] = f 4127 return f
4128
4129 - def rss(self, f):
4130 """ 4131 example: 4132 4133 service = Service() 4134 @service.rss 4135 def myfunction(): 4136 return dict(title=..., link=..., description=..., 4137 created_on=..., entries=[dict(title=..., link=..., 4138 description=..., created_on=...]) 4139 def call(): 4140 return service() 4141 4142 Then call it with: 4143 4144 wget http://..../app/default/call/rss/myfunction 4145 4146 """ 4147 self.rss_procedures[f.__name__] = f 4148 return f
4149
4150 - def json(self, f):
4151 """ 4152 example: 4153 4154 service = Service() 4155 @service.json 4156 def myfunction(a, b): 4157 return [{a: b}] 4158 def call(): 4159 return service() 4160 4161 Then call it with: 4162 4163 wget http://..../app/default/call/json/myfunction?a=hello&b=world 4164 4165 """ 4166 self.json_procedures[f.__name__] = f 4167 return f
4168
4169 - def jsonrpc(self, f):
4170 """ 4171 example: 4172 4173 service = Service() 4174 @service.jsonrpc 4175 def myfunction(a, b): 4176 return a + b 4177 def call(): 4178 return service() 4179 4180 Then call it with: 4181 4182 wget http://..../app/default/call/jsonrpc/myfunction?a=hello&b=world 4183 4184 """ 4185 self.jsonrpc_procedures[f.__name__] = f 4186 return f
4187
4188 - def jsonrpc2(self, f):
4189 """ 4190 example: 4191 4192 service = Service() 4193 @service.jsonrpc2 4194 def myfunction(a, b): 4195 return a + b 4196 def call(): 4197 return service() 4198 4199 Then call it with: 4200 4201 wget --post-data '{"jsonrpc": "2.0", "id": 1, "method": "myfunction", "params": {"a": 1, "b": 2}}' http://..../app/default/call/jsonrpc2 4202 4203 """ 4204 self.jsonrpc2_procedures[f.__name__] = f 4205 return f
4206
4207 - def xmlrpc(self, f):
4208 """ 4209 example: 4210 4211 service = Service() 4212 @service.xmlrpc 4213 def myfunction(a, b): 4214 return a + b 4215 def call(): 4216 return service() 4217 4218 The call it with: 4219 4220 wget http://..../app/default/call/xmlrpc/myfunction?a=hello&b=world 4221 4222 """ 4223 self.xmlrpc_procedures[f.__name__] = f 4224 return f
4225
4226 - def amfrpc(self, f):
4227 """ 4228 example: 4229 4230 service = Service() 4231 @service.amfrpc 4232 def myfunction(a, b): 4233 return a + b 4234 def call(): 4235 return service() 4236 4237 The call it with: 4238 4239 wget http://..../app/default/call/amfrpc/myfunction?a=hello&b=world 4240 4241 """ 4242 self.amfrpc_procedures[f.__name__] = f 4243 return f
4244
4245 - def amfrpc3(self, domain='default'):
4246 """ 4247 example: 4248 4249 service = Service() 4250 @service.amfrpc3('domain') 4251 def myfunction(a, b): 4252 return a + b 4253 def call(): 4254 return service() 4255 4256 The call it with: 4257 4258 wget http://..../app/default/call/amfrpc3/myfunction?a=hello&b=world 4259 4260 """ 4261 if not isinstance(domain, str): 4262 raise SyntaxError("AMF3 requires a domain for function") 4263 4264 def _amfrpc3(f): 4265 if domain: 4266 self.amfrpc3_procedures[domain + '.' + f.__name__] = f 4267 else: 4268 self.amfrpc3_procedures[f.__name__] = f 4269 return f
4270 return _amfrpc3
4271
4272 - def soap(self, name=None, returns=None, args=None, doc=None):
4273 """ 4274 example: 4275 4276 service = Service() 4277 @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,}) 4278 def myfunction(a, b): 4279 return a + b 4280 def call(): 4281 return service() 4282 4283 The call it with: 4284 4285 from gluon.contrib.pysimplesoap.client import SoapClient 4286 client = SoapClient(wsdl="http://..../app/default/call/soap?WSDL") 4287 response = client.MyFunction(a=1,b=2) 4288 return response['result'] 4289 4290 Exposes online generated documentation and xml example messages at: 4291 - http://..../app/default/call/soap 4292 """ 4293 4294 def _soap(f): 4295 self.soap_procedures[name or f.__name__] = f, returns, args, doc 4296 return f
4297 return _soap 4298
4299 - def serve_run(self, args=None):
4300 request = current.request 4301 if not args: 4302 args = request.args 4303 if args and args[0] in self.run_procedures: 4304 return str(universal_caller(self.run_procedures[args[0]], 4305 *args[1:], **dict(request.vars))) 4306 self.error()
4307
4308 - def serve_csv(self, args=None):
4309 request = current.request 4310 response = current.response 4311 response.headers['Content-Type'] = 'text/x-csv' 4312 if not args: 4313 args = request.args 4314 4315 def none_exception(value): 4316 if isinstance(value, unicode): 4317 return value.encode('utf8') 4318 if hasattr(value, 'isoformat'): 4319 return value.isoformat()[:19].replace('T', ' ') 4320 if value is None: 4321 return '<NULL>' 4322 return value
4323 if args and args[0] in self.run_procedures: 4324 import types 4325 r = universal_caller(self.run_procedures[args[0]], 4326 *args[1:], **dict(request.vars)) 4327 s = cStringIO.StringIO() 4328 if hasattr(r, 'export_to_csv_file'): 4329 r.export_to_csv_file(s) 4330 elif r and not isinstance(r, types.GeneratorType) and isinstance(r[0], (dict, Storage)): 4331 import csv 4332 writer = csv.writer(s) 4333 writer.writerow(r[0].keys()) 4334 for line in r: 4335 writer.writerow([none_exception(v) 4336 for v in line.values()]) 4337 else: 4338 import csv 4339 writer = csv.writer(s) 4340 for line in r: 4341 writer.writerow(line) 4342 return s.getvalue() 4343 self.error() 4344
4345 - def serve_xml(self, args=None):
4346 request = current.request 4347 response = current.response 4348 response.headers['Content-Type'] = 'text/xml' 4349 if not args: 4350 args = request.args 4351 if args and args[0] in self.run_procedures: 4352 s = universal_caller(self.run_procedures[args[0]], 4353 *args[1:], **dict(request.vars)) 4354 if hasattr(s, 'as_list'): 4355 s = s.as_list() 4356 return serializers.xml(s, quote=False) 4357 self.error()
4358
4359 - def serve_rss(self, args=None):
4360 request = current.request 4361 response = current.response 4362 if not args: 4363 args = request.args 4364 if args and args[0] in self.rss_procedures: 4365 feed = universal_caller(self.rss_procedures[args[0]], 4366 *args[1:], **dict(request.vars)) 4367 else: 4368 self.error() 4369 response.headers['Content-Type'] = 'application/rss+xml' 4370 return serializers.rss(feed)
4371
4372 - def serve_json(self, args=None):
4373 request = current.request 4374 response = current.response 4375 response.headers['Content-Type'] = 'application/json; charset=utf-8' 4376 if not args: 4377 args = request.args 4378 d = dict(request.vars) 4379 if args and args[0] in self.json_procedures: 4380 s = universal_caller(self.json_procedures[args[0]], *args[1:], **d) 4381 if hasattr(s, 'as_list'): 4382 s = s.as_list() 4383 return response.json(s) 4384 self.error()
4385
4386 - class JsonRpcException(Exception):
4387 - def __init__(self, code, info):
4388 jrpc_error = Service.jsonrpc_errors.get(code) 4389 if jrpc_error: 4390 self.message, self.description = jrpc_error 4391 self.code, self.info = code, info
4392 4393 # jsonrpc 2.0 error types. records the following structure {code: (message,meaning)} 4394 jsonrpc_errors = { 4395 -32700: ("Parse error. Invalid JSON was received by the server.", "An error occurred on the server while parsing the JSON text."), 4396 -32600: ("Invalid Request", "The JSON sent is not a valid Request object."), 4397 -32601: ("Method not found", "The method does not exist / is not available."), 4398 -32602: ("Invalid params", "Invalid method parameter(s)."), 4399 -32603: ("Internal error", "Internal JSON-RPC error."), 4400 -32099: ("Server error", "Reserved for implementation-defined server-errors.")} 4401 4402
4403 - def serve_jsonrpc(self):
4404 def return_response(id, result): 4405 return serializers.json({'version': '1.1', 4406 'id': id, 'result': result, 'error': None})
4407 4408 def return_error(id, code, message, data=None): 4409 error = {'name': 'JSONRPCError', 4410 'code': code, 'message': message} 4411 if data is not None: 4412 error['data'] = data 4413 return serializers.json({'id': id, 4414 'version': '1.1', 4415 'error': error, 4416 }) 4417 4418 request = current.request 4419 response = current.response 4420 response.headers['Content-Type'] = 'application/json; charset=utf-8' 4421 methods = self.jsonrpc_procedures 4422 data = json_parser.loads(request.body.read()) 4423 jsonrpc_2 = data.get('jsonrpc') 4424 if jsonrpc_2: #hand over to version 2 of the protocol 4425 return self.serve_jsonrpc2(data) 4426 id, method, params = data['id'], data['method'], data.get('params', '') 4427 if not method in methods: 4428 return return_error(id, 100, 'method "%s" does not exist' % method) 4429 try: 4430 if isinstance(params,dict): 4431 s = methods[method](**params) 4432 else: 4433 s = methods[method](*params) 4434 if hasattr(s, 'as_list'): 4435 s = s.as_list() 4436 return return_response(id, s) 4437 except Service.JsonRpcException, e: 4438 return return_error(id, e.code, e.info) 4439 except BaseException: 4440 etype, eval, etb = sys.exc_info() 4441 code = 100 4442 message = '%s: %s' % (etype.__name__, eval) 4443 data = request.is_local and traceback.format_tb(etb) 4444 return return_error(id, code, message, data) 4445 except: 4446 etype, eval, etb = sys.exc_info() 4447 return return_error(id, 100, 'Exception %s: %s' % (etype, eval)) 4448
4449 - def serve_jsonrpc2(self, data=None, batch_element=False):
4450 4451 def return_response(id, result): 4452 if not must_respond: 4453 return None 4454 return serializers.json({'jsonrpc': '2.0', 4455 'id': id, 'result': result})
4456 4457 def return_error(id, code, message=None, data=None): 4458 error = {'code': code} 4459 if message is None: 4460 error['message'] = Service.jsonrpc_errors[code][0] 4461 else: 4462 error['message'] = message 4463 if data is None: 4464 error['data'] = Service.jsonrpc_errors[code][1] 4465 else: 4466 error['data'] = data 4467 return serializers.json({'jsonrpc': '2.0', 4468 'id': id, 4469 'error': error}) 4470 4471 def validate(data): 4472 """ 4473 Validate request as defined in: http://www.jsonrpc.org/specification#request_object. 4474 4475 :param data: The json object. 4476 :type name: str. 4477 4478 :returns: 4479 - True -- if successful 4480 - False -- if no error should be reported (i.e. data is missing 'id' member) 4481 4482 :raises: JsonRPCException 4483 4484 """ 4485 4486 iparms = set(data.keys()) 4487 mandatory_args = set(['jsonrpc', 'method']) 4488 missing_args = mandatory_args - iparms 4489 4490 if missing_args: 4491 raise Service.JsonRpcException(-32600, 'Missing arguments %s.' % list(missing_args)) 4492 if data['jsonrpc'] != '2.0': 4493 raise Service.JsonRpcException(-32603, 'Unsupported jsonrpc version "%s"' % data['jsonrpc']) 4494 if 'id' not in iparms: 4495 return False 4496 4497 return True 4498 4499 4500 4501 request = current.request 4502 response = current.response 4503 if not data: 4504 response.headers['Content-Type'] = 'application/json; charset=utf-8' 4505 try: 4506 data = json_parser.loads(request.body.read()) 4507 except ValueError: # decoding error in json lib 4508 return return_error(None, -32700) 4509 except json_parser.JSONDecodeError: # decoding error in simplejson lib 4510 return return_error(None, -32700) 4511 4512 # Batch handling 4513 if isinstance(data, list) and not batch_element: 4514 retlist = [] 4515 for c in data: 4516 retstr = self.serve_jsonrpc2(c, batch_element=True) 4517 if retstr: # do not add empty responses 4518 retlist.append(retstr) 4519 if len(retlist) == 0: # return nothing 4520 return '' 4521 else: 4522 return "[" + ','.join(retlist) + "]" 4523 methods = self.jsonrpc2_procedures 4524 methods.update(self.jsonrpc_procedures) 4525 4526 try: 4527 must_respond = validate(data) 4528 except Service.JsonRpcException, e: 4529 return return_error(None, e.code, e.info) 4530 4531 id, method, params = data.get('id'), data['method'], data.get('params', '') 4532 if not method in methods: 4533 return return_error(id, -32601, data='Method "%s" does not exist' % method) 4534 try: 4535 if isinstance(params,dict): 4536 s = methods[method](**params) 4537 else: 4538 s = methods[method](*params) 4539 if hasattr(s, 'as_list'): 4540 s = s.as_list() 4541 if must_respond: 4542 return return_response(id, s) 4543 else: 4544 return '' 4545 except HTTP, e: 4546 raise e 4547 except Service.JsonRpcException, e: 4548 return return_error(id, e.code, e.info) 4549 except BaseException: 4550 etype, eval, etb = sys.exc_info() 4551 code = -32099 4552 data = '%s: %s\n' % (etype.__name__, eval) + str(request.is_local and traceback.format_tb(etb)) 4553 return return_error(id, code, data=data) 4554 except: 4555 etype, eval, etb = sys.exc_info() 4556 return return_error(id, -32099, data='Exception %s: %s' % (etype, eval)) 4557 4558
4559 - def serve_xmlrpc(self):
4560 request = current.request 4561 response = current.response 4562 services = self.xmlrpc_procedures.values() 4563 return response.xmlrpc(request, services)
4564
4565 - def serve_amfrpc(self, version=0):
4566 try: 4567 import pyamf 4568 import pyamf.remoting.gateway 4569 except: 4570 return "pyamf not installed or not in Python sys.path" 4571 request = current.request 4572 response = current.response 4573 if version == 3: 4574 services = self.amfrpc3_procedures 4575 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 4576 pyamf_request = pyamf.remoting.decode(request.body) 4577 else: 4578 services = self.amfrpc_procedures 4579 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 4580 context = pyamf.get_context(pyamf.AMF0) 4581 pyamf_request = pyamf.remoting.decode(request.body, context) 4582 pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion) 4583 for name, message in pyamf_request: 4584 pyamf_response[name] = base_gateway.getProcessor(message)(message) 4585 response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE 4586 if version == 3: 4587 return pyamf.remoting.encode(pyamf_response).getvalue() 4588 else: 4589 return pyamf.remoting.encode(pyamf_response, context).getvalue()
4590
4591 - def serve_soap(self, version="1.1"):
4592 try: 4593 from contrib.pysimplesoap.server import SoapDispatcher 4594 except: 4595 return "pysimplesoap not installed in contrib" 4596 request = current.request 4597 response = current.response 4598 procedures = self.soap_procedures 4599 4600 location = "%s://%s%s" % ( 4601 request.env.wsgi_url_scheme, 4602 request.env.http_host, 4603 URL(r=request, f="call/soap", vars={})) 4604 namespace = 'namespace' in response and response.namespace or location 4605 documentation = response.description or '' 4606 dispatcher = SoapDispatcher( 4607 name=response.title, 4608 location=location, 4609 action=location, # SOAPAction 4610 namespace=namespace, 4611 prefix='pys', 4612 documentation=documentation, 4613 ns=True) 4614 for method, (function, returns, args, doc) in procedures.iteritems(): 4615 dispatcher.register_function(method, function, returns, args, doc) 4616 if request.env.request_method == 'POST': 4617 # Process normal Soap Operation 4618 response.headers['Content-Type'] = 'text/xml' 4619 return dispatcher.dispatch(request.body.read()) 4620 elif 'WSDL' in request.vars: 4621 # Return Web Service Description 4622 response.headers['Content-Type'] = 'text/xml' 4623 return dispatcher.wsdl() 4624 elif 'op' in request.vars: 4625 # Return method help webpage 4626 response.headers['Content-Type'] = 'text/html' 4627 method = request.vars['op'] 4628 sample_req_xml, sample_res_xml, doc = dispatcher.help(method) 4629 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 4630 A("See all webservice operations", 4631 _href=URL(r=request, f="call/soap", vars={})), 4632 H2(method), 4633 P(doc), 4634 UL(LI("Location: %s" % dispatcher.location), 4635 LI("Namespace: %s" % dispatcher.namespace), 4636 LI("SoapAction: %s" % dispatcher.action), 4637 ), 4638 H3("Sample SOAP XML Request Message:"), 4639 CODE(sample_req_xml, language="xml"), 4640 H3("Sample SOAP XML Response Message:"), 4641 CODE(sample_res_xml, language="xml"), 4642 ] 4643 return {'body': body} 4644 else: 4645 # Return general help and method list webpage 4646 response.headers['Content-Type'] = 'text/html' 4647 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 4648 P(response.description), 4649 P("The following operations are available"), 4650 A("See WSDL for webservice description", 4651 _href=URL(r=request, f="call/soap", vars={"WSDL":None})), 4652 UL([LI(A("%s: %s" % (method, doc or ''), 4653 _href=URL(r=request, f="call/soap", vars={'op': method}))) 4654 for method, doc in dispatcher.list_methods()]), 4655 ] 4656 return {'body': body}
4657
4658 - def __call__(self):
4659 """ 4660 register services with: 4661 service = Service() 4662 @service.run 4663 @service.rss 4664 @service.json 4665 @service.jsonrpc 4666 @service.xmlrpc 4667 @service.amfrpc 4668 @service.amfrpc3('domain') 4669 @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,}) 4670 4671 expose services with 4672 4673 def call(): return service() 4674 4675 call services with 4676 http://..../app/default/call/run?[parameters] 4677 http://..../app/default/call/rss?[parameters] 4678 http://..../app/default/call/json?[parameters] 4679 http://..../app/default/call/jsonrpc 4680 http://..../app/default/call/xmlrpc 4681 http://..../app/default/call/amfrpc 4682 http://..../app/default/call/amfrpc3 4683 http://..../app/default/call/soap 4684 """ 4685 4686 request = current.request 4687 if len(request.args) < 1: 4688 raise HTTP(404, "Not Found") 4689 arg0 = request.args(0) 4690 if arg0 == 'run': 4691 return self.serve_run(request.args[1:]) 4692 elif arg0 == 'rss': 4693 return self.serve_rss(request.args[1:]) 4694 elif arg0 == 'csv': 4695 return self.serve_csv(request.args[1:]) 4696 elif arg0 == 'xml': 4697 return self.serve_xml(request.args[1:]) 4698 elif arg0 == 'json': 4699 return self.serve_json(request.args[1:]) 4700 elif arg0 == 'jsonrpc': 4701 return self.serve_jsonrpc() 4702 elif arg0 == 'jsonrpc2': 4703 return self.serve_jsonrpc2() 4704 elif arg0 == 'xmlrpc': 4705 return self.serve_xmlrpc() 4706 elif arg0 == 'amfrpc': 4707 return self.serve_amfrpc() 4708 elif arg0 == 'amfrpc3': 4709 return self.serve_amfrpc(3) 4710 elif arg0 == 'soap': 4711 return self.serve_soap() 4712 else: 4713 self.error()
4714
4715 - def error(self):
4716 raise HTTP(404, "Object does not exist")
4717
4718 4719 -def completion(callback):
4720 """ 4721 Executes a task on completion of the called action. For example: 4722 4723 from gluon.tools import completion 4724 @completion(lambda d: logging.info(repr(d))) 4725 def index(): 4726 return dict(message='hello') 4727 4728 It logs the output of the function every time input is called. 4729 The argument of completion is executed in a new thread. 4730 """ 4731 def _completion(f): 4732 def __completion(*a, **b): 4733 d = None 4734 try: 4735 d = f(*a, **b) 4736 return d 4737 finally: 4738 thread.start_new_thread(callback, (d,))
4739 return __completion 4740 return _completion 4741
4742 4743 -def prettydate(d, T=lambda x: x):
4744 if isinstance(d, datetime.datetime): 4745 dt = datetime.datetime.now() - d 4746 elif isinstance(d, datetime.date): 4747 dt = datetime.date.today() - d 4748 elif not d: 4749 return '' 4750 else: 4751 return '[invalid date]' 4752 if dt.days < 0: 4753 suffix = ' from now' 4754 dt = -dt 4755 else: 4756 suffix = ' ago' 4757 if dt.days >= 2 * 365: 4758 return T('%d years' + suffix) % int(dt.days / 365) 4759 elif dt.days >= 365: 4760 return T('1 year' + suffix) 4761 elif dt.days >= 60: 4762 return T('%d months' + suffix) % int(dt.days / 30) 4763 elif dt.days > 21: 4764 return T('1 month' + suffix) 4765 elif dt.days >= 14: 4766 return T('%d weeks' + suffix) % int(dt.days / 7) 4767 elif dt.days >= 7: 4768 return T('1 week' + suffix) 4769 elif dt.days > 1: 4770 return T('%d days' + suffix) % dt.days 4771 elif dt.days == 1: 4772 return T('1 day' + suffix) 4773 elif dt.seconds >= 2 * 60 * 60: 4774 return T('%d hours' + suffix) % int(dt.seconds / 3600) 4775 elif dt.seconds >= 60 * 60: 4776 return T('1 hour' + suffix) 4777 elif dt.seconds >= 2 * 60: 4778 return T('%d minutes' + suffix) % int(dt.seconds / 60) 4779 elif dt.seconds >= 60: 4780 return T('1 minute' + suffix) 4781 elif dt.seconds > 1: 4782 return T('%d seconds' + suffix) % dt.seconds 4783 elif dt.seconds == 1: 4784 return T('1 second' + suffix) 4785 else: 4786 return T('now')
4787
4788 4789 -def test_thread_separation():
4790 def f(): 4791 c = PluginManager() 4792 lock1.acquire() 4793 lock2.acquire() 4794 c.x = 7 4795 lock1.release() 4796 lock2.release()
4797 lock1 = thread.allocate_lock() 4798 lock2 = thread.allocate_lock() 4799 lock1.acquire() 4800 thread.start_new_thread(f, ()) 4801 a = PluginManager() 4802 a.x = 5 4803 lock1.release() 4804 lock2.acquire() 4805 return a.x 4806
4807 4808 -class PluginManager(object):
4809 """ 4810 4811 Plugin Manager is similar to a storage object but it is a single level singleton 4812 this means that multiple instances within the same thread share the same attributes 4813 Its constructor is also special. The first argument is the name of the plugin you are defining. 4814 The named arguments are parameters needed by the plugin with default values. 4815 If the parameters were previous defined, the old values are used. 4816 4817 For example: 4818 4819 ### in some general configuration file: 4820 >>> plugins = PluginManager() 4821 >>> plugins.me.param1=3 4822 4823 ### within the plugin model 4824 >>> _ = PluginManager('me',param1=5,param2=6,param3=7) 4825 4826 ### where the plugin is used 4827 >>> print plugins.me.param1 4828 3 4829 >>> print plugins.me.param2 4830 6 4831 >>> plugins.me.param3 = 8 4832 >>> print plugins.me.param3 4833 8 4834 4835 Here are some tests: 4836 4837 >>> a=PluginManager() 4838 >>> a.x=6 4839 >>> b=PluginManager('check') 4840 >>> print b.x 4841 6 4842 >>> b=PluginManager() # reset settings 4843 >>> print b.x 4844 <Storage {}> 4845 >>> b.x=7 4846 >>> print a.x 4847 7 4848 >>> a.y.z=8 4849 >>> print b.y.z 4850 8 4851 >>> test_thread_separation() 4852 5 4853 >>> plugins=PluginManager('me',db='mydb') 4854 >>> print plugins.me.db 4855 mydb 4856 >>> print 'me' in plugins 4857 True 4858 >>> print plugins.me.installed 4859 True 4860 """ 4861 instances = {} 4862
4863 - def __new__(cls, *a, **b):
4864 id = thread.get_ident() 4865 lock = thread.allocate_lock() 4866 try: 4867 lock.acquire() 4868 try: 4869 return cls.instances[id] 4870 except KeyError: 4871 instance = object.__new__(cls, *a, **b) 4872 cls.instances[id] = instance 4873 return instance 4874 finally: 4875 lock.release()
4876
4877 - def __init__(self, plugin=None, **defaults):
4878 if not plugin: 4879 self.__dict__.clear() 4880 settings = self.__getattr__(plugin) 4881 settings.installed = True 4882 settings.update( 4883 (k, v) for k, v in defaults.items() if not k in settings)
4884
4885 - def __getattr__(self, key):
4886 if not key in self.__dict__: 4887 self.__dict__[key] = Storage() 4888 return self.__dict__[key]
4889
4890 - def keys(self):
4891 return self.__dict__.keys()
4892
4893 - def __contains__(self, key):
4894 return key in self.__dict__
4895
4896 4897 -class Expose(object):
4898 - def __init__(self, base=None, basename=None, extensions=None, allow_download=True):
4899 """ 4900 Usage: 4901 4902 def static(): 4903 return dict(files=Expose()) 4904 4905 or 4906 4907 def static(): 4908 path = os.path.join(request.folder,'static','public') 4909 return dict(files=Expose(path,basename='public')) 4910 4911 extensions: 4912 an optional list of file extensions for filtering displayed files: 4913 ['.py', '.jpg'] 4914 allow_download: whether to allow downloading selected files 4915 """ 4916 current.session.forget() 4917 base = base or os.path.join(current.request.folder, 'static') 4918 basename = basename or current.request.function 4919 self.basename = basename 4920 self.args = current.request.raw_args and \ 4921 [arg for arg in current.request.raw_args.split('/') if arg] or [] 4922 filename = os.path.join(base, *self.args) 4923 if not os.path.exists(filename): 4924 raise HTTP(404, "FILE NOT FOUND") 4925 if not os.path.normpath(filename).startswith(base): 4926 raise HTTP(401, "NOT AUTHORIZED") 4927 if allow_download and not os.path.isdir(filename): 4928 current.response.headers['Content-Type'] = contenttype(filename) 4929 raise HTTP(200, open(filename, 'rb'), **current.response.headers) 4930 self.path = path = os.path.join(filename, '*') 4931 self.folders = [f[len(path) - 1:] for f in sorted(glob.glob(path)) 4932 if os.path.isdir(f) and not self.isprivate(f)] 4933 self.filenames = [f[len(path) - 1:] for f in sorted(glob.glob(path)) 4934 if not os.path.isdir(f) and not self.isprivate(f)] 4935 if 'README' in self.filenames: 4936 readme = open(os.path.join(filename,'README')).read() 4937 self.paragraph = MARKMIN(readme) 4938 else: 4939 self.paragraph = None 4940 if extensions: 4941 self.filenames = [f for f in self.filenames 4942 if os.path.splitext(f)[-1] in extensions]
4943
4944 - def breadcrumbs(self, basename):
4945 path = [] 4946 span = SPAN() 4947 span.append(A(basename, _href=URL())) 4948 for arg in self.args: 4949 span.append('/') 4950 path.append(arg) 4951 span.append(A(arg, _href=URL(args='/'.join(path)))) 4952 return span
4953
4954 - def table_folders(self):
4955 if self.folders: 4956 return SPAN(H3('Folders'), TABLE( 4957 *[TR(TD(A(folder, _href=URL(args=self.args + [folder])))) 4958 for folder in self.folders], 4959 **dict(_class="table"))) 4960 return ''
4961 4962 @staticmethod
4963 - def isprivate(f):
4964 return 'private' in f or f.startswith('.') or f.endswith('~')
4965 4966 @staticmethod
4967 - def isimage(f):
4968 return os.path.splitext(f)[-1].lower() in ( 4969 '.png', '.jpg', '.jpeg', '.gif', '.tiff')
4970
4971 - def table_files(self, width=160):
4972 if self.filenames: 4973 return SPAN(H3('Files'), 4974 TABLE(*[TR(TD(A(f, _href=URL(args=self.args + [f]))), 4975 TD(IMG(_src=URL(args=self.args + [f]), 4976 _style='max-width:%spx' % width) 4977 if width and self.isimage(f) else '')) 4978 for f in self.filenames], 4979 **dict(_class="table"))) 4980 return ''
4981
4982 - def xml(self):
4983 return DIV( 4984 H2(self.breadcrumbs(self.basename)), 4985 self.paragraph or '', 4986 self.table_folders(), 4987 self.table_files()).xml()
4988
4989 4990 -class Wiki(object):
4991 everybody = 'everybody' 4992 rows_page = 25
4993 - def markmin_base(self,body):
4994 return MARKMIN(body, extra=self.settings.extra, 4995 url=True, environment=self.env, 4996 autolinks=lambda link: expand_one(link, {})).xml()
4997
4998 - def render_tags(self, tags):
4999 return DIV( 5000 _class='w2p_wiki_tags', 5001 *[A(t.strip(), _href=URL(args='_search', vars=dict(q=t))) 5002 for t in tags or [] if t.strip()])
5003
5004 - def markmin_render(self, page):
5005 return self.markmin_base(page.body) + self.render_tags(page.tags).xml()
5006
5007 - def html_render(self, page):
5008 html = page.body 5009 # @///function -> http://..../function 5010 html = replace_at_urls(html, URL) 5011 # http://...jpg -> <img src="http://...jpg/> or embed 5012 html = replace_autolinks(html, lambda link: expand_one(link, {})) 5013 # @{component:name} -> <script>embed component name</script> 5014 html = replace_components(html, self.env) 5015 html = html + self.render_tags(page.tags).xml() 5016 return html
5017 5018 @staticmethod
5019 - def component(text):
5020 """ 5021 In wiki docs allows @{component:controller/function/args} 5022 which renders as a LOAD(..., ajax=True) 5023 """ 5024 items = text.split('/') 5025 controller, function, args = items[0], items[1], items[2:] 5026 return LOAD(controller, function, args=args, ajax=True).xml()
5027
5028 - def get_render(self):
5029 if isinstance(self.settings.render, basestring): 5030 r = getattr(self, "%s_render" % self.settings.render) 5031 elif callable(self.settings.render): 5032 r = self.settings.render 5033 else: 5034 raise ValueError("Invalid render type %s" % type(render)) 5035 return r
5036
5037 - def __init__(self, auth, env=None, render='markmin', 5038 manage_permissions=False, force_prefix='', 5039 restrict_search=False, extra=None, 5040 menu_groups=None, templates=None, migrate=True, 5041 controller=None, function=None):
5042 5043 settings = self.settings = auth.settings.wiki 5044 5045 # render: "markmin", "html", ..., <function> 5046 settings.render = render 5047 perms = settings.manage_permissions = manage_permissions 5048 5049 settings.force_prefix = force_prefix 5050 settings.restrict_search = restrict_search 5051 settings.extra = extra or {} 5052 settings.menu_groups = menu_groups 5053 settings.templates = templates 5054 settings.controller = controller 5055 settings.function = function 5056 5057 db = auth.db 5058 self.env = env or {} 5059 self.env['component'] = Wiki.component 5060 self.auth = auth 5061 self.wiki_menu_items = None 5062 5063 if self.auth.user: 5064 self.settings.force_prefix = force_prefix % self.auth.user 5065 else: 5066 self.settings.force_prefix = force_prefix 5067 5068 self.host = current.request.env.http_host 5069 5070 table_definitions = [ 5071 ('wiki_page', { 5072 'args':[ 5073 Field('slug', 5074 requires=[IS_SLUG(), 5075 IS_NOT_IN_DB(db, 'wiki_page.slug')], 5076 writable=False), 5077 Field('title', unique=True), 5078 Field('body', 'text', notnull=True), 5079 Field('tags', 'list:string'), 5080 Field('can_read', 'list:string', 5081 writable=perms, 5082 readable=perms, 5083 default=[Wiki.everybody]), 5084 Field('can_edit', 'list:string', 5085 writable=perms, readable=perms, 5086 default=[Wiki.everybody]), 5087 Field('changelog'), 5088 Field('html', 'text', 5089 compute=self.get_render(), 5090 readable=False, writable=False), 5091 auth.signature], 5092 'vars':{'format':'%(title)s', 'migrate':migrate}}), 5093 ('wiki_tag', { 5094 'args':[ 5095 Field('name'), 5096 Field('wiki_page', 'reference wiki_page'), 5097 auth.signature], 5098 'vars':{'format':'%(title)s', 'migrate':migrate}}), 5099 ('wiki_media', { 5100 'args':[ 5101 Field('wiki_page', 'reference wiki_page'), 5102 Field('title', required=True), 5103 Field('filename', 'upload', required=True), 5104 auth.signature], 5105 'vars':{'format':'%(title)s', 'migrate':migrate}}), 5106 ] 5107 5108 # define only non-existent tables 5109 for key, value in table_definitions: 5110 args = [] 5111 if not key in db.tables(): 5112 # look for wiki_ extra fields in auth.settings 5113 extra_fields = auth.settings.extra_fields 5114 if extra_fields: 5115 if key in extra_fields: 5116 if extra_fields[key]: 5117 for field in extra_fields[key]: 5118 args.append(field) 5119 args += value['args'] 5120 db.define_table(key, *args, **value['vars']) 5121 5122 if self.settings.templates is None and not \ 5123 self.settings.manage_permissions: 5124 self.settings.templates = db.wiki_page.tags.contains('template')&\ 5125 db.wiki_page.can_read.contains('everybody') 5126 5127 def update_tags_insert(page, id, db=db): 5128 for tag in page.tags or []: 5129 tag = tag.strip().lower() 5130 if tag: 5131 db.wiki_tag.insert(name=tag, wiki_page=id)
5132 5133 def update_tags_update(dbset, page, db=db): 5134 page = dbset.select().first() 5135 db(db.wiki_tag.wiki_page == page.id).delete() 5136 for tag in page.tags or []: 5137 tag = tag.strip().lower() 5138 if tag: 5139 db.wiki_tag.insert(name=tag, wiki_page=page.id)
5140 db.wiki_page._after_insert.append(update_tags_insert) 5141 db.wiki_page._after_update.append(update_tags_update) 5142 5143 if (auth.user and 5144 check_credentials(current.request, gae_login=False) and 5145 not 'wiki_editor' in auth.user_groups.values()): 5146 group = db.auth_group(role='wiki_editor') 5147 gid = group.id if group else db.auth_group.insert( 5148 role='wiki_editor') 5149 auth.add_membership(gid) 5150 5151 settings.lock_keys = True 5152 5153 # WIKI ACCESS POLICY 5154
5155 - def not_authorized(self, page=None):
5156 raise HTTP(401)
5157
5158 - def can_read(self, page):
5159 if 'everybody' in page.can_read or not \ 5160 self.settings.manage_permissions: 5161 return True 5162 elif self.auth.user: 5163 groups = self.auth.user_groups.values() 5164 if ('wiki_editor' in groups or 5165 set(groups).intersection(set(page.can_read + page.can_edit)) or 5166 page.created_by == self.auth.user.id): 5167 return True 5168 return False
5169
5170 - def can_edit(self, page=None):
5171 if not self.auth.user: 5172 redirect(self.auth.settings.login_url) 5173 groups = self.auth.user_groups.values() 5174 return ('wiki_editor' in groups or 5175 (page is None and 'wiki_author' in groups) or 5176 not page is None and ( 5177 set(groups).intersection(set(page.can_edit)) or 5178 page.created_by == self.auth.user.id))
5179
5180 - def can_manage(self):
5181 if not self.auth.user: 5182 return False 5183 groups = self.auth.user_groups.values() 5184 return 'wiki_editor' in groups
5185
5186 - def can_search(self):
5187 return True
5188
5189 - def can_see_menu(self):
5190 if self.settings.menu_groups is None: 5191 return True 5192 if self.auth.user: 5193 groups = self.auth.user_groups.values() 5194 if any(t in self.settings.menu_groups for t in groups): 5195 return True 5196 return False
5197 5198 ### END POLICY 5199
5200 - def automenu(self):
5201 """adds the menu if not present""" 5202 request = current.request 5203 if not self.wiki_menu_items and self.settings.controller and self.settings.function: 5204 self.wiki_menu_items = self.menu(self.settings.controller, 5205 self.settings.function) 5206 current.response.menu += self.wiki_menu_items
5207
5208 - def __call__(self):
5209 request = current.request 5210 settings = self.settings 5211 settings.controller = settings.controller or request.controller 5212 settings.function = settings.function or request.function 5213 self.automenu() 5214 5215 zero = request.args(0) or 'index' 5216 if zero and zero.isdigit(): 5217 return self.media(int(zero)) 5218 elif not zero or not zero.startswith('_'): 5219 return self.read(zero) 5220 elif zero == '_edit': 5221 return self.edit(request.args(1) or 'index',request.args(2) or 0) 5222 elif zero == '_editmedia': 5223 return self.editmedia(request.args(1) or 'index') 5224 elif zero == '_create': 5225 return self.create() 5226 elif zero == '_pages': 5227 return self.pages() 5228 elif zero == '_search': 5229 return self.search() 5230 elif zero == '_recent': 5231 ipage = int(request.vars.page or 0) 5232 query = self.auth.db.wiki_page.created_by == request.args( 5233 1, cast=int) 5234 return self.search(query=query, 5235 orderby=~self.auth.db.wiki_page.created_on, 5236 limitby=(ipage * self.rows_page, 5237 (ipage + 1) * self.rows_page), 5238 ) 5239 elif zero == '_cloud': 5240 return self.cloud() 5241 elif zero == '_preview': 5242 return self.preview(self.get_render())
5243
5244 - def first_paragraph(self, page):
5245 if not self.can_read(page): 5246 mm = (page.body or '').replace('\r', '') 5247 ps = [p for p in mm.split('\n\n') 5248 if not p.startswith('#') and p.strip()] 5249 if ps: 5250 return ps[0] 5251 return ''
5252
5253 - def fix_hostname(self, body):
5254 return (body or '').replace('://HOSTNAME', '://%s' % self.host)
5255
5256 - def read(self, slug):
5257 if slug in '_cloud': 5258 return self.cloud() 5259 elif slug in '_search': 5260 return self.search() 5261 page = self.auth.db.wiki_page(slug=slug) 5262 if not page: 5263 redirect(URL(args=('_create', slug))) 5264 if not self.can_read(page): 5265 return self.not_authorized(page) 5266 if current.request.extension == 'html': 5267 if not page: 5268 url = URL(args=('_edit', slug)) 5269 return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn")) 5270 else: 5271 return dict(title=page.title, 5272 slug=page.slug, 5273 page=page, 5274 content=XML(self.fix_hostname(page.html)), 5275 tags=page.tags, 5276 created_on=page.created_on, 5277 modified_on=page.modified_on) 5278 elif current.request.extension == 'load': 5279 return self.fix_hostname(page.html) if page else '' 5280 else: 5281 if not page: 5282 raise HTTP(404) 5283 else: 5284 return dict(title=page.title, 5285 slug=page.slug, 5286 page=page, 5287 content=page.body, 5288 tags=page.tags, 5289 created_on=page.created_on, 5290 modified_on=page.modified_on)
5291
5292 - def check_editor(self, role='wiki_editor', act=False):
5293 if not self.auth.user: 5294 if not act: 5295 return False 5296 redirect(self.auth.settings.login_url) 5297 elif not self.auth.has_membership(role): 5298 if not act: 5299 return False 5300 raise HTTP(401, "Not Authorized") 5301 return True
5302
5303 - def edit(self,slug,from_template=0):
5304 auth = self.auth 5305 db = auth.db 5306 page = db.wiki_page(slug=slug) 5307 if not self.can_edit(page): 5308 return self.not_authorized(page) 5309 title_guess = ' '.join(c.capitalize() for c in slug.split('-')) 5310 if not page: 5311 if not (self.can_manage() or 5312 slug.startswith(self.settings.force_prefix)): 5313 current.session.flash = 'slug must have "%s" prefix' \ 5314 % self.settings.force_prefix 5315 redirect(URL(args=('_create'))) 5316 db.wiki_page.can_read.default = [Wiki.everybody] 5317 db.wiki_page.can_edit.default = [auth.user_group_role()] 5318 db.wiki_page.title.default = title_guess 5319 db.wiki_page.slug.default = slug 5320 if slug == 'wiki-menu': 5321 db.wiki_page.body.default = \ 5322 '- Menu Item > @////index\n- - Submenu > http://web2py.com' 5323 else: 5324 db.wiki_page.body.default = db(db.wiki_page.id==from_template).select(db.wiki_page.body)[0].body if int(from_template) > 0 else '## %s\n\npage content' % title_guess 5325 vars = current.request.post_vars 5326 if vars.body: 5327 vars.body = vars.body.replace('://%s' % self.host, '://HOSTNAME') 5328 form = SQLFORM(db.wiki_page, page, deletable=True, 5329 formstyle='table2cols', showid=False).process() 5330 if form.deleted: 5331 current.session.flash = 'page deleted' 5332 redirect(URL()) 5333 elif form.accepted: 5334 current.session.flash = 'page created' 5335 redirect(URL(args=slug)) 5336 script = """ 5337 jQuery(function() { 5338 if (!jQuery('#wiki_page_body').length) return; 5339 var pagecontent = jQuery('#wiki_page_body'); 5340 pagecontent.css('font-family', 5341 'Monaco,Menlo,Consolas,"Courier New",monospace'); 5342 var prevbutton = jQuery('<button class="btn nopreview">Preview</button>'); 5343 var preview = jQuery('<div id="preview"></div>').hide(); 5344 var previewmedia = jQuery('<div id="previewmedia"></div>'); 5345 var form = pagecontent.closest('form'); 5346 preview.insertBefore(form); 5347 prevbutton.insertBefore(form); 5348 if(%(link_media)s) { 5349 var mediabutton = jQuery('<button class="btn nopreview">Media</button>'); 5350 mediabutton.insertBefore(form); 5351 previewmedia.insertBefore(form); 5352 mediabutton.toggle(function() { 5353 web2py_component('%(urlmedia)s', 'previewmedia'); 5354 }, function() { 5355 previewmedia.empty(); 5356 }); 5357 } 5358 prevbutton.click(function(e) { 5359 e.preventDefault(); 5360 if (prevbutton.hasClass('nopreview')) { 5361 prevbutton.addClass('preview').removeClass( 5362 'nopreview').html('Edit Source'); 5363 web2py_ajax_page('post', '%(url)s', {body : jQuery('#wiki_page_body').val()}, 'preview'); 5364 form.fadeOut('fast', function() {preview.fadeIn()}); 5365 } else { 5366 prevbutton.addClass( 5367 'nopreview').removeClass('preview').html('Preview'); 5368 preview.fadeOut('fast', function() {form.fadeIn()}); 5369 } 5370 }) 5371 }) 5372 """ % dict(url=URL(args=('_preview', slug)),link_media=('true' if page else 'false'), 5373 urlmedia=URL(extension='load', 5374 args=('_editmedia',slug), 5375 vars=dict(embedded=1))) 5376 return dict(content=TAG[''](form, SCRIPT(script)))
5377
5378 - def editmedia(self, slug):
5379 auth = self.auth 5380 db = auth.db 5381 page = db.wiki_page(slug=slug) 5382 if not (page and self.can_edit(page)): 5383 return self.not_authorized(page) 5384 self.auth.db.wiki_media.id.represent = lambda id, row: \ 5385 id if not row.filename else \ 5386 SPAN('@////%i/%s.%s' % 5387 (id, IS_SLUG.urlify(row.title.split('.')[0]), 5388 row.filename.split('.')[-1])) 5389 self.auth.db.wiki_media.wiki_page.default = page.id 5390 self.auth.db.wiki_media.wiki_page.writable = False 5391 links = [] 5392 csv = True 5393 create = True 5394 if current.request.vars.embedded: 5395 script = "var c = jQuery('#wiki_page_body'); c.val(c.val() + jQuery('%s').text()); return false;" 5396 fragment = self.auth.db.wiki_media.id.represent 5397 csv = False 5398 create = False 5399 links=[ 5400 lambda row: 5401 A('copy into source', _href='#', _onclick=script % (fragment(row.id, row))) 5402 ] 5403 content = SQLFORM.grid( 5404 self.auth.db.wiki_media.wiki_page == page.id, 5405 orderby=self.auth.db.wiki_media.title, 5406 links = links, 5407 csv = csv, 5408 create = create, 5409 args=['_editmedia', slug], 5410 user_signature=False) 5411 return dict(content=content)
5412
5413 - def create(self):
5414 if not self.can_edit(): 5415 return self.not_authorized() 5416 db = self.auth.db 5417 slugs=db(db.wiki_page.id>0).select(db.wiki_page.id,db.wiki_page.slug) 5418 options=[OPTION(row.slug,_value=row.id) for row in slugs] 5419 options.insert(0, OPTION('',_value='')) 5420 fields = [Field("slug", default=current.request.args(1) or 5421 self.settings.force_prefix, 5422 requires=(IS_SLUG(), IS_NOT_IN_DB(db,db.wiki_page.slug))),] 5423 if self.settings.templates: 5424 fields.append( 5425 Field("from_template", "reference wiki_page", 5426 requires=IS_EMPTY_OR( 5427 IS_IN_DB(db(self.settings.templates), 5428 db.wiki_page._id, 5429 '%(slug)s')), 5430 comment=current.T( 5431 "Choose Template or empty for new Page"))) 5432 form = SQLFORM.factory(*fields, **dict(_class="well")) 5433 form.element("[type=submit]").attributes["_value"] = \ 5434 current.T("Create Page from Slug") 5435 5436 if form.process().accepted: 5437 form.vars.from_template = 0 if not form.vars.from_template \ 5438 else form.vars.from_template 5439 redirect(URL(args=('_edit', form.vars.slug,form.vars.from_template or 0))) # added param 5440 return dict(content=form)
5441
5442 - def pages(self):
5443 if not self.can_manage(): 5444 return self.not_authorized() 5445 self.auth.db.wiki_page.slug.represent = lambda slug, row: SPAN( 5446 '@////%s' % slug) 5447 self.auth.db.wiki_page.title.represent = lambda title, row: \ 5448 A(title, _href=URL(args=row.slug)) 5449 wiki_table = self.auth.db.wiki_page 5450 content = SQLFORM.grid( 5451 wiki_table, 5452 fields = [wiki_table.slug, 5453 wiki_table.title, wiki_table.tags, 5454 wiki_table.can_read, wiki_table.can_edit], 5455 links=[ 5456 lambda row: 5457 A('edit', _href=URL(args=('_edit', row.slug)),_class='btn'), 5458 lambda row: 5459 A('media', _href=URL(args=('_editmedia', row.slug)),_class='btn')], 5460 details=False, editable=False, deletable=False, create=False, 5461 orderby=self.auth.db.wiki_page.title, 5462 args=['_pages'], 5463 user_signature=False) 5464 5465 return dict(content=content)
5466
5467 - def media(self, id):
5468 request, db = current.request, self.auth.db 5469 media = db.wiki_media(id) 5470 if media: 5471 if self.settings.manage_permissions: 5472 page = db.wiki_page(media.wiki_page) 5473 if not self.can_read(page): 5474 return self.not_authorized(page) 5475 request.args = [media.filename] 5476 return current.response.download(request, db) 5477 else: 5478 raise HTTP(404)
5479
5480 - def menu(self, controller='default', function='index'):
5481 db = self.auth.db 5482 request = current.request 5483 menu_page = db.wiki_page(slug='wiki-menu') 5484 menu = [] 5485 if menu_page: 5486 tree = {'': menu} 5487 regex = re.compile('[\r\n\t]*(?P<base>(\s*\-\s*)+)(?P<title>\w.*?)\s+\>\s+(?P<link>\S+)') 5488 for match in regex.finditer(self.fix_hostname(menu_page.body)): 5489 base = match.group('base').replace(' ', '') 5490 title = match.group('title') 5491 link = match.group('link') 5492 title_page = None 5493 if link.startswith('@'): 5494 items = link[2:].split('/') 5495 if len(items) > 3: 5496 title_page = items[3] 5497 link = URL(a=items[0] or None, c=items[1] or controller, 5498 f=items[2] or function, args=items[3:]) 5499 parent = tree.get(base[1:], tree['']) 5500 subtree = [] 5501 tree[base] = subtree 5502 parent.append((current.T(title), 5503 request.args(0) == title_page, 5504 link, subtree)) 5505 if self.can_see_menu(): 5506 submenu = [] 5507 menu.append((current.T('[Wiki]'), None, None, submenu)) 5508 if URL() == URL(controller, function): 5509 if not str(request.args(0)).startswith('_'): 5510 slug = request.args(0) or 'index' 5511 mode = 1 5512 elif request.args(0) == '_edit': 5513 slug = request.args(1) or 'index' 5514 mode = 2 5515 elif request.args(0) == '_editmedia': 5516 slug = request.args(1) or 'index' 5517 mode = 3 5518 else: 5519 mode = 0 5520 if mode in (2, 3): 5521 submenu.append((current.T('View Page'), None, 5522 URL(controller, function, args=slug))) 5523 if mode in (1, 3): 5524 submenu.append((current.T('Edit Page'), None, 5525 URL(controller, function, args=('_edit', slug)))) 5526 if mode in (1, 2): 5527 submenu.append((current.T('Edit Page Media'), None, 5528 URL(controller, function, args=('_editmedia', slug)))) 5529 5530 submenu.append((current.T('Create New Page'), None, 5531 URL(controller, function, args=('_create')))) 5532 # Moved next if to inside self.auth.user check 5533 if self.can_manage(): 5534 submenu.append((current.T('Manage Pages'), None, 5535 URL(controller, function, args=('_pages')))) 5536 submenu.append((current.T('Edit Menu'), None, 5537 URL(controller, function, args=('_edit', 'wiki-menu')))) 5538 # Also moved inside self.auth.user check 5539 submenu.append((current.T('Search Pages'), None, 5540 URL(controller, function, args=('_search')))) 5541 return menu
5542
5543 - def search(self, tags=None, query=None, cloud=True, preview=True, 5544 limitby=(0, 100), orderby=None):
5545 if not self.can_search(): 5546 return self.not_authorized() 5547 request = current.request 5548 content = CAT() 5549 if tags is None and query is None: 5550 form = FORM(INPUT(_name='q', requires=IS_NOT_EMPTY(), 5551 value=request.vars.q), 5552 INPUT(_type="submit", _value=current.T('Search')), 5553 _method='GET') 5554 content.append(DIV(form, _class='w2p_wiki_form')) 5555 if request.vars.q: 5556 tags = [v.strip() for v in request.vars.q.split(',')] 5557 tags = [v.lower() for v in tags if v] 5558 if tags or not query is None: 5559 db = self.auth.db 5560 count = db.wiki_tag.wiki_page.count() 5561 fields = [db.wiki_page.id, db.wiki_page.slug, 5562 db.wiki_page.title, db.wiki_page.tags, 5563 db.wiki_page.can_read] 5564 if preview: 5565 fields.append(db.wiki_page.body) 5566 if query is None: 5567 query = (db.wiki_page.id == db.wiki_tag.wiki_page) &\ 5568 (db.wiki_tag.name.belongs(tags)) 5569 query = query | db.wiki_page.title.contains(request.vars.q) 5570 if self.settings.restrict_search and not self.manage(): 5571 query = query & (db.wiki_page.created_by == self.auth.user_id) 5572 pages = db(query).select(count, 5573 *fields, **dict(orderby=orderby or ~count, 5574 groupby=reduce(lambda a, b: a | b, fields), 5575 distinct=True, 5576 limitby=limitby)) 5577 if request.extension in ('html', 'load'): 5578 if not pages: 5579 content.append(DIV(current.T("No results"), 5580 _class='w2p_wiki_form')) 5581 5582 def link(t): 5583 return A(t, _href=URL(args='_search', vars=dict(q=t)))
5584 items = [DIV(H3(A(p.wiki_page.title, _href=URL( 5585 args=p.wiki_page.slug))), 5586 MARKMIN(self.first_paragraph(p.wiki_page)) 5587 if preview else '', 5588 DIV(_class='w2p_wiki_tags', 5589 *[link(t.strip()) for t in 5590 p.wiki_page.tags or [] if t.strip()]), 5591 _class='w2p_wiki_search_item') 5592 for p in pages] 5593 content.append(DIV(_class='w2p_wiki_pages', *items)) 5594 else: 5595 cloud = False 5596 content = [p.wiki_page.as_dict() for p in pages] 5597 elif cloud: 5598 content.append(self.cloud()['content']) 5599 if request.extension == 'load': 5600 return content 5601 return dict(content=content) 5602
5603 - def cloud(self):
5604 db = self.auth.db 5605 count = db.wiki_tag.wiki_page.count(distinct=True) 5606 ids = db(db.wiki_tag).select( 5607 db.wiki_tag.name, count, 5608 distinct=True, 5609 groupby=db.wiki_tag.name, 5610 orderby=~count, limitby=(0, 20)) 5611 if ids: 5612 a, b = ids[0](count), ids[-1](count) 5613 5614 def style(c): 5615 STYLE = 'padding:0 0.2em;line-height:%.2fem;font-size:%.2fem' 5616 size = (1.5 * (c - b) / max(a - b, 1) + 1.3) 5617 return STYLE % (1.3, size)
5618 items = [] 5619 for item in ids: 5620 items.append(A(item.wiki_tag.name, 5621 _style=style(item(count)), 5622 _href=URL(args='_search', 5623 vars=dict(q=item.wiki_tag.name)))) 5624 items.append(' ') 5625 return dict(content=DIV(_class='w2p_cloud', *items)) 5626
5627 - def preview(self, render):
5628 request = current.request 5629 return render(request.post_vars)
5630 5631 5632 if __name__ == '__main__': 5633 import doctest 5634 doctest.testmod() 5635