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

Source Code for Module web2py.gluon.tools

   1  #!/bin/python 
   2  # -*- coding: utf-8 -*- 
   3   
   4  """ 
   5  This file is part of the web2py Web Framework 
   6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
   7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
   8  """ 
   9   
  10  from contenttype import contenttype 
  11  from storage import Storage, StorageList, Settings, Messages 
  12  from validators import IS_NOT_IN_DB, IS_NOT_EMPTY, IS_IN_DB, IS_EMAIL, IS_EXPR, IS_IN_SET, IS_INT_IN_RANGE, CRYPT 
  13  from html import DIV, URL, A, BR, SPAN, XML, UL, LI, H1, H2, H3, P, SCRIPT, TAG, IFRAME, LABEL, CODE 
  14  from html import FORM, INPUT, OPTION, SELECT 
  15  from html import TABLE, TR, TD 
  16  from sqlhtml import SQLFORM, SQLTABLE 
  17  from http import HTTP, redirect 
  18  from utils import web2py_uuid 
  19   
  20  import base64 
  21  import cPickle 
  22  import datetime 
  23  import thread 
  24  import logging 
  25  import sys 
  26  import os 
  27  import re 
  28  import time 
  29  import smtplib 
  30  import urllib 
  31  import urllib2 
  32  import Cookie 
  33  import cStringIO 
  34   
  35  from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string 
  36   
  37  import serializers 
  38  import contrib.simplejson as simplejson 
  39  from dal import Field 
  40   
  41  __all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'PluginManager', 'fetch', 'geocode', 'prettydate'] 
  42   
  43  logger = logging.getLogger("web2py") 
  44   
  45  DEFAULT = lambda: None 
  46   
47 -def callback(actions,form,tablename=None):
48 if actions: 49 if tablename and isinstance(actions,dict): 50 actions = actions.get(tablename, []) 51 if not isinstance(actions,(list, tuple)): 52 actions = [actions] 53 [action(form) for action in actions]
54
55 -def validators(*a):
56 b = [] 57 for item in a: 58 if isinstance(item, (list, tuple)): 59 b = b + list(item) 60 else: 61 b.append(item) 62 return b
63
64 -def call_or_redirect(f,*args):
65 if callable(f): 66 redirect(f(*args)) 67 else: 68 redirect(f)
69
70 -class Mail(object):
71 """ 72 Class for configuring and sending emails with alternative text / html 73 body, multiple attachments and encryption support 74 75 Works with SMTP and Google App Engine. 76 """ 77
78 - class Attachment(MIMEBase.MIMEBase):
79 """ 80 Email attachment 81 82 Arguments:: 83 84 payload: path to file or file-like object with read() method 85 filename: name of the attachment stored in message; if set to 86 None, it will be fetched from payload path; file-like 87 object payload must have explicit filename specified 88 content_id: id of the attachment; automatically contained within 89 < and > 90 content_type: content type of the attachment; if set to None, 91 it will be fetched from filename using gluon.contenttype 92 module 93 encoding: encoding of all strings passed to this function (except 94 attachment body) 95 96 Content ID is used to identify attachments within the html body; 97 in example, attached image with content ID 'photo' may be used in 98 html message as a source of img tag <img src="cid:photo" />. 99 100 Examples:: 101 102 #Create attachment from text file: 103 attachment = Mail.Attachment('/path/to/file.txt') 104 105 Content-Type: text/plain 106 MIME-Version: 1.0 107 Content-Disposition: attachment; filename="file.txt" 108 Content-Transfer-Encoding: base64 109 110 SOMEBASE64CONTENT= 111 112 #Create attachment from image file with custom filename and cid: 113 attachment = Mail.Attachment('/path/to/file.png', 114 filename='photo.png', 115 content_id='photo') 116 117 Content-Type: image/png 118 MIME-Version: 1.0 119 Content-Disposition: attachment; filename="photo.png" 120 Content-Id: <photo> 121 Content-Transfer-Encoding: base64 122 123 SOMEOTHERBASE64CONTENT= 124 """ 125
126 - def __init__( 127 self, 128 payload, 129 filename=None, 130 content_id=None, 131 content_type=None, 132 encoding='utf-8'):
133 if isinstance(payload, str): 134 if filename == None: 135 filename = os.path.basename(payload) 136 handle = open(payload, 'rb') 137 payload = handle.read() 138 handle.close() 139 else: 140 if filename == None: 141 raise Exception('Missing attachment name') 142 payload = payload.read() 143 filename = filename.encode(encoding) 144 if content_type == None: 145 content_type = contenttype(filename) 146 self.my_filename = filename 147 self.my_payload = payload 148 MIMEBase.MIMEBase.__init__(self, *content_type.split('/', 1)) 149 self.set_payload(payload) 150 self['Content-Disposition'] = 'attachment; filename="%s"' % filename 151 if content_id != None: 152 self['Content-Id'] = '<%s>' % content_id.encode(encoding) 153 Encoders.encode_base64(self)
154
155 - def __init__(self, server=None, sender=None, login=None, tls=True):
156 """ 157 Main Mail object 158 159 Arguments:: 160 161 server: SMTP server address in address:port notation 162 sender: sender email address 163 login: sender login name and password in login:password notation 164 or None if no authentication is required 165 tls: enables/disables encryption (True by default) 166 167 In Google App Engine use:: 168 169 server='gae' 170 171 For sake of backward compatibility all fields are optional and default 172 to None, however, to be able to send emails at least server and sender 173 must be specified. They are available under following fields: 174 175 mail.settings.server 176 mail.settings.sender 177 mail.settings.login 178 179 When server is 'logging', email is logged but not sent (debug mode) 180 181 Optionally you can use PGP encryption or X509: 182 183 mail.settings.cipher_type = None 184 mail.settings.sign = True 185 mail.settings.sign_passphrase = None 186 mail.settings.encrypt = True 187 mail.settings.x509_sign_keyfile = None 188 mail.settings.x509_sign_certfile = None 189 mail.settings.x509_crypt_certfiles = None 190 191 cipher_type : None 192 gpg - need a python-pyme package and gpgme lib 193 x509 - smime 194 sign : sign the message (True or False) 195 sign_passphrase : passphrase for key signing 196 encrypt : encrypt the message 197 ... x509 only ... 198 x509_sign_keyfile : the signers private key filename (PEM format) 199 x509_sign_certfile: the signers certificate filename (PEM format) 200 x509_crypt_certfiles: the certificates file to encrypt the messages 201 with can be a file name or a list of 202 file names (PEM format) 203 204 Examples:: 205 206 #Create Mail object with authentication data for remote server: 207 mail = Mail('example.com:25', 'me@example.com', 'me:password') 208 """ 209 210 self.settings = Settings() 211 self.settings.server = server 212 self.settings.sender = sender 213 self.settings.login = login 214 self.settings.tls = tls 215 self.settings.cipher_type = None 216 self.settings.sign = True 217 self.settings.sign_passphrase = None 218 self.settings.encrypt = True 219 self.settings.x509_sign_keyfile = None 220 self.settings.x509_sign_certfile = None 221 self.settings.x509_crypt_certfiles = None 222 self.settings.debug = False 223 self.settings.lock_keys = True 224 self.result = {} 225 self.error = None
226
227 - def send( 228 self, 229 to, 230 subject='None', 231 message='None', 232 attachments=None, 233 cc=None, 234 bcc=None, 235 reply_to=None, 236 encoding='utf-8', 237 ):
238 """ 239 Sends an email using data specified in constructor 240 241 Arguments:: 242 243 to: list or tuple of receiver addresses; will also accept single 244 object 245 subject: subject of the email 246 message: email body text; depends on type of passed object: 247 if 2-list or 2-tuple is passed: first element will be 248 source of plain text while second of html text; 249 otherwise: object will be the only source of plain text 250 and html source will be set to None; 251 If text or html source is: 252 None: content part will be ignored, 253 string: content part will be set to it, 254 file-like object: content part will be fetched from 255 it using it's read() method 256 attachments: list or tuple of Mail.Attachment objects; will also 257 accept single object 258 cc: list or tuple of carbon copy receiver addresses; will also 259 accept single object 260 bcc: list or tuple of blind carbon copy receiver addresses; will 261 also accept single object 262 reply_to: address to which reply should be composed 263 encoding: encoding of all strings passed to this method (including 264 message bodies) 265 266 Examples:: 267 268 #Send plain text message to single address: 269 mail.send('you@example.com', 270 'Message subject', 271 'Plain text body of the message') 272 273 #Send html message to single address: 274 mail.send('you@example.com', 275 'Message subject', 276 '<html>Plain text body of the message</html>') 277 278 #Send text and html message to three addresses (two in cc): 279 mail.send('you@example.com', 280 'Message subject', 281 ('Plain text body', '<html>html body</html>'), 282 cc=['other1@example.com', 'other2@example.com']) 283 284 #Send html only message with image attachment available from 285 the message by 'photo' content id: 286 mail.send('you@example.com', 287 'Message subject', 288 (None, '<html><img src="cid:photo" /></html>'), 289 Mail.Attachment('/path/to/photo.jpg' 290 content_id='photo')) 291 292 #Send email with two attachments and no body text 293 mail.send('you@example.com, 294 'Message subject', 295 None, 296 [Mail.Attachment('/path/to/fist.file'), 297 Mail.Attachment('/path/to/second.file')]) 298 299 Returns True on success, False on failure. 300 301 Before return, method updates two object's fields: 302 self.result: return value of smtplib.SMTP.sendmail() or GAE's 303 mail.send_mail() method 304 self.error: Exception message or None if above was successful 305 """ 306 307 def encode_header(key): 308 if [c for c in key if 32>ord(c) or ord(c)>127]: 309 return Header.Header(key.encode('utf-8'),'utf-8') 310 else: 311 return key
312 313 if not isinstance(self.settings.server, str): 314 raise Exception('Server address not specified') 315 if not isinstance(self.settings.sender, str): 316 raise Exception('Sender address not specified') 317 payload_in = MIMEMultipart.MIMEMultipart('mixed') 318 if to: 319 if not isinstance(to, (list,tuple)): 320 to = [to] 321 else: 322 raise Exception('Target receiver address not specified') 323 if cc: 324 if not isinstance(cc, (list, tuple)): 325 cc = [cc] 326 if bcc: 327 if not isinstance(bcc, (list, tuple)): 328 bcc = [bcc] 329 if message == None: 330 text = html = None 331 elif isinstance(message, (list, tuple)): 332 text, html = message 333 elif message.strip().startswith('<html') and message.strip().endswith('</html>'): 334 text = self.settings.server=='gae' and message or None 335 html = message 336 else: 337 text = message 338 html = None 339 if text != None or html != None: 340 attachment = MIMEMultipart.MIMEMultipart('alternative') 341 if text != None: 342 if isinstance(text, basestring): 343 text = text.decode(encoding).encode('utf-8') 344 else: 345 text = text.read().decode(encoding).encode('utf-8') 346 attachment.attach(MIMEText.MIMEText(text,_charset='utf-8')) 347 if html != None: 348 if isinstance(html, basestring): 349 html = html.decode(encoding).encode('utf-8') 350 else: 351 html = html.read().decode(encoding).encode('utf-8') 352 attachment.attach(MIMEText.MIMEText(html, 'html',_charset='utf-8')) 353 payload_in.attach(attachment) 354 if attachments == None: 355 pass 356 elif isinstance(attachments, (list, tuple)): 357 for attachment in attachments: 358 payload_in.attach(attachment) 359 else: 360 payload_in.attach(attachments) 361 362 363 ####################################################### 364 # CIPHER # 365 ####################################################### 366 cipher_type = self.settings.cipher_type 367 sign = self.settings.sign 368 sign_passphrase = self.settings.sign_passphrase 369 encrypt = self.settings.encrypt 370 ####################################################### 371 # GPGME # 372 ####################################################### 373 if cipher_type == 'gpg': 374 if not sign and not encrypt: 375 self.error="No sign and no encrypt is set but cipher type to gpg" 376 return False 377 378 # need a python-pyme package and gpgme lib 379 from pyme import core, errors 380 from pyme.constants.sig import mode 381 ############################################ 382 # sign # 383 ############################################ 384 if sign: 385 import string 386 core.check_version(None) 387 pin=string.replace(payload_in.as_string(),'\n','\r\n') 388 plain = core.Data(pin) 389 sig = core.Data() 390 c = core.Context() 391 c.set_armor(1) 392 c.signers_clear() 393 # search for signing key for From: 394 for sigkey in c.op_keylist_all(self.settings.sender, 1): 395 if sigkey.can_sign: 396 c.signers_add(sigkey) 397 if not c.signers_enum(0): 398 self.error='No key for signing [%s]' % self.settings.sender 399 return False 400 c.set_passphrase_cb(lambda x,y,z: sign_passphrase) 401 try: 402 # make a signature 403 c.op_sign(plain,sig,mode.DETACH) 404 sig.seek(0,0) 405 # make it part of the email 406 payload=MIMEMultipart.MIMEMultipart('signed', 407 boundary=None, 408 _subparts=None, 409 **dict(micalg="pgp-sha1", 410 protocol="application/pgp-signature")) 411 # insert the origin payload 412 payload.attach(payload_in) 413 # insert the detached signature 414 p=MIMEBase.MIMEBase("application",'pgp-signature') 415 p.set_payload(sig.read()) 416 payload.attach(p) 417 # it's just a trick to handle the no encryption case 418 payload_in=payload 419 except errors.GPGMEError, ex: 420 self.error="GPG error: %s" % ex.getstring() 421 return False 422 ############################################ 423 # encrypt # 424 ############################################ 425 if encrypt: 426 core.check_version(None) 427 plain = core.Data(payload_in.as_string()) 428 cipher = core.Data() 429 c = core.Context() 430 c.set_armor(1) 431 # collect the public keys for encryption 432 recipients=[] 433 rec=to 434 if cc: 435 rec.extend(cc) 436 if bcc: 437 rec.extend(bcc) 438 for addr in rec: 439 c.op_keylist_start(addr,0) 440 r = c.op_keylist_next() 441 if r == None: 442 self.error='No key for [%s]' % addr 443 return False 444 recipients.append(r) 445 try: 446 # make the encryption 447 c.op_encrypt(recipients, 1, plain, cipher) 448 cipher.seek(0,0) 449 # make it a part of the email 450 payload=MIMEMultipart.MIMEMultipart('encrypted', 451 boundary=None, 452 _subparts=None, 453 **dict(protocol="application/pgp-encrypted")) 454 p=MIMEBase.MIMEBase("application",'pgp-encrypted') 455 p.set_payload("Version: 1\r\n") 456 payload.attach(p) 457 p=MIMEBase.MIMEBase("application",'octet-stream') 458 p.set_payload(cipher.read()) 459 payload.attach(p) 460 except errors.GPGMEError, ex: 461 self.error="GPG error: %s" % ex.getstring() 462 return False 463 ####################################################### 464 # X.509 # 465 ####################################################### 466 elif cipher_type == 'x509': 467 if not sign and not encrypt: 468 self.error="No sign and no encrypt is set but cipher type to x509" 469 return False 470 x509_sign_keyfile=self.settings.x509_sign_keyfile 471 if self.settings.x509_sign_certfile: 472 x509_sign_certfile=self.settings.x509_sign_certfile 473 else: 474 # if there is no sign certfile we'll assume the 475 # cert is in keyfile 476 x509_sign_certfile=self.settings.x509_sign_keyfile 477 # crypt certfiles could be a string or a list 478 x509_crypt_certfiles=self.settings.x509_crypt_certfiles 479 480 481 # need m2crypto 482 from M2Crypto import BIO, SMIME, X509 483 msg_bio = BIO.MemoryBuffer(payload_in.as_string()) 484 s = SMIME.SMIME() 485 486 # SIGN 487 if sign: 488 #key for signing 489 try: 490 s.load_key(x509_sign_keyfile, x509_sign_certfile, callback=lambda x: sign_passphrase) 491 if encrypt: 492 p7 = s.sign(msg_bio) 493 else: 494 p7 = s.sign(msg_bio,flags=SMIME.PKCS7_DETACHED) 495 msg_bio = BIO.MemoryBuffer(payload_in.as_string()) # Recreate coz sign() has consumed it. 496 except Exception,e: 497 self.error="Something went wrong on signing: <%s>" %str(e) 498 return False 499 500 # ENCRYPT 501 if encrypt: 502 try: 503 sk = X509.X509_Stack() 504 if not isinstance(x509_crypt_certfiles, (list, tuple)): 505 x509_crypt_certfiles = [x509_crypt_certfiles] 506 507 # make an encryption cert's stack 508 for x in x509_crypt_certfiles: 509 sk.push(X509.load_cert(x)) 510 s.set_x509_stack(sk) 511 512 s.set_cipher(SMIME.Cipher('des_ede3_cbc')) 513 tmp_bio = BIO.MemoryBuffer() 514 if sign: 515 s.write(tmp_bio, p7) 516 else: 517 tmp_bio.write(payload_in.as_string()) 518 p7 = s.encrypt(tmp_bio) 519 except Exception,e: 520 self.error="Something went wrong on encrypting: <%s>" %str(e) 521 return False 522 523 # Final stage in sign and encryption 524 out = BIO.MemoryBuffer() 525 if encrypt: 526 s.write(out, p7) 527 else: 528 if sign: 529 s.write(out, p7, msg_bio, SMIME.PKCS7_DETACHED) 530 else: 531 out.write('\r\n') 532 out.write(payload_in.as_string()) 533 out.close() 534 st=str(out.read()) 535 payload=message_from_string(st) 536 else: 537 # no cryptography process as usual 538 payload=payload_in 539 payload['From'] = encode_header(self.settings.sender.decode(encoding)) 540 if to: 541 payload['To'] = encode_header(', '.join(to).decode(encoding)) 542 if reply_to: 543 payload['Reply-To'] = encode_header(reply_to.decode(encoding)) 544 if cc: 545 payload['Cc'] = encode_header(', '.join(cc).decode(encoding)) 546 to.extend(cc) 547 if bcc: 548 to.extend(bcc) 549 payload['Subject'] = encode_header(subject.decode(encoding)) 550 payload['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S +0000", 551 time.gmtime()) 552 result = {} 553 try: 554 if self.settings.server == 'logging': 555 logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\n\n%s\n%s\n' % \ 556 ('-'*40,self.settings.sender, 557 ', '.join(to),text or html,'-'*40)) 558 elif self.settings.server == 'gae': 559 from google.appengine.api import mail 560 attachments = attachments and [(a.my_filename,a.my_payload) for a in attachments] 561 if attachments: 562 result = mail.send_mail(sender=self.settings.sender, to=to, 563 subject=subject, body=text, html=html, 564 attachments=attachments) 565 elif html: 566 result = mail.send_mail(sender=self.settings.sender, to=to, 567 subject=subject, body=text, html=html) 568 else: 569 result = mail.send_mail(sender=self.settings.sender, to=to, 570 subject=subject, body=text) 571 else: 572 server = smtplib.SMTP(*self.settings.server.split(':')) 573 if self.settings.login != None: 574 if self.settings.tls: 575 server.ehlo() 576 server.starttls() 577 server.ehlo() 578 server.login(*self.settings.login.split(':',1)) 579 result = server.sendmail(self.settings.sender, to, payload.as_string()) 580 server.quit() 581 except Exception, e: 582 logger.warn('Mail.send failure:%s' % e) 583 self.result = result 584 self.error = e 585 return False 586 self.result = result 587 self.error = None 588 return True
589 590
591 -class Recaptcha(DIV):
592 593 API_SSL_SERVER = 'https://api-secure.recaptcha.net' 594 API_SERVER = 'http://api.recaptcha.net' 595 VERIFY_SERVER = 'api-verify.recaptcha.net' 596
597 - def __init__( 598 self, 599 request, 600 public_key='', 601 private_key='', 602 use_ssl=False, 603 error=None, 604 error_message='invalid', 605 label = 'Verify:', 606 options = '' 607 ):
608 self.remote_addr = request.env.remote_addr 609 self.public_key = public_key 610 self.private_key = private_key 611 self.use_ssl = use_ssl 612 self.error = error 613 self.errors = Storage() 614 self.error_message = error_message 615 self.components = [] 616 self.attributes = {} 617 self.label = label 618 self.options = options 619 self.comment = ''
620
621 - def _validate(self):
622 623 # for local testing: 624 625 recaptcha_challenge_field = \ 626 self.request_vars.recaptcha_challenge_field 627 recaptcha_response_field = \ 628 self.request_vars.recaptcha_response_field 629 private_key = self.private_key 630 remoteip = self.remote_addr 631 if not (recaptcha_response_field and recaptcha_challenge_field 632 and len(recaptcha_response_field) 633 and len(recaptcha_challenge_field)): 634 self.errors['captcha'] = self.error_message 635 return False 636 params = urllib.urlencode({ 637 'privatekey': private_key, 638 'remoteip': remoteip, 639 'challenge': recaptcha_challenge_field, 640 'response': recaptcha_response_field, 641 }) 642 request = urllib2.Request( 643 url='http://%s/verify' % self.VERIFY_SERVER, 644 data=params, 645 headers={'Content-type': 'application/x-www-form-urlencoded', 646 'User-agent': 'reCAPTCHA Python'}) 647 httpresp = urllib2.urlopen(request) 648 return_values = httpresp.read().splitlines() 649 httpresp.close() 650 return_code = return_values[0] 651 if return_code == 'true': 652 del self.request_vars.recaptcha_challenge_field 653 del self.request_vars.recaptcha_response_field 654 self.request_vars.captcha = '' 655 return True 656 self.errors['captcha'] = self.error_message 657 return False
658
659 - def xml(self):
660 public_key = self.public_key 661 use_ssl = (self.use_ssl, ) 662 error_param = '' 663 if self.error: 664 error_param = '&error=%s' % self.error 665 if use_ssl: 666 server = self.API_SSL_SERVER 667 else: 668 server = self.API_SERVER 669 captcha = DIV( 670 SCRIPT("var RecaptchaOptions = {%s};" % self.options), 671 SCRIPT(_type="text/javascript", 672 _src="%s/challenge?k=%s%s" % (server,public_key,error_param)), 673 TAG.noscript(IFRAME(_src="%s/noscript?k=%s%s" % (server,public_key,error_param), 674 _height="300",_width="500",_frameborder="0"), BR(), 675 INPUT(_type='hidden', _name='recaptcha_response_field', 676 _value='manual_challenge')), _id='recaptcha') 677 if not self.errors.captcha: 678 return XML(captcha).xml() 679 else: 680 captcha.append(DIV(self.errors['captcha'], _class='error')) 681 return XML(captcha).xml()
682 683
684 -def addrow(form,a,b,c,style,_id,position=-1):
685 if style == "divs": 686 form[0].insert(position, DIV(DIV(LABEL(a),_class='w2p_fl'), 687 DIV(b, _class='w2p_fw'), 688 DIV(c, _class='w2p_fc'), 689 _id = _id)) 690 elif style == "table2cols": 691 form[0].insert(position, TR(LABEL(a),'')) 692 form[0].insert(position+1, TR(b, _colspan=2, _id = _id)) 693 elif style == "ul": 694 form[0].insert(position, LI(DIV(LABEL(a),_class='w2p_fl'), 695 DIV(b, _class='w2p_fw'), 696 DIV(c, _class='w2p_fc'), 697 _id = _id)) 698 else: 699 form[0].insert(position, TR(LABEL(a),b,c,_id = _id))
700 701
702 -class Auth(object):
703 """ 704 Class for authentication, authorization, role based access control. 705 706 Includes: 707 708 - registration and profile 709 - login and logout 710 - username and password retrieval 711 - event logging 712 - role creation and assignment 713 - user defined group/role based permission 714 715 Authentication Example:: 716 717 from contrib.utils import * 718 mail=Mail() 719 mail.settings.server='smtp.gmail.com:587' 720 mail.settings.sender='you@somewhere.com' 721 mail.settings.login='username:password' 722 auth=Auth(globals(), db) 723 auth.settings.mailer=mail 724 # auth.settings....=... 725 auth.define_tables() 726 def authentication(): 727 return dict(form=auth()) 728 729 exposes: 730 731 - http://.../{application}/{controller}/authentication/login 732 - http://.../{application}/{controller}/authentication/logout 733 - http://.../{application}/{controller}/authentication/register 734 - http://.../{application}/{controller}/authentication/verify_email 735 - http://.../{application}/{controller}/authentication/retrieve_username 736 - http://.../{application}/{controller}/authentication/retrieve_password 737 - http://.../{application}/{controller}/authentication/reset_password 738 - http://.../{application}/{controller}/authentication/profile 739 - http://.../{application}/{controller}/authentication/change_password 740 741 On registration a group with role=new_user.id is created 742 and user is given membership of this group. 743 744 You can create a group with:: 745 746 group_id=auth.add_group('Manager', 'can access the manage action') 747 auth.add_permission(group_id, 'access to manage') 748 749 Here \"access to manage\" is just a user defined string. 750 You can give access to a user:: 751 752 auth.add_membership(group_id, user_id) 753 754 If user id is omitted, the logged in user is assumed 755 756 Then you can decorate any action:: 757 758 @auth.requires_permission('access to manage') 759 def manage(): 760 return dict() 761 762 You can restrict a permission to a specific table:: 763 764 auth.add_permission(group_id, 'edit', db.sometable) 765 @auth.requires_permission('edit', db.sometable) 766 767 Or to a specific record:: 768 769 auth.add_permission(group_id, 'edit', db.sometable, 45) 770 @auth.requires_permission('edit', db.sometable, 45) 771 772 If authorization is not granted calls:: 773 774 auth.settings.on_failed_authorization 775 776 Other options:: 777 778 auth.settings.mailer=None 779 auth.settings.expiration=3600 # seconds 780 781 ... 782 783 ### these are messages that can be customized 784 ... 785 """ 786 787
788 - def url(self, f=None, args=[], vars={}):
789 return self.environment.URL(r=self.environment.request, 790 c=self.settings.controller, 791 f=f, args=args, vars=vars)
792
793 - def __init__(self, environment, db=None, controller='default'):
794 """ 795 auth=Auth(globals(), db) 796 797 - globals() has to be the web2py environment including 798 request, response, session 799 - db has to be the database where to create tables for authentication 800 801 """ 802 803 self.environment = Storage(environment) 804 self.db = db 805 request = self.environment.request 806 session = self.environment.session 807 auth = session.auth 808 if auth and auth.last_visit and auth.last_visit + \ 809 datetime.timedelta(days=0, seconds=auth.expiration) > request.now: 810 self.user = auth.user 811 # this is a trick to speed up sessions 812 if (request.now - auth.last_visit).seconds > (auth.expiration/10): 813 auth.last_visit = request.now 814 else: 815 self.user = None 816 session.auth = None 817 self.settings = Settings() 818 819 # ## what happens after login? 820 821 # ## what happens after registration? 822 823 self.settings.hideerror = False 824 self.settings.actions_disabled = [] 825 self.settings.reset_password_requires_verification = False 826 self.settings.registration_requires_verification = False 827 self.settings.registration_requires_approval = False 828 self.settings.alternate_requires_registration = False 829 self.settings.create_user_groups = True 830 831 self.settings.controller = controller 832 self.settings.login_url = self.url('user', args='login') 833 self.settings.logged_url = self.url('user', args='profile') 834 self.settings.download_url = self.url('download') 835 self.settings.mailer = None 836 self.settings.login_captcha = None 837 self.settings.register_captcha = None 838 self.settings.retrieve_username_captcha = None 839 self.settings.retrieve_password_captcha = None 840 self.settings.captcha = None 841 self.settings.expiration = 3600 # one hour 842 self.settings.long_expiration = 3600*30*24 # one month 843 self.settings.remember_me_form = True 844 self.settings.allow_basic_login = False 845 self.settings.allow_basic_login_only = False 846 self.settings.on_failed_authorization = \ 847 self.url('user',args='not_authorized') 848 849 self.settings.on_failed_authentication = lambda x: redirect(x) 850 851 self.settings.formstyle = 'table3cols' 852 853 # ## table names to be used 854 855 self.settings.password_field = 'password' 856 self.settings.table_user_name = 'auth_user' 857 self.settings.table_group_name = 'auth_group' 858 self.settings.table_membership_name = 'auth_membership' 859 self.settings.table_permission_name = 'auth_permission' 860 self.settings.table_event_name = 'auth_event' 861 862 # ## if none, they will be created 863 864 self.settings.table_user = None 865 self.settings.table_group = None 866 self.settings.table_membership = None 867 self.settings.table_permission = None 868 self.settings.table_event = None 869 870 # ## 871 872 self.settings.showid = False 873 874 # ## these should be functions or lambdas 875 876 self.settings.login_next = self.url('index') 877 self.settings.login_onvalidation = [] 878 self.settings.login_onaccept = [] 879 self.settings.login_methods = [self] 880 self.settings.login_form = self 881 self.settings.login_email_validate = True 882 self.settings.login_userfield = None 883 884 self.settings.logout_next = self.url('index') 885 self.settings.logout_onlogout = None 886 887 self.settings.register_next = self.url('index') 888 self.settings.register_onvalidation = [] 889 self.settings.register_onaccept = [] 890 self.settings.register_fields = None 891 892 self.settings.verify_email_next = self.url('user', args='login') 893 self.settings.verify_email_onaccept = [] 894 895 self.settings.profile_next = self.url('index') 896 self.settings.profile_onvalidation = [] 897 self.settings.profile_onaccept = [] 898 self.settings.profile_fields = None 899 self.settings.retrieve_username_next = self.url('index') 900 self.settings.retrieve_password_next = self.url('index') 901 self.settings.request_reset_password_next = self.url('user', args='login') 902 self.settings.reset_password_next = self.url('user', args='login') 903 904 self.settings.change_password_next = self.url('index') 905 self.settings.change_password_onvalidation = [] 906 self.settings.change_password_onaccept = [] 907 908 self.settings.retrieve_password_onvalidation = [] 909 self.settings.reset_password_onvalidation = [] 910 911 self.settings.hmac_key = None 912 self.settings.lock_keys = True 913 914 915 # ## these are messages that can be customized 916 self.messages = Messages(self.environment.T) 917 self.messages.submit_button = 'Submit' 918 self.messages.verify_password = 'Verify Password' 919 self.messages.delete_label = 'Check to delete:' 920 self.messages.function_disabled = 'Function disabled' 921 self.messages.access_denied = 'Insufficient privileges' 922 self.messages.registration_verifying = 'Registration needs verification' 923 self.messages.registration_pending = 'Registration is pending approval' 924 self.messages.login_disabled = 'Login disabled by administrator' 925 self.messages.logged_in = 'Logged in' 926 self.messages.email_sent = 'Email sent' 927 self.messages.unable_to_send_email = 'Unable to send email' 928 self.messages.email_verified = 'Email verified' 929 self.messages.logged_out = 'Logged out' 930 self.messages.registration_successful = 'Registration successful' 931 self.messages.invalid_email = 'Invalid email' 932 self.messages.unable_send_email = 'Unable to send email' 933 self.messages.invalid_login = 'Invalid login' 934 self.messages.invalid_user = 'Invalid user' 935 self.messages.invalid_password = 'Invalid password' 936 self.messages.is_empty = "Cannot be empty" 937 self.messages.mismatched_password = "Password fields don't match" 938 self.messages.verify_email = \ 939 'Click on the link http://...verify_email/%(key)s to verify your email' 940 self.messages.verify_email_subject = 'Email verification' 941 self.messages.username_sent = 'Your username was emailed to you' 942 self.messages.new_password_sent = 'A new password was emailed to you' 943 self.messages.password_changed = 'Password changed' 944 self.messages.retrieve_username = 'Your username is: %(username)s' 945 self.messages.retrieve_username_subject = 'Username retrieve' 946 self.messages.retrieve_password = 'Your password is: %(password)s' 947 self.messages.retrieve_password_subject = 'Password retrieve' 948 self.messages.reset_password = \ 949 'Click on the link http://...reset_password/%(key)s to reset your password' 950 self.messages.reset_password_subject = 'Password reset' 951 self.messages.invalid_reset_password = 'Invalid reset password' 952 self.messages.profile_updated = 'Profile updated' 953 self.messages.new_password = 'New password' 954 self.messages.old_password = 'Old password' 955 self.messages.group_description = \ 956 'Group uniquely assigned to user %(id)s' 957 958 self.messages.register_log = 'User %(id)s Registered' 959 self.messages.login_log = 'User %(id)s Logged-in' 960 self.messages.login_failed_log = None 961 self.messages.logout_log = 'User %(id)s Logged-out' 962 self.messages.profile_log = 'User %(id)s Profile updated' 963 self.messages.verify_email_log = 'User %(id)s Verification email sent' 964 self.messages.retrieve_username_log = 'User %(id)s Username retrieved' 965 self.messages.retrieve_password_log = 'User %(id)s Password retrieved' 966 self.messages.reset_password_log = 'User %(id)s Password reset' 967 self.messages.change_password_log = 'User %(id)s Password changed' 968 self.messages.add_group_log = 'Group %(group_id)s created' 969 self.messages.del_group_log = 'Group %(group_id)s deleted' 970 self.messages.add_membership_log = None 971 self.messages.del_membership_log = None 972 self.messages.has_membership_log = None 973 self.messages.add_permission_log = None 974 self.messages.del_permission_log = None 975 self.messages.has_permission_log = None 976 self.messages.impersonate_log = 'User %(id)s is impersonating %(other_id)s' 977 978 self.messages.label_first_name = 'First name' 979 self.messages.label_last_name = 'Last name' 980 self.messages.label_username = 'Username' 981 self.messages.label_email = 'E-mail' 982 self.messages.label_password = 'Password' 983 self.messages.label_registration_key = 'Registration key' 984 self.messages.label_reset_password_key = 'Reset Password key' 985 self.messages.label_registration_id = 'Registration identifier' 986 self.messages.label_role = 'Role' 987 self.messages.label_description = 'Description' 988 self.messages.label_user_id = 'User ID' 989 self.messages.label_group_id = 'Group ID' 990 self.messages.label_name = 'Name' 991 self.messages.label_table_name = 'Table name' 992 self.messages.label_record_id = 'Record ID' 993 self.messages.label_time_stamp = 'Timestamp' 994 self.messages.label_client_ip = 'Client IP' 995 self.messages.label_origin = 'Origin' 996 self.messages.label_remember_me = "Remember me (for 30 days)" 997 self.messages['T'] = self.environment.T 998 self.messages.verify_password_comment = 'please input your password again' 999 self.messages.lock_keys = True 1000 1001 # for "remember me" option 1002 response = self.environment.response 1003 if auth and auth.remember: #when user wants to be logged in for longer 1004 #import time 1005 #t = time.strftime( 1006 # "%a, %d-%b-%Y %H:%M:%S %Z", 1007 # time.gmtime(time.time() + auth.expiration) # one month longer 1008 #) 1009 # sets for appropriate cookie an appropriate expiration time 1010 response.cookies[response.session_id_name]["expires"] = auth.expiration
1011
1012 - def _get_user_id(self):
1013 "accessor for auth.user_id" 1014 return self.user and self.user.id or None
1015 user_id = property(_get_user_id, doc="user.id or None") 1016
1017 - def _HTTP(self, *a, **b):
1018 """ 1019 only used in lambda: self._HTTP(404) 1020 """ 1021 1022 raise HTTP(*a, **b)
1023
1024 - def __call__(self):
1025 """ 1026 usage: 1027 1028 def authentication(): return dict(form=auth()) 1029 """ 1030 1031 request = self.environment.request 1032 args = request.args 1033 if not args: 1034 redirect(self.url(args='login',vars=request.vars)) 1035 elif args[0] in self.settings.actions_disabled: 1036 raise HTTP(404) 1037 if args[0] == 'login': 1038 return self.login() 1039 elif args[0] == 'logout': 1040 return self.logout() 1041 elif args[0] == 'register': 1042 return self.register() 1043 elif args[0] == 'verify_email': 1044 return self.verify_email() 1045 elif args[0] == 'retrieve_username': 1046 return self.retrieve_username() 1047 elif args[0] == 'retrieve_password': 1048 return self.retrieve_password() 1049 elif args[0] == 'reset_password': 1050 return self.reset_password() 1051 elif args[0] == 'request_reset_password': 1052 return self.request_reset_password() 1053 elif args[0] == 'change_password': 1054 return self.change_password() 1055 elif args[0] == 'profile': 1056 return self.profile() 1057 elif args[0] == 'groups': 1058 return self.groups() 1059 elif args[0] == 'impersonate': 1060 return self.impersonate() 1061 elif args[0] == 'not_authorized': 1062 return self.not_authorized() 1063 else: 1064 raise HTTP(404)
1065
1066 - def navbar(self,prefix='Welcome',action=None):
1067 request = self.environment.request 1068 T = self.environment.T 1069 if isinstance(prefix,str): 1070 prefix = T(prefix) 1071 if not action: 1072 action=URL(request.application,request.controller,'user') 1073 if prefix: 1074 prefix = prefix.strip()+' ' 1075 if self.user_id: 1076 logout=A(T('logout'),_href=action+'/logout') 1077 profile=A(T('profile'),_href=action+'/profile') 1078 password=A(T('password'),_href=action+'/change_password') 1079 bar = SPAN(prefix,self.user.first_name,' [ ', logout, ']',_class='auth_navbar') 1080 if not 'profile' in self.settings.actions_disabled: 1081 bar.insert(4, ' | ') 1082 bar.insert(5, profile) 1083 if not 'change_password' in self.settings.actions_disabled: 1084 bar.insert(-1, ' | ') 1085 bar.insert(-1, password) 1086 else: 1087 login=A(T('login'),_href=action+'/login') 1088 register=A(T('register'),_href=action+'/register') 1089 retrieve_username=A(T('forgot username?'), 1090 _href=action+'/retrieve_username') 1091 lost_password=A(T('lost password?'), 1092 _href=action+'/request_reset_password') 1093 bar = SPAN('[ ',login,' ]',_class='auth_navbar') 1094 1095 if not 'register' in self.settings.actions_disabled: 1096 bar.insert(2, ' | ') 1097 bar.insert(3, register) 1098 if 'username' in self.settings.table_user.fields() and \ 1099 not 'retrieve_username' in self.settings.actions_disabled: 1100 bar.insert(-1, ' | ') 1101 bar.insert(-1, retrieve_username) 1102 if not 'request_reset_password' in self.settings.actions_disabled: 1103 bar.insert(-1, ' | ') 1104 bar.insert(-1, lost_password) 1105 return bar
1106
1107 - def __get_migrate(self, tablename, migrate=True):
1108 1109 if type(migrate).__name__ == 'str': 1110 return (migrate + tablename + '.table') 1111 elif migrate == False: 1112 return False 1113 else: 1114 return True
1115
1116 - def define_tables(self, username=False, migrate=True, fake_migrate=False):
1117 """ 1118 to be called unless tables are defined manually 1119 1120 usages:: 1121 1122 # defines all needed tables and table files 1123 # 'myprefix_auth_user.table', ... 1124 auth.define_tables(migrate='myprefix_') 1125 1126 # defines all needed tables without migration/table files 1127 auth.define_tables(migrate=False) 1128 1129 """ 1130 1131 db = self.db 1132 if not self.settings.table_user_name in db.tables: 1133 passfield = self.settings.password_field 1134 if username: 1135 table = db.define_table( 1136 self.settings.table_user_name, 1137 Field('first_name', length=128, default='', 1138 label=self.messages.label_first_name), 1139 Field('last_name', length=128, default='', 1140 label=self.messages.label_last_name), 1141 Field('username', length=128, default='', 1142 label=self.messages.label_username), 1143 Field('email', length=512, default='', 1144 label=self.messages.label_email), 1145 Field(passfield, 'password', length=512, 1146 readable=False, label=self.messages.label_password), 1147 Field('registration_key', length=512, 1148 writable=False, readable=False, default='', 1149 label=self.messages.label_registration_key), 1150 Field('reset_password_key', length=512, 1151 writable=False, readable=False, default='', 1152 label=self.messages.label_reset_password_key), 1153 Field('registration_id', length=512, 1154 writable=False, readable=False, default='', 1155 label=self.messages.label_registration_id), 1156 migrate=\ 1157 self.__get_migrate(self.settings.table_user_name, migrate), 1158 fake_migrate=fake_migrate, 1159 format='%(username)s') 1160 table.username.requires = IS_NOT_IN_DB(db, table.username) 1161 else: 1162 table = db.define_table( 1163 self.settings.table_user_name, 1164 Field('first_name', length=128, default='', 1165 label=self.messages.label_first_name), 1166 Field('last_name', length=128, default='', 1167 label=self.messages.label_last_name), 1168 Field('email', length=512, default='', 1169 label=self.messages.label_email), 1170 Field(passfield, 'password', length=512, 1171 readable=False, label=self.messages.label_password), 1172 Field('registration_key', length=512, 1173 writable=False, readable=False, default='', 1174 label=self.messages.label_registration_key), 1175 Field('reset_password_key', length=512, 1176 writable=False, readable=False, default='', 1177 label=self.messages.label_reset_password_key), 1178 migrate=\ 1179 self.__get_migrate(self.settings.table_user_name, migrate), 1180 fake_migrate=fake_migrate, 1181 format='%(first_name)s %(last_name)s (%(id)s)') 1182 table.first_name.requires = \ 1183 IS_NOT_EMPTY(error_message=self.messages.is_empty) 1184 table.last_name.requires = \ 1185 IS_NOT_EMPTY(error_message=self.messages.is_empty) 1186 table[passfield].requires = [CRYPT(key=self.settings.hmac_key)] 1187 table.email.requires = \ 1188 [IS_EMAIL(error_message=self.messages.invalid_email), 1189 IS_NOT_IN_DB(db, table.email)] 1190 table.registration_key.default = '' 1191 self.settings.table_user = db[self.settings.table_user_name] 1192 if not self.settings.table_group_name in db.tables: 1193 table = db.define_table( 1194 self.settings.table_group_name, 1195 Field('role', length=512, default='', 1196 label=self.messages.label_role), 1197 Field('description', 'text', 1198 label=self.messages.label_description), 1199 migrate=self.__get_migrate( 1200 self.settings.table_group_name, migrate), 1201 fake_migrate=fake_migrate, 1202 format = '%(role)s (%(id)s)') 1203 table.role.requires = IS_NOT_IN_DB(db, '%s.role' 1204 % self.settings.table_group_name) 1205 self.settings.table_group = db[self.settings.table_group_name] 1206 if not self.settings.table_membership_name in db.tables: 1207 table = db.define_table( 1208 self.settings.table_membership_name, 1209 Field('user_id', self.settings.table_user, 1210 label=self.messages.label_user_id), 1211 Field('group_id', self.settings.table_group, 1212 label=self.messages.label_group_id), 1213 migrate=self.__get_migrate( 1214 self.settings.table_membership_name, migrate), 1215 fake_migrate=fake_migrate) 1216 table.user_id.requires = IS_IN_DB(db, '%s.id' % 1217 self.settings.table_user_name, 1218 '%(first_name)s %(last_name)s (%(id)s)') 1219 table.group_id.requires = IS_IN_DB(db, '%s.id' % 1220 self.settings.table_group_name, 1221 '%(role)s (%(id)s)') 1222 self.settings.table_membership = db[self.settings.table_membership_name] 1223 if not self.settings.table_permission_name in db.tables: 1224 table = db.define_table( 1225 self.settings.table_permission_name, 1226 Field('group_id', self.settings.table_group, 1227 label=self.messages.label_group_id), 1228 Field('name', default='default', length=512, 1229 label=self.messages.label_name), 1230 Field('table_name', length=512, 1231 label=self.messages.label_table_name), 1232 Field('record_id', 'integer', 1233 label=self.messages.label_record_id), 1234 migrate=self.__get_migrate( 1235 self.settings.table_permission_name, migrate), 1236 fake_migrate=fake_migrate) 1237 table.group_id.requires = IS_IN_DB(db, '%s.id' % 1238 self.settings.table_group_name, 1239 '%(role)s (%(id)s)') 1240 table.name.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1241 table.table_name.requires = IS_IN_SET(self.db.tables) 1242 table.record_id.requires = IS_INT_IN_RANGE(0, 10 ** 9) 1243 self.settings.table_permission = db[self.settings.table_permission_name] 1244 if not self.settings.table_event_name in db.tables: 1245 table = db.define_table( 1246 self.settings.table_event_name, 1247 Field('time_stamp', 'datetime', 1248 default=self.environment.request.now, 1249 label=self.messages.label_time_stamp), 1250 Field('client_ip', 1251 default=self.environment.request.client, 1252 label=self.messages.label_client_ip), 1253 Field('user_id', self.settings.table_user, default=None, 1254 label=self.messages.label_user_id), 1255 Field('origin', default='auth', length=512, 1256 label=self.messages.label_origin), 1257 Field('description', 'text', default='', 1258 label=self.messages.label_description), 1259 migrate=self.__get_migrate( 1260 self.settings.table_event_name, migrate), 1261 fake_migrate=fake_migrate) 1262 table.user_id.requires = IS_IN_DB(db, '%s.id' % 1263 self.settings.table_user_name, 1264 '%(first_name)s %(last_name)s (%(id)s)') 1265 table.origin.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1266 table.description.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1267 self.settings.table_event = db[self.settings.table_event_name] 1268 def lazy_user (auth = self): return auth.user_id 1269 now = self.environment.request.now 1270 self.signature = db.Table(self.db,'auth_signature', 1271 Field('is_active','boolean',default=True), 1272 Field('created_on','datetime',default=now, 1273 writable=False,readable=False), 1274 Field('created_by',self.settings.table_user,default=lazy_user, 1275 writable=False,readable=False), 1276 Field('modified_on','datetime',update=now,default=now, 1277 writable=False,readable=False), 1278 Field('modified_by',self.settings.table_user, 1279 default=lazy_user,update=lazy_user, 1280 writable=False,readable=False))
1281 1282
1283 - def log_event(self, description, origin='auth'):
1284 """ 1285 usage:: 1286 1287 auth.log_event(description='this happened', origin='auth') 1288 """ 1289 1290 if self.is_logged_in(): 1291 user_id = self.user.id 1292 else: 1293 user_id = None # user unknown 1294 self.settings.table_event.insert(description=description, 1295 origin=origin, user_id=user_id)
1296
1297 - def get_or_create_user(self, keys):
1298 """ 1299 Used for alternate login methods: 1300 If the user exists already then password is updated. 1301 If the user doesn't yet exist, then they are created. 1302 """ 1303 table_user = self.settings.table_user 1304 if 'registration_id' in table_user.fields() and 'registration_id' in keys: 1305 username = 'registration_id' 1306 elif 'username' in table_user.fields(): 1307 username = 'username' 1308 elif 'email' in table_user.fields(): 1309 username = 'email' 1310 else: 1311 raise SyntaxError, "user must have username or email" 1312 passfield = self.settings.password_field 1313 user = self.db(table_user[username] == keys[username]).select().first() 1314 if user: 1315 if passfield in keys and keys[passfield]: 1316 user.update_record(**{passfield: keys[passfield], 1317 'registration_key': ''}) 1318 else: 1319 d = {username: keys[username], 1320 'first_name': keys.get('first_name', keys[username]), 1321 'last_name': keys.get('last_name', ''), 1322 'registration_key': ''} 1323 keys = dict([(k, v) for (k, v) in keys.items() \ 1324 if k in table_user.fields]) 1325 d.update(keys) 1326 user_id = table_user.insert(**d) 1327 user = self.user = table_user[user_id] 1328 if self.settings.create_user_groups: 1329 group_id = self.add_group("user_%s" % user_id) 1330 self.add_membership(group_id, user_id) 1331 return user
1332
1333 - def basic(self):
1334 if not self.settings.allow_basic_login: 1335 return False 1336 basic = self.environment.request.env.http_authorization 1337 if not basic or not basic[:6].lower() == 'basic ': 1338 return False 1339 (username, password) = base64.b64decode(basic[6:]).split(':') 1340 return self.login_bare(username, password)
1341
1342 - def login_bare(self, username, password):
1343 """ 1344 logins user 1345 """ 1346 1347 request = self.environment.request 1348 session = self.environment.session 1349 table_user = self.settings.table_user 1350 if self.settings.login_userfield: 1351 userfield = self.settings.login_userfield 1352 elif 'username' in table_user.fields: 1353 userfield = 'username' 1354 else: 1355 userfield = 'email' 1356 passfield = self.settings.password_field 1357 user = self.db(table_user[userfield] == username).select().first() 1358 password = table_user[passfield].validate(password)[0] 1359 if user: 1360 if not user.registration_key and user[passfield] == password: 1361 user = Storage(table_user._filter_fields(user, id=True)) 1362 session.auth = Storage(user=user, last_visit=request.now, 1363 expiration=self.settings.expiration) 1364 self.user = user 1365 return user 1366 return False
1367
1368 - def login( 1369 self, 1370 next=DEFAULT, 1371 onvalidation=DEFAULT, 1372 onaccept=DEFAULT, 1373 log=DEFAULT, 1374 ):
1375 """ 1376 returns a login form 1377 1378 .. method:: Auth.login([next=DEFAULT [, onvalidation=DEFAULT 1379 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1380 1381 """ 1382 1383 table_user = self.settings.table_user 1384 if self.settings.login_userfield: 1385 username = self.settings.login_userfield 1386 elif 'username' in table_user.fields: 1387 username = 'username' 1388 else: 1389 username = 'email' 1390 if 'username' in table_user.fields or not self.settings.login_email_validate: 1391 tmpvalidator = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1392 else: 1393 tmpvalidator = IS_EMAIL(error_message=self.messages.invalid_email) 1394 old_requires = table_user[username].requires 1395 table_user[username].requires = tmpvalidator 1396 request = self.environment.request 1397 response = self.environment.response 1398 session = self.environment.session 1399 passfield = self.settings.password_field 1400 if next == DEFAULT: 1401 next = request.get_vars._next \ 1402 or request.post_vars._next \ 1403 or self.settings.login_next 1404 if onvalidation == DEFAULT: 1405 onvalidation = self.settings.login_onvalidation 1406 if onaccept == DEFAULT: 1407 onaccept = self.settings.login_onaccept 1408 if log == DEFAULT: 1409 log = self.messages.login_log 1410 1411 user = None # default 1412 1413 # do we use our own login form, or from a central source? 1414 if self.settings.login_form == self: 1415 form = SQLFORM( 1416 table_user, 1417 fields=[username, passfield], 1418 hidden=dict(_next=next), 1419 showid=self.settings.showid, 1420 submit_button=self.messages.submit_button, 1421 delete_label=self.messages.delete_label, 1422 formstyle=self.settings.formstyle 1423 ) 1424 1425 if self.settings.remember_me_form: 1426 ## adds a new input checkbox "remember me for longer" 1427 addrow(form,XML("&nbsp;"), 1428 DIV(XML("&nbsp;"), 1429 INPUT(_type='checkbox', 1430 _class='checkbox', 1431 _id="auth_user_remember", 1432 _name="remember", 1433 ), 1434 XML("&nbsp;&nbsp;"), 1435 LABEL( 1436 self.messages.label_remember_me, 1437 _for="auth_user_remember", 1438 )),"", 1439 self.settings.formstyle, 1440 'auth_user_remember__row') 1441 1442 captcha = self.settings.login_captcha or \ 1443 (self.settings.login_captcha!=False and self.settings.captcha) 1444 if captcha: 1445 addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') 1446 accepted_form = False 1447 if form.accepts(request, session, 1448 formname='login', dbio=False, 1449 onvalidation=onvalidation, 1450 hideerror=self.settings.hideerror): 1451 accepted_form = True 1452 # check for username in db 1453 user = self.db(table_user[username] == form.vars[username]).select().first() 1454 if user: 1455 # user in db, check if registration pending or disabled 1456 temp_user = user 1457 if temp_user.registration_key == 'pending': 1458 response.flash = self.messages.registration_pending 1459 return form 1460 elif temp_user.registration_key in ('disabled','blocked'): 1461 response.flash = self.messages.login_disabled 1462 return form 1463 elif temp_user.registration_key!=None and temp_user.registration_key.strip(): 1464 response.flash = \ 1465 self.messages.registration_verifying 1466 return form 1467 # try alternate logins 1st as these have the current version of the password 1468 user = None 1469 for login_method in self.settings.login_methods: 1470 if login_method != self and \ 1471 login_method(request.vars[username], 1472 request.vars[passfield]): 1473 if not self in self.settings.login_methods: 1474 # do not store password in db 1475 form.vars[passfield] = None 1476 user = self.get_or_create_user(form.vars) 1477 break 1478 if not user: 1479 # alternates have failed, maybe because service inaccessible 1480 if self.settings.login_methods[0] == self: 1481 # try logging in locally using cached credentials 1482 if temp_user[passfield] == form.vars.get(passfield, ''): 1483 # success 1484 user = temp_user 1485 else: 1486 # user not in db 1487 if not self.settings.alternate_requires_registration: 1488 # we're allowed to auto-register users from external systems 1489 for login_method in self.settings.login_methods: 1490 if login_method != self and \ 1491 login_method(request.vars[username], 1492 request.vars[passfield]): 1493 if not self in self.settings.login_methods: 1494 # do not store password in db 1495 form.vars[passfield] = None 1496 user = self.get_or_create_user(form.vars) 1497 break 1498 if not user: 1499 if self.settings.login_failed_log: 1500 self.log_event(self.settings.login_failed_log % request.post_vars) 1501 # invalid login 1502 session.flash = self.messages.invalid_login 1503 redirect(self.url(args=request.args,vars=request.get_vars)) 1504 1505 else: 1506 # use a central authentication server 1507 cas = self.settings.login_form 1508 cas_user = cas.get_user() 1509 1510 if cas_user: 1511 cas_user[passfield] = None 1512 user = self.get_or_create_user(cas_user) 1513 elif hasattr(cas,'login_form'): 1514 return cas.login_form() 1515 else: 1516 # we need to pass through login again before going on 1517 next = self.url('user',args='login',vars=dict(_next=next)) 1518 redirect(cas.login_url(next)) 1519 1520 1521 # process authenticated users 1522 if user: 1523 user = Storage(table_user._filter_fields(user, id=True)) 1524 1525 if request.vars.has_key("remember"): 1526 # user wants to be logged in for longer 1527 session.auth = Storage( 1528 user = user, 1529 last_visit = request.now, 1530 expiration = self.settings.long_expiration, 1531 remember = True, 1532 ) 1533 else: 1534 # user doesn't want to be logged in for longer 1535 session.auth = Storage( 1536 user = user, 1537 last_visit = request.now, 1538 expiration = self.settings.expiration, 1539 remember = False, 1540 ) 1541 1542 self.user = user 1543 session.flash = self.messages.logged_in 1544 if log and self.user: 1545 self.log_event(log % self.user) 1546 1547 # how to continue 1548 if self.settings.login_form == self: 1549 if accepted_form: 1550 callback(onaccept,form) 1551 if isinstance(next, (list, tuple)): 1552 # fix issue with 2.6 1553 next = next[0] 1554 if next and not next[0] == '/' and next[:4] != 'http': 1555 next = self.url(next.replace('[id]', str(form.vars.id))) 1556 redirect(next) 1557 table_user[username].requires = old_requires 1558 return form 1559 else: 1560 redirect(next)
1561
1562 - def logout(self, next=DEFAULT, onlogout=DEFAULT, log=DEFAULT):
1563 """ 1564 logout and redirects to login 1565 1566 .. method:: Auth.logout ([next=DEFAULT[, onlogout=DEFAULT[, 1567 log=DEFAULT]]]) 1568 1569 """ 1570 1571 if next == DEFAULT: 1572 next = self.settings.logout_next 1573 if onlogout == DEFAULT: 1574 onlogout = self.settings.logout_onlogout 1575 if onlogout: 1576 onlogout(self.user) 1577 if log == DEFAULT: 1578 log = self.messages.logout_log 1579 if log and self.user: 1580 self.log_event(log % self.user) 1581 1582 if self.settings.login_form != self: 1583 cas = self.settings.login_form 1584 cas_user = cas.get_user() 1585 if cas_user: 1586 next = cas.logout_url(next) 1587 1588 self.environment.session.auth = None 1589 self.environment.session.flash = self.messages.logged_out 1590 if next: 1591 redirect(next)
1592
1593 - def register( 1594 self, 1595 next=DEFAULT, 1596 onvalidation=DEFAULT, 1597 onaccept=DEFAULT, 1598 log=DEFAULT, 1599 ):
1600 """ 1601 returns a registration form 1602 1603 .. method:: Auth.register([next=DEFAULT [, onvalidation=DEFAULT 1604 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1605 1606 """ 1607 1608 table_user = self.settings.table_user 1609 request = self.environment.request 1610 response = self.environment.response 1611 session = self.environment.session 1612 if self.is_logged_in(): 1613 redirect(self.settings.logged_url) 1614 if next == DEFAULT: 1615 next = request.get_vars._next \ 1616 or request.post_vars._next \ 1617 or self.settings.register_next 1618 if onvalidation == DEFAULT: 1619 onvalidation = self.settings.register_onvalidation 1620 if onaccept == DEFAULT: 1621 onaccept = self.settings.register_onaccept 1622 if log == DEFAULT: 1623 log = self.messages.register_log 1624 1625 passfield = self.settings.password_field 1626 formstyle = self.settings.formstyle 1627 form = SQLFORM(table_user, 1628 fields = self.settings.register_fields, 1629 hidden=dict(_next=next), 1630 showid=self.settings.showid, 1631 submit_button=self.messages.submit_button, 1632 delete_label=self.messages.delete_label, 1633 formstyle=formstyle 1634 ) 1635 for i, row in enumerate(form[0].components): 1636 item = row.element('input',_name=passfield) 1637 if item: 1638 form.custom.widget.password_two = \ 1639 INPUT(_name="password_two", _type="password", 1640 requires=IS_EXPR('value==%s' % \ 1641 repr(request.vars.get(passfield, None)), 1642 error_message=self.messages.mismatched_password)) 1643 1644 addrow(form, self.messages.verify_password + ':', 1645 form.custom.widget.password_two, 1646 self.messages.verify_password_comment, 1647 formstyle, 1648 '%s_%s__row' % (table_user, 'password_two'), 1649 position=i+1) 1650 break 1651 captcha = self.settings.register_captcha or self.settings.captcha 1652 if captcha: 1653 addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') 1654 1655 table_user.registration_key.default = key = web2py_uuid() 1656 if form.accepts(request, session, formname='register', 1657 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1658 description = self.messages.group_description % form.vars 1659 if self.settings.create_user_groups: 1660 group_id = self.add_group("user_%s" % form.vars.id, description) 1661 self.add_membership(group_id, form.vars.id) 1662 if self.settings.registration_requires_verification: 1663 if not self.settings.mailer or \ 1664 not self.settings.mailer.send(to=form.vars.email, 1665 subject=self.messages.verify_email_subject, 1666 message=self.messages.verify_email 1667 % dict(key=key)): 1668 self.db.rollback() 1669 response.flash = self.messages.unable_send_email 1670 return form 1671 session.flash = self.messages.email_sent 1672 elif self.settings.registration_requires_approval: 1673 table_user[form.vars.id] = dict(registration_key='pending') 1674 session.flash = self.messages.registration_pending 1675 else: 1676 table_user[form.vars.id] = dict(registration_key='') 1677 session.flash = self.messages.registration_successful 1678 table_user = self.settings.table_user 1679 if 'username' in table_user.fields: 1680 username = 'username' 1681 else: 1682 username = 'email' 1683 user = self.db(table_user[username] == form.vars[username]).select().first() 1684 user = Storage(table_user._filter_fields(user, id=True)) 1685 session.auth = Storage(user=user, last_visit=request.now, 1686 expiration=self.settings.expiration) 1687 self.user = user 1688 session.flash = self.messages.logged_in 1689 if log: 1690 self.log_event(log % form.vars) 1691 callback(onaccept,form) 1692 if not next: 1693 next = self.url(args = request.args) 1694 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1695 next = next[0] 1696 elif next and not next[0] == '/' and next[:4] != 'http': 1697 next = self.url(next.replace('[id]', str(form.vars.id))) 1698 redirect(next) 1699 return form
1700
1701 - def is_logged_in(self):
1702 """ 1703 checks if the user is logged in and returns True/False. 1704 if so user is in auth.user as well as in session.auth.user 1705 """ 1706 1707 if self.user: 1708 return True 1709 return False
1710
1711 - def verify_email( 1712 self, 1713 next=DEFAULT, 1714 onaccept=DEFAULT, 1715 log=DEFAULT, 1716 ):
1717 """ 1718 action user to verify the registration email, XXXXXXXXXXXXXXXX 1719 1720 .. method:: Auth.verify_email([next=DEFAULT [, onvalidation=DEFAULT 1721 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1722 1723 """ 1724 1725 key = self.environment.request.args[-1] 1726 table_user = self.settings.table_user 1727 user = self.db(table_user.registration_key == key).select().first() 1728 if not user: 1729 raise HTTP(404) 1730 if self.settings.registration_requires_approval: 1731 user.update_record(registration_key = 'pending') 1732 self.environment.session.flash = self.messages.registration_pending 1733 else: 1734 user.update_record(registration_key = '') 1735 self.environment.session.flash = self.messages.email_verified 1736 if log == DEFAULT: 1737 log = self.messages.verify_email_log 1738 if next == DEFAULT: 1739 next = self.settings.verify_email_next 1740 if onaccept == DEFAULT: 1741 onaccept = self.settings.verify_email_onaccept 1742 if log: 1743 self.log_event(log % user) 1744 callback(onaccept,user) 1745 redirect(next)
1746
1747 - def retrieve_username( 1748 self, 1749 next=DEFAULT, 1750 onvalidation=DEFAULT, 1751 onaccept=DEFAULT, 1752 log=DEFAULT, 1753 ):
1754 """ 1755 returns a form to retrieve the user username 1756 (only if there is a username field) 1757 1758 .. method:: Auth.retrieve_username([next=DEFAULT 1759 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1760 1761 """ 1762 1763 table_user = self.settings.table_user 1764 if not 'username' in table_user.fields: 1765 raise HTTP(404) 1766 request = self.environment.request 1767 response = self.environment.response 1768 session = self.environment.session 1769 captcha = self.settings.retrieve_username_captcha or \ 1770 (self.settings.retrieve_username_captcha!=False and self.settings.captcha) 1771 if not self.settings.mailer: 1772 response.flash = self.messages.function_disabled 1773 return '' 1774 if next == DEFAULT: 1775 next = request.get_vars._next \ 1776 or request.post_vars._next \ 1777 or self.settings.retrieve_username_next 1778 if onvalidation == DEFAULT: 1779 onvalidation = self.settings.retrieve_username_onvalidation 1780 if onaccept == DEFAULT: 1781 onaccept = self.settings.retrieve_username_onaccept 1782 if log == DEFAULT: 1783 log = self.messages.retrieve_username_log 1784 old_requires = table_user.email.requires 1785 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 1786 error_message=self.messages.invalid_email)] 1787 form = SQLFORM(table_user, 1788 fields=['email'], 1789 hidden=dict(_next=next), 1790 showid=self.settings.showid, 1791 submit_button=self.messages.submit_button, 1792 delete_label=self.messages.delete_label, 1793 formstyle=self.settings.formstyle 1794 ) 1795 if captcha: 1796 addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') 1797 1798 if form.accepts(request, session, 1799 formname='retrieve_username', dbio=False, 1800 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1801 user = self.db(table_user.email == form.vars.email).select().first() 1802 if not user: 1803 self.environment.session.flash = \ 1804 self.messages.invalid_email 1805 redirect(self.url(args=request.args)) 1806 username = user.username 1807 self.settings.mailer.send(to=form.vars.email, 1808 subject=self.messages.retrieve_username_subject, 1809 message=self.messages.retrieve_username 1810 % dict(username=username)) 1811 session.flash = self.messages.email_sent 1812 if log: 1813 self.log_event(log % user) 1814 callback(onaccept,form) 1815 if not next: 1816 next = self.url(args = request.args) 1817 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1818 next = next[0] 1819 elif next and not next[0] == '/' and next[:4] != 'http': 1820 next = self.url(next.replace('[id]', str(form.vars.id))) 1821 redirect(next) 1822 table_user.email.requires = old_requires 1823 return form
1824
1825 - def random_password(self):
1826 import string 1827 import random 1828 password = '' 1829 specials=r'!#$*' 1830 for i in range(0,3): 1831 password += random.choice(string.lowercase) 1832 password += random.choice(string.uppercase) 1833 password += random.choice(string.digits) 1834 password += random.choice(specials) 1835 return ''.join(random.sample(password,len(password)))
1836
1837 - def reset_password_deprecated( 1838 self, 1839 next=DEFAULT, 1840 onvalidation=DEFAULT, 1841 onaccept=DEFAULT, 1842 log=DEFAULT, 1843 ):
1844 """ 1845 returns a form to reset the user password (deprecated) 1846 1847 .. method:: Auth.reset_password_deprecated([next=DEFAULT 1848 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1849 1850 """ 1851 1852 table_user = self.settings.table_user 1853 request = self.environment.request 1854 response = self.environment.response 1855 session = self.environment.session 1856 if not self.settings.mailer: 1857 response.flash = self.messages.function_disabled 1858 return '' 1859 if next == DEFAULT: 1860 next = request.get_vars._next \ 1861 or request.post_vars._next \ 1862 or self.settings.retrieve_password_next 1863 if onvalidation == DEFAULT: 1864 onvalidation = self.settings.retrieve_password_onvalidation 1865 if onaccept == DEFAULT: 1866 onaccept = self.settings.retrieve_password_onaccept 1867 if log == DEFAULT: 1868 log = self.messages.retrieve_password_log 1869 old_requires = table_user.email.requires 1870 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 1871 error_message=self.messages.invalid_email)] 1872 form = SQLFORM(table_user, 1873 fields=['email'], 1874 hidden=dict(_next=next), 1875 showid=self.settings.showid, 1876 submit_button=self.messages.submit_button, 1877 delete_label=self.messages.delete_label, 1878 formstyle=self.settings.formstyle 1879 ) 1880 if form.accepts(request, session, 1881 formname='retrieve_password', dbio=False, 1882 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1883 user = self.db(table_user.email == form.vars.email).select().first() 1884 if not user: 1885 self.environment.session.flash = \ 1886 self.messages.invalid_email 1887 redirect(self.url(args=request.args)) 1888 elif user.registration_key in ('pending','disabled','blocked'): 1889 self.environment.session.flash = \ 1890 self.messages.registration_pending 1891 redirect(self.url(args=request.args)) 1892 password = self.random_password() 1893 passfield = self.settings.password_field 1894 d = {passfield: table_user[passfield].validate(password)[0], 1895 'registration_key': ''} 1896 user.update_record(**d) 1897 if self.settings.mailer and \ 1898 self.settings.mailer.send(to=form.vars.email, 1899 subject=self.messages.retrieve_password_subject, 1900 message=self.messages.retrieve_password \ 1901 % dict(password=password)): 1902 session.flash = self.messages.email_sent 1903 else: 1904 session.flash = self.messages.unable_to_send_email 1905 if log: 1906 self.log_event(log % user) 1907 callback(onaccept,form) 1908 if not next: 1909 next = self.url(args = request.args) 1910 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1911 next = next[0] 1912 elif next and not next[0] == '/' and next[:4] != 'http': 1913 next = self.url(next.replace('[id]', str(form.vars.id))) 1914 redirect(next) 1915 table_user.email.requires = old_requires 1916 return form
1917
1918 - def reset_password( 1919 self, 1920 next=DEFAULT, 1921 onvalidation=DEFAULT, 1922 onaccept=DEFAULT, 1923 log=DEFAULT, 1924 ):
1925 """ 1926 returns a form to reset the user password 1927 1928 .. method:: Auth.reset_password([next=DEFAULT 1929 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1930 1931 """ 1932 1933 table_user = self.settings.table_user 1934 request = self.environment.request 1935 # response = self.environment.response 1936 session = self.environment.session 1937 1938 if next == DEFAULT: 1939 next = request.get_vars._next \ 1940 or request.post_vars._next \ 1941 or self.settings.reset_password_next 1942 1943 try: 1944 key = request.vars.key or request.args[-1] 1945 t0 = int(key.split('-')[0]) 1946 if time.time()-t0 > 60*60*24: raise Exception 1947 user = self.db(table_user.reset_password_key == key).select().first() 1948 if not user: raise Exception 1949 except Exception: 1950 session.flash = self.messages.invalid_reset_password 1951 redirect(next) 1952 passfield = self.settings.password_field 1953 form = SQLFORM.factory( 1954 Field('new_password', 'password', 1955 label=self.messages.new_password, 1956 requires=self.settings.table_user[passfield].requires), 1957 Field('new_password2', 'password', 1958 label=self.messages.verify_password, 1959 requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), 1960 self.messages.mismatched_password)]), 1961 submit_button=self.messages.submit_button, 1962 formstyle=self.settings.formstyle, 1963 ) 1964 if form.accepts(request,session,hideerror=self.settings.hideerror): 1965 user.update_record(**{passfield:form.vars.new_password, 1966 'registration_key':'', 1967 'reset_password_key':''}) 1968 session.flash = self.messages.password_changed 1969 redirect(next) 1970 return form
1971
1972 - def request_reset_password( 1973 self, 1974 next=DEFAULT, 1975 onvalidation=DEFAULT, 1976 onaccept=DEFAULT, 1977 log=DEFAULT, 1978 ):
1979 """ 1980 returns a form to reset the user password 1981 1982 .. method:: Auth.reset_password([next=DEFAULT 1983 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1984 1985 """ 1986 1987 table_user = self.settings.table_user 1988 request = self.environment.request 1989 response = self.environment.response 1990 session = self.environment.session 1991 captcha = self.settings.retrieve_password_captcha or \ 1992 (self.settings.retrieve_password_captcha!=False and self.settings.captcha) 1993 1994 if next == DEFAULT: 1995 next = request.get_vars._next \ 1996 or request.post_vars._next \ 1997 or self.settings.request_reset_password_next 1998 1999 if not self.settings.mailer: 2000 response.flash = self.messages.function_disabled 2001 return '' 2002 if onvalidation == DEFAULT: 2003 onvalidation = self.settings.reset_password_onvalidation 2004 if onaccept == DEFAULT: 2005 onaccept = self.settings.reset_password_onaccept 2006 if log == DEFAULT: 2007 log = self.messages.reset_password_log 2008 # old_requires = table_user.email.requires <<< perhaps should be restored 2009 table_user.email.requires = [ 2010 IS_EMAIL(error_message=self.messages.invalid_email), 2011 IS_IN_DB(self.db, table_user.email, 2012 error_message=self.messages.invalid_email)] 2013 form = SQLFORM(table_user, 2014 fields=['email'], 2015 hidden=dict(_next=next), 2016 showid=self.settings.showid, 2017 submit_button=self.messages.submit_button, 2018 delete_label=self.messages.delete_label, 2019 formstyle=self.settings.formstyle 2020 ) 2021 if captcha: 2022 addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') 2023 if form.accepts(request, session, 2024 formname='reset_password', dbio=False, 2025 onvalidation=onvalidation, 2026 hideerror=self.settings.hideerror): 2027 user = self.db(table_user.email == form.vars.email).select().first() 2028 if not user: 2029 session.flash = self.messages.invalid_email 2030 redirect(self.url(args=request.args)) 2031 elif user.registration_key in ('pending','disabled','blocked'): 2032 session.flash = self.messages.registration_pending 2033 redirect(self.url(args=request.args)) 2034 reset_password_key = str(int(time.time()))+'-' + web2py_uuid() 2035 2036 if self.settings.mailer.send(to=form.vars.email, 2037 subject=self.messages.reset_password_subject, 2038 message=self.messages.reset_password % \ 2039 dict(key=reset_password_key)): 2040 session.flash = self.messages.email_sent 2041 user.update_record(reset_password_key=reset_password_key) 2042 else: 2043 session.flash = self.messages.unable_to_send_email 2044 if log: 2045 self.log_event(log % user) 2046 callback(onaccept,form) 2047 if not next: 2048 next = self.url(args = request.args) 2049 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2050 next = next[0] 2051 elif next and not next[0] == '/' and next[:4] != 'http': 2052 next = self.url(next.replace('[id]', str(form.vars.id))) 2053 redirect(next) 2054 # old_requires = table_user.email.requires 2055 return form
2056
2057 - def retrieve_password( 2058 self, 2059 next=DEFAULT, 2060 onvalidation=DEFAULT, 2061 onaccept=DEFAULT, 2062 log=DEFAULT, 2063 ):
2064 if self.settings.reset_password_requires_verification: 2065 return self.request_reset_password(next,onvalidation,onaccept,log) 2066 else: 2067 return self.reset_password_deprecated(next,onvalidation,onaccept,log)
2068
2069 - def change_password( 2070 self, 2071 next=DEFAULT, 2072 onvalidation=DEFAULT, 2073 onaccept=DEFAULT, 2074 log=DEFAULT, 2075 ):
2076 """ 2077 returns a form that lets the user change password 2078 2079 .. method:: Auth.change_password([next=DEFAULT[, onvalidation=DEFAULT[, 2080 onaccept=DEFAULT[, log=DEFAULT]]]]) 2081 """ 2082 2083 if not self.is_logged_in(): 2084 redirect(self.settings.login_url) 2085 db = self.db 2086 table_user = self.settings.table_user 2087 usern = self.settings.table_user_name 2088 s = db(table_user.id == self.user.id) 2089 2090 request = self.environment.request 2091 session = self.environment.session 2092 if next == DEFAULT: 2093 next = request.get_vars._next \ 2094 or request.post_vars._next \ 2095 or self.settings.change_password_next 2096 if onvalidation == DEFAULT: 2097 onvalidation = self.settings.change_password_onvalidation 2098 if onaccept == DEFAULT: 2099 onaccept = self.settings.change_password_onaccept 2100 if log == DEFAULT: 2101 log = self.messages.change_password_log 2102 passfield = self.settings.password_field 2103 form = SQLFORM.factory( 2104 Field('old_password', 'password', 2105 label=self.messages.old_password, 2106 requires=validators( 2107 table_user[passfield].requires, 2108 IS_IN_DB(s, '%s.%s' % (usern, passfield), 2109 error_message=self.messages.invalid_password))), 2110 Field('new_password', 'password', 2111 label=self.messages.new_password, 2112 requires=table_user[passfield].requires), 2113 Field('new_password2', 'password', 2114 label=self.messages.verify_password, 2115 requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), 2116 self.messages.mismatched_password)]), 2117 submit_button=self.messages.submit_button, 2118 formstyle = self.settings.formstyle 2119 ) 2120 if form.accepts(request, session, 2121 formname='change_password', 2122 onvalidation=onvalidation, 2123 hideerror=self.settings.hideerror): 2124 d = {passfield: form.vars.new_password} 2125 s.update(**d) 2126 session.flash = self.messages.password_changed 2127 if log: 2128 self.log_event(log % self.user) 2129 callback(onaccept,form) 2130 if not next: 2131 next = self.url(args=request.args) 2132 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2133 next = next[0] 2134 elif next and not next[0] == '/' and next[:4] != 'http': 2135 next = self.url(next.replace('[id]', str(form.vars.id))) 2136 redirect(next) 2137 return form
2138
2139 - def profile( 2140 self, 2141 next=DEFAULT, 2142 onvalidation=DEFAULT, 2143 onaccept=DEFAULT, 2144 log=DEFAULT, 2145 ):
2146 """ 2147 returns a form that lets the user change his/her profile 2148 2149 .. method:: Auth.profile([next=DEFAULT [, onvalidation=DEFAULT 2150 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2151 2152 """ 2153 2154 table_user = self.settings.table_user 2155 if not self.is_logged_in(): 2156 redirect(self.settings.login_url) 2157 passfield = self.settings.password_field 2158 self.settings.table_user[passfield].writable = False 2159 request = self.environment.request 2160 session = self.environment.session 2161 if next == DEFAULT: 2162 next = request.get_vars._next \ 2163 or request.post_vars._next \ 2164 or self.settings.profile_next 2165 if onvalidation == DEFAULT: 2166 onvalidation = self.settings.profile_onvalidation 2167 if onaccept == DEFAULT: 2168 onaccept = self.settings.profile_onaccept 2169 if log == DEFAULT: 2170 log = self.messages.profile_log 2171 form = SQLFORM( 2172 table_user, 2173 self.user.id, 2174 fields = self.settings.profile_fields, 2175 hidden = dict(_next=next), 2176 showid = self.settings.showid, 2177 submit_button = self.messages.submit_button, 2178 delete_label = self.messages.delete_label, 2179 upload = self.settings.download_url, 2180 formstyle = self.settings.formstyle 2181 ) 2182 if form.accepts(request, session, 2183 formname='profile', 2184 onvalidation=onvalidation,hideerror=self.settings.hideerror): 2185 self.user.update(table_user._filter_fields(form.vars)) 2186 session.flash = self.messages.profile_updated 2187 if log: 2188 self.log_event(log % self.user) 2189 callback(onaccept,form) 2190 if not next: 2191 next = self.url(args=request.args) 2192 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2193 next = next[0] 2194 elif next and not next[0] == '/' and next[:4] != 'http': 2195 next = self.url(next.replace('[id]', str(form.vars.id))) 2196 redirect(next) 2197 return form
2198
2199 - def is_impersonating(self):
2200 return self.environment.session.auth.impersonator
2201
2202 - def impersonate(self, user_id=DEFAULT):
2203 """ 2204 usage: POST TO http://..../impersonate request.post_vars.user_id=<id> 2205 set request.post_vars.user_id to 0 to restore original user. 2206 2207 requires impersonator is logged in and 2208 has_permission('impersonate', 'auth_user', user_id) 2209 """ 2210 request = self.environment.request 2211 session = self.environment.session 2212 auth = session.auth 2213 if not self.is_logged_in() or not self.environment.request.post_vars: 2214 raise HTTP(401, "Not Authorized") 2215 current_id = auth.user.id 2216 requested_id = user_id 2217 if user_id == DEFAULT: 2218 user_id = self.environment.request.post_vars.user_id 2219 if user_id and user_id != self.user.id and user_id != '0': 2220 if not self.has_permission('impersonate', 2221 self.settings.table_user_name, 2222 user_id): 2223 raise HTTP(403, "Forbidden") 2224 user = self.settings.table_user(user_id) 2225 if not user: 2226 raise HTTP(401, "Not Authorized") 2227 auth.impersonator = cPickle.dumps(session) 2228 auth.user.update( 2229 self.settings.table_user._filter_fields(user, True)) 2230 self.user = auth.user 2231 if self.settings.login_onaccept: 2232 form = Storage(dict(vars=self.user)) 2233 self.settings.login_onaccept(form) 2234 log = self.messages.impersonate_log 2235 if log: 2236 self.log_event(log % dict(id=current_id,other_id=auth.user.id)) 2237 elif user_id in (0, '0') and self.is_impersonating(): 2238 session.clear() 2239 session.update(cPickle.loads(auth.impersonator)) 2240 self.user = session.auth.user 2241 if requested_id == DEFAULT and not request.post_vars: 2242 return SQLFORM.factory(Field('user_id','integer')) 2243 return self.user
2244
2245 - def groups(self):
2246 """ 2247 displays the groups and their roles for the logged in user 2248 """ 2249 2250 if not self.is_logged_in(): 2251 redirect(self.settings.login_url) 2252 memberships = self.db(self.settings.table_membership.user_id 2253 == self.user.id).select() 2254 table = TABLE() 2255 for membership in memberships: 2256 groups = self.db(self.settings.table_group.id 2257 == membership.group_id).select() 2258 if groups: 2259 group = groups[0] 2260 table.append(TR(H3(group.role, '(%s)' % group.id))) 2261 table.append(TR(P(group.description))) 2262 if not memberships: 2263 return None 2264 return table
2265
2266 - def not_authorized(self):
2267 """ 2268 you can change the view for this page to make it look as you like 2269 """ 2270 2271 return 'ACCESS DENIED'
2272
2273 - def requires(self, condition):
2274 """ 2275 decorator that prevents access to action if not logged in 2276 """ 2277 2278 def decorator(action): 2279 2280 def f(*a, **b): 2281 2282 if self.settings.allow_basic_login_only and not self.basic(): 2283 if self.environment.request.is_restful: 2284 raise HTTP(403,"Not authorized") 2285 return call_or_redirect(self.settings.on_failed_authorization) 2286 2287 if not condition: 2288 if self.environment.request.is_restful: 2289 raise HTTP(403,"Not authorized") 2290 if not self.basic() and not self.is_logged_in(): 2291 request = self.environment.request 2292 next = URL(r=request,args=request.args, 2293 vars=request.get_vars) 2294 self.environment.session.flash = self.environment.response.flash 2295 return call_or_redirect(self.settings.on_failed_authentication, 2296 self.settings.login_url + \ 2297 '?_next='+urllib.quote(next)) 2298 else: 2299 self.environment.session.flash = \ 2300 self.messages.access_denied 2301 return call_or_redirect(self.settings.on_failed_authorization) 2302 return action(*a, **b)
2303 f.__doc__ = action.__doc__ 2304 f.__name__ = action.__name__ 2305 f.__dict__.update(action.__dict__) 2306 return f
2307 2308 return decorator 2309
2310 - def requires_login(self):
2311 """ 2312 decorator that prevents access to action if not logged in 2313 """ 2314 2315 def decorator(action): 2316 2317 def f(*a, **b): 2318 2319 if self.settings.allow_basic_login_only and not self.basic(): 2320 if self.environment.request.is_restful: 2321 raise HTTP(403,"Not authorized") 2322 return call_or_redirect(self.settings.on_failed_authorization) 2323 2324 if not self.basic() and not self.is_logged_in(): 2325 if self.environment.request.is_restful: 2326 raise HTTP(403,"Not authorized") 2327 request = self.environment.request 2328 next = URL(r=request,args=request.args, 2329 vars=request.get_vars) 2330 self.environment.session.flash = self.environment.response.flash 2331 return call_or_redirect(self.settings.on_failed_authentication, 2332 self.settings.login_url + \ 2333 '?_next='+urllib.quote(next) 2334 ) 2335 return action(*a, **b)
2336 f.__doc__ = action.__doc__ 2337 f.__name__ = action.__name__ 2338 f.__dict__.update(action.__dict__) 2339 return f 2340 2341 return decorator 2342
2343 - def requires_membership(self, role=None, group_id=None):
2344 """ 2345 decorator that prevents access to action if not logged in or 2346 if user logged in is not a member of group_id. 2347 If role is provided instead of group_id then the 2348 group_id is calculated. 2349 """ 2350 2351 def decorator(action): 2352 def f(*a, **b): 2353 if self.settings.allow_basic_login_only and not self.basic(): 2354 if self.environment.request.is_restful: 2355 raise HTTP(403,"Not authorized") 2356 return call_or_redirect(self.settings.on_failed_authorization) 2357 2358 if not self.basic() and not self.is_logged_in(): 2359 if self.environment.request.is_restful: 2360 raise HTTP(403,"Not authorized") 2361 request = self.environment.request 2362 next = URL(r=request,args=request.args, 2363 vars=request.get_vars) 2364 self.environment.session.flash = self.environment.response.flash 2365 return call_or_redirect(self.settings.on_failed_authentication, 2366 self.settings.login_url + \ 2367 '?_next='+urllib.quote(next) 2368 ) 2369 if not self.has_membership(group_id=group_id, role=role): 2370 self.environment.session.flash = \ 2371 self.messages.access_denied 2372 return call_or_redirect(self.settings.on_failed_authorization) 2373 return action(*a, **b)
2374 f.__doc__ = action.__doc__ 2375 f.__name__ = action.__name__ 2376 f.__dict__.update(action.__dict__) 2377 return f 2378 2379 return decorator 2380
2381 - def requires_permission( 2382 self, 2383 name, 2384 table_name='', 2385 record_id=0, 2386 ):
2387 """ 2388 decorator that prevents access to action if not logged in or 2389 if user logged in is not a member of any group (role) that 2390 has 'name' access to 'table_name', 'record_id'. 2391 """ 2392 2393 def decorator(action): 2394 2395 def f(*a, **b): 2396 if self.settings.allow_basic_login_only and not self.basic(): 2397 if self.environment.request.is_restful: 2398 raise HTTP(403,"Not authorized") 2399 return call_or_redirect(self.settings.on_failed_authorization) 2400 2401 if not self.basic() and not self.is_logged_in(): 2402 if self.environment.request.is_restful: 2403 raise HTTP(403,"Not authorized") 2404 request = self.environment.request 2405 next = URL(r=request,args=request.args, 2406 vars=request.get_vars) 2407 self.environment.session.flash = self.environment.response.flash 2408 return call_or_redirect(self.settings.on_failed_authentication, 2409 self.settings.login_url + 2410 '?_next='+urllib.quote(next) 2411 ) 2412 2413 if not self.has_permission(name, table_name, record_id): 2414 self.environment.session.flash = \ 2415 self.messages.access_denied 2416 return call_or_redirect(self.settings.on_failed_authorization) 2417 return action(*a, **b)
2418 f.__doc__ = action.__doc__ 2419 f.__name__ = action.__name__ 2420 f.__dict__.update(action.__dict__) 2421 return f 2422 2423 return decorator 2424
2425 - def add_group(self, role, description=''):
2426 """ 2427 creates a group associated to a role 2428 """ 2429 2430 group_id = self.settings.table_group.insert(role=role, 2431 description=description) 2432 log = self.messages.add_group_log 2433 if log: 2434 self.log_event(log % dict(group_id=group_id, role=role)) 2435 return group_id
2436
2437 - def del_group(self, group_id):
2438 """ 2439 deletes a group 2440 """ 2441 2442 self.db(self.settings.table_group.id == group_id).delete() 2443 self.db(self.settings.table_membership.group_id 2444 == group_id).delete() 2445 self.db(self.settings.table_permission.group_id 2446 == group_id).delete() 2447 log = self.messages.del_group_log 2448 if log: 2449 self.log_event(log % dict(group_id=group_id))
2450
2451 - def id_group(self, role):
2452 """ 2453 returns the group_id of the group specified by the role 2454 """ 2455 rows = self.db(self.settings.table_group.role == role).select() 2456 if not rows: 2457 return None 2458 return rows[0].id
2459
2460 - def user_group(self, user_id = None):
2461 """ 2462 returns the group_id of the group uniquely associated to this user 2463 i.e. role=user:[user_id] 2464 """ 2465 if not user_id and self.user: 2466 user_id = self.user.id 2467 role = 'user_%s' % user_id 2468 return self.id_group(role)
2469
2470 - def has_membership(self, group_id=None, user_id=None, role=None):
2471 """ 2472 checks if user is member of group_id or role 2473 """ 2474 2475 group_id = group_id or self.id_group(role) 2476 try: 2477 group_id = int(group_id) 2478 except: 2479 group_id = self.id_group(group_id) # interpret group_id as a role 2480 if not user_id and self.user: 2481 user_id = self.user.id 2482 membership = self.settings.table_membership 2483 if self.db((membership.user_id == user_id) 2484 & (membership.group_id == group_id)).select(): 2485 r = True 2486 else: 2487 r = False 2488 log = self.messages.has_membership_log 2489 if log: 2490 self.log_event(log % dict(user_id=user_id, 2491 group_id=group_id, check=r)) 2492 return r
2493
2494 - def add_membership(self, group_id=None, user_id=None, role=None):
2495 """ 2496 gives user_id membership of group_id or role 2497 if user_id==None than user_id is that of current logged in user 2498 """ 2499 2500 group_id = group_id or self.id_group(role) 2501 try: 2502 group_id = int(group_id) 2503 except: 2504 group_id = self.id_group(group_id) # interpret group_id as a role 2505 if not user_id and self.user: 2506 user_id = self.user.id 2507 membership = self.settings.table_membership 2508 record = membership(user_id = user_id,group_id = group_id) 2509 if record: 2510 return record.id 2511 else: 2512 id = membership.insert(group_id=group_id, user_id=user_id) 2513 log = self.messages.add_membership_log 2514 if log: 2515 self.log_event(log % dict(user_id=user_id, 2516 group_id=group_id)) 2517 return id
2518
2519 - def del_membership(self, group_id, user_id=None, role=None):
2520 """ 2521 revokes membership from group_id to user_id 2522 if user_id==None than user_id is that of current logged in user 2523 """ 2524 2525 group_id = group_id or self.id_group(role) 2526 if not user_id and self.user: 2527 user_id = self.user.id 2528 membership = self.settings.table_membership 2529 log = self.messages.del_membership_log 2530 if log: 2531 self.log_event(log % dict(user_id=user_id, 2532 group_id=group_id)) 2533 return self.db(membership.user_id 2534 == user_id)(membership.group_id 2535 == group_id).delete()
2536
2537 - def has_permission( 2538 self, 2539 name='any', 2540 table_name='', 2541 record_id=0, 2542 user_id=None, 2543 group_id=None, 2544 ):
2545 """ 2546 checks if user_id or current logged in user is member of a group 2547 that has 'name' permission on 'table_name' and 'record_id' 2548 if group_id is passed, it checks whether the group has the permission 2549 """ 2550 2551 if not user_id and not group_id and self.user: 2552 user_id = self.user.id 2553 if user_id: 2554 membership = self.settings.table_membership 2555 rows = self.db(membership.user_id 2556 == user_id).select(membership.group_id) 2557 groups = set([row.group_id for row in rows]) 2558 if group_id and not group_id in groups: 2559 return False 2560 else: 2561 groups = set([group_id]) 2562 permission = self.settings.table_permission 2563 rows = self.db(permission.name == name)(permission.table_name 2564 == str(table_name))(permission.record_id 2565 == record_id).select(permission.group_id) 2566 groups_required = set([row.group_id for row in rows]) 2567 if record_id: 2568 rows = self.db(permission.name 2569 == name)(permission.table_name 2570 == str(table_name))(permission.record_id 2571 == 0).select(permission.group_id) 2572 groups_required = groups_required.union(set([row.group_id 2573 for row in rows])) 2574 if groups.intersection(groups_required): 2575 r = True 2576 else: 2577 r = False 2578 log = self.messages.has_permission_log 2579 if log and user_id: 2580 self.log_event(log % dict(user_id=user_id, name=name, 2581 table_name=table_name, record_id=record_id)) 2582 return r
2583
2584 - def add_permission( 2585 self, 2586 group_id, 2587 name='any', 2588 table_name='', 2589 record_id=0, 2590 ):
2591 """ 2592 gives group_id 'name' access to 'table_name' and 'record_id' 2593 """ 2594 2595 permission = self.settings.table_permission 2596 if group_id == 0: 2597 group_id = self.user_group() 2598 id = permission.insert(group_id=group_id, name=name, 2599 table_name=str(table_name), 2600 record_id=long(record_id)) 2601 log = self.messages.add_permission_log 2602 if log: 2603 self.log_event(log % dict(permission_id=id, group_id=group_id, 2604 name=name, table_name=table_name, 2605 record_id=record_id)) 2606 return id
2607
2608 - def del_permission( 2609 self, 2610 group_id, 2611 name='any', 2612 table_name='', 2613 record_id=0, 2614 ):
2615 """ 2616 revokes group_id 'name' access to 'table_name' and 'record_id' 2617 """ 2618 2619 permission = self.settings.table_permission 2620 log = self.messages.del_permission_log 2621 if log: 2622 self.log_event(log % dict(group_id=group_id, name=name, 2623 table_name=table_name, record_id=record_id)) 2624 return self.db(permission.group_id == group_id)(permission.name 2625 == name)(permission.table_name 2626 == str(table_name))(permission.record_id 2627 == long(record_id)).delete()
2628
2629 - def accessible_query(self, name, table, user_id=None):
2630 """ 2631 returns a query with all accessible records for user_id or 2632 the current logged in user 2633 this method does not work on GAE because uses JOIN and IN 2634 2635 example:: 2636 2637 db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL) 2638 2639 """ 2640 if not user_id: 2641 user_id = self.user.id 2642 if self.has_permission(name, table, 0, user_id): 2643 return table.id > 0 2644 db = self.db 2645 membership = self.settings.table_membership 2646 permission = self.settings.table_permission 2647 return table.id.belongs(db(membership.user_id == user_id)\ 2648 (membership.group_id == permission.group_id)\ 2649 (permission.name == name)\ 2650 (permission.table_name == table)\ 2651 ._select(permission.record_id))
2652 2653
2654 -class Crud(object):
2655
2656 - def url(self, f=None, args=[], vars={}):
2657 """ 2658 this should point to the controller that exposes 2659 download and crud 2660 """ 2661 return self.environment.URL(r=self.environment.request, 2662 c=self.settings.controller, 2663 f=f, args=args, vars=vars)
2664
2665 - def __init__(self, environment, db, controller='default'):
2666 self.environment = Storage(environment) 2667 self.db = db 2668 self.settings = Settings() 2669 self.settings.auth = None 2670 self.settings.logger = None 2671 2672 self.settings.create_next = None 2673 self.settings.update_next = None 2674 self.settings.controller = controller 2675 self.settings.delete_next = self.url() 2676 self.settings.download_url = self.url('download') 2677 self.settings.create_onvalidation = StorageList() 2678 self.settings.update_onvalidation = StorageList() 2679 self.settings.delete_onvalidation = StorageList() 2680 self.settings.create_onaccept = StorageList() 2681 self.settings.update_onaccept = StorageList() 2682 self.settings.update_ondelete = StorageList() 2683 self.settings.delete_onaccept = StorageList() 2684 self.settings.update_deletable = True 2685 self.settings.showid = False 2686 self.settings.keepvalues = False 2687 self.settings.create_captcha = None 2688 self.settings.update_captcha = None 2689 self.settings.captcha = None 2690 self.settings.formstyle = 'table3cols' 2691 self.settings.hideerror = False 2692 self.settings.detect_record_change = True 2693 self.settings.lock_keys = True 2694 2695 self.messages = Messages(self.environment.T) 2696 self.messages.submit_button = 'Submit' 2697 self.messages.delete_label = 'Check to delete:' 2698 self.messages.record_created = 'Record Created' 2699 self.messages.record_updated = 'Record Updated' 2700 self.messages.record_deleted = 'Record Deleted' 2701 2702 self.messages.update_log = 'Record %(id)s updated' 2703 self.messages.create_log = 'Record %(id)s created' 2704 self.messages.read_log = 'Record %(id)s read' 2705 self.messages.delete_log = 'Record %(id)s deleted' 2706 2707 self.messages.lock_keys = True
2708
2709 - def __call__(self):
2710 2711 args = self.environment.request.args 2712 if len(args) < 1: 2713 redirect(self.url(args='tables')) 2714 elif args[0] == 'tables': 2715 return self.tables() 2716 elif args[0] == 'create': 2717 return self.create(args(1)) 2718 elif args[0] == 'select': 2719 return self.select(args(1),linkto=self.url(args='read')) 2720 elif args[0] == 'read': 2721 return self.read(args(1), args(2)) 2722 elif args[0] == 'update': 2723 return self.update(args(1), args(2)) 2724 elif args[0] == 'delete': 2725 return self.delete(args(1), args(2)) 2726 else: 2727 raise HTTP(404)
2728
2729 - def log_event(self, message):
2730 if self.settings.logger: 2731 self.settings.logger.log_event(message, 'crud')
2732
2733 - def has_permission(self, name, table, record=0):
2734 if not self.settings.auth: 2735 return True 2736 try: 2737 record_id = record.id 2738 except: 2739 record_id = record 2740 return self.settings.auth.has_permission(name, str(table), record_id)
2741
2742 - def tables(self):
2743 return TABLE(*[TR(A(name, 2744 _href=self.url(args=('select',name)))) \ 2745 for name in self.db.tables])
2746 2747 2748 @staticmethod
2749 - def archive(form,archive_table=None,current_record='current_record'):
2750 """ 2751 If you have a table (db.mytable) that needs full revision history you can just do:: 2752 2753 form=crud.update(db.mytable,myrecord,onaccept=crud.archive) 2754 2755 crud.archive will define a new table "mytable_archive" and store the 2756 previous record in the newly created table including a reference 2757 to the current record. 2758 2759 If you want to access such table you need to define it yourself in a model:: 2760 2761 db.define_table('mytable_archive', 2762 Field('current_record',db.mytable), 2763 db.mytable) 2764 2765 Notice such table includes all fields of db.mytable plus one: current_record. 2766 crud.archive does not timestamp the stored record unless your original table 2767 has a fields like:: 2768 2769 db.define_table(..., 2770 Field('saved_on','datetime', 2771 default=request.now,update=request.now,writable=False), 2772 Field('saved_by',auth.user, 2773 default=auth.user_id,update=auth.user_id,writable=False), 2774 2775 there is nothing special about these fields since they are filled before 2776 the record is archived. 2777 2778 If you want to change the archive table name and the name of the reference field 2779 you can do, for example:: 2780 2781 db.define_table('myhistory', 2782 Field('parent_record',db.mytable), 2783 db.mytable) 2784 2785 and use it as:: 2786 2787 form=crud.update(db.mytable,myrecord, 2788 onaccept=lambda form:crud.archive(form, 2789 archive_table=db.myhistory, 2790 current_record='parent_record')) 2791 2792 """ 2793 old_record = form.record 2794 if not old_record: 2795 return None 2796 table = form.table 2797 if not archive_table: 2798 archive_table_name = '%s_archive' % table 2799 if archive_table_name in table._db: 2800 archive_table = table._db[archive_table_name] 2801 else: 2802 archive_table = table._db.define_table(archive_table_name, 2803 Field(current_record,table), 2804 table) 2805 new_record = {current_record:old_record.id} 2806 for fieldname in archive_table.fields: 2807 if not fieldname in ['id',current_record] and fieldname in old_record: 2808 new_record[fieldname]=old_record[fieldname] 2809 id = archive_table.insert(**new_record) 2810 return id
2811
2812 - def update( 2813 self, 2814 table, 2815 record, 2816 next=DEFAULT, 2817 onvalidation=DEFAULT, 2818 onaccept=DEFAULT, 2819 ondelete=DEFAULT, 2820 log=DEFAULT, 2821 message=DEFAULT, 2822 deletable=DEFAULT, 2823 formname=DEFAULT, 2824 ):
2825 """ 2826 .. method:: Crud.update(table, record, [next=DEFAULT 2827 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT 2828 [, message=DEFAULT[, deletable=DEFAULT]]]]]]) 2829 2830 """ 2831 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 2832 or (isinstance(record, str) and not str(record).isdigit()): 2833 raise HTTP(404) 2834 if not isinstance(table, self.db.Table): 2835 table = self.db[table] 2836 try: 2837 record_id = record.id 2838 except: 2839 record_id = record or 0 2840 if record_id and not self.has_permission('update', table, record_id): 2841 redirect(self.settings.auth.settings.on_failed_authorization) 2842 if not record_id \ 2843 and not self.has_permission('create', table, record_id): 2844 redirect(self.settings.auth.settings.on_failed_authorization) 2845 2846 request = self.environment.request 2847 response = self.environment.response 2848 session = self.environment.session 2849 if request.extension == 'json' and request.vars.json: 2850 request.vars.update(simplejson.loads(request.vars.json)) 2851 if next == DEFAULT: 2852 next = request.get_vars._next \ 2853 or request.post_vars._next \ 2854 or self.settings.update_next 2855 if onvalidation == DEFAULT: 2856 onvalidation = self.settings.update_onvalidation 2857 if onaccept == DEFAULT: 2858 onaccept = self.settings.update_onaccept 2859 if ondelete == DEFAULT: 2860 ondelete = self.settings.update_ondelete 2861 if log == DEFAULT: 2862 log = self.messages.update_log 2863 if deletable == DEFAULT: 2864 deletable = self.settings.update_deletable 2865 if message == DEFAULT: 2866 message = self.messages.record_updated 2867 form = SQLFORM( 2868 table, 2869 record, 2870 hidden=dict(_next=next), 2871 showid=self.settings.showid, 2872 submit_button=self.messages.submit_button, 2873 delete_label=self.messages.delete_label, 2874 deletable=deletable, 2875 upload=self.settings.download_url, 2876 formstyle=self.settings.formstyle 2877 ) 2878 self.accepted = False 2879 self.deleted = False 2880 captcha = self.settings.update_captcha or \ 2881 self.settings.captcha 2882 if record and captcha: 2883 addrow(form, captcha.label, captcha, captcha.comment, 2884 self.settings.formstyle,'captcha__row') 2885 captcha = self.settings.create_captcha or \ 2886 self.settings.captcha 2887 if not record and captcha: 2888 addrow(form, captcha.label, captcha, captcha.comment, 2889 self.settings.formstyle,'captcha__row') 2890 if not request.extension in ('html','load'): 2891 (_session, _formname) = (None, None) 2892 else: 2893 (_session, _formname) = \ 2894 (session, '%s/%s' % (table._tablename, form.record_id)) 2895 if formname!=DEFAULT: 2896 _formname = formname 2897 keepvalues = self.settings.keepvalues 2898 if request.vars.delete_this_record: 2899 keepvalues = False 2900 if isinstance(onvalidation,StorageList): 2901 onvalidation=onvalidation.get(table._tablename, []) 2902 if form.accepts(request, _session, formname=_formname, 2903 onvalidation=onvalidation, keepvalues=keepvalues, 2904 hideerror=self.settings.hideerror, 2905 detect_record_change = self.settings.detect_record_change): 2906 self.accepted = True 2907 response.flash = message 2908 if log: 2909 self.log_event(log % form.vars) 2910 if request.vars.delete_this_record: 2911 self.deleted = True 2912 message = self.messages.record_deleted 2913 callback(ondelete,form,table._tablename) 2914 response.flash = message 2915 callback(onaccept,form,table._tablename) 2916 if not request.extension in ('html','load'): 2917 raise HTTP(200, 'RECORD CREATED/UPDATED') 2918 if isinstance(next, (list, tuple)): ### fix issue with 2.6 2919 next = next[0] 2920 if next: # Only redirect when explicit 2921 if next[0] != '/' and next[:4] != 'http': 2922 next = URL(r=request, 2923 f=next.replace('[id]', str(form.vars.id))) 2924 session.flash = response.flash 2925 redirect(next) 2926 elif not request.extension in ('html','load'): 2927 raise HTTP(401) 2928 return form
2929
2930 - def create( 2931 self, 2932 table, 2933 next=DEFAULT, 2934 onvalidation=DEFAULT, 2935 onaccept=DEFAULT, 2936 log=DEFAULT, 2937 message=DEFAULT, 2938 formname=DEFAULT, 2939 ):
2940 """ 2941 .. method:: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT 2942 [, onaccept=DEFAULT [, log=DEFAULT[, message=DEFAULT]]]]]) 2943 """ 2944 2945 if next == DEFAULT: 2946 next = self.settings.create_next 2947 if onvalidation == DEFAULT: 2948 onvalidation = self.settings.create_onvalidation 2949 if onaccept == DEFAULT: 2950 onaccept = self.settings.create_onaccept 2951 if log == DEFAULT: 2952 log = self.messages.create_log 2953 if message == DEFAULT: 2954 message = self.messages.record_created 2955 return self.update( 2956 table, 2957 None, 2958 next=next, 2959 onvalidation=onvalidation, 2960 onaccept=onaccept, 2961 log=log, 2962 message=message, 2963 deletable=False, 2964 formname=formname, 2965 )
2966
2967 - def read(self, table, record):
2968 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 2969 or (isinstance(record, str) and not str(record).isdigit()): 2970 raise HTTP(404) 2971 if not isinstance(table, self.db.Table): 2972 table = self.db[table] 2973 if not self.has_permission('read', table, record): 2974 redirect(self.settings.auth.settings.on_failed_authorization) 2975 form = SQLFORM( 2976 table, 2977 record, 2978 readonly=True, 2979 comments=False, 2980 upload=self.settings.download_url, 2981 showid=self.settings.showid, 2982 formstyle=self.settings.formstyle 2983 ) 2984 if not self.environment.request.extension in ('html','load'): 2985 return table._filter_fields(form.record, id=True) 2986 return form
2987
2988 - def delete( 2989 self, 2990 table, 2991 record_id, 2992 next=DEFAULT, 2993 message=DEFAULT, 2994 ):
2995 """ 2996 .. method:: Crud.delete(table, record_id, [next=DEFAULT 2997 [, message=DEFAULT]]) 2998 """ 2999 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 3000 or not str(record_id).isdigit(): 3001 raise HTTP(404) 3002 if not isinstance(table, self.db.Table): 3003 table = self.db[table] 3004 if not self.has_permission('delete', table, record_id): 3005 redirect(self.settings.auth.settings.on_failed_authorization) 3006 request = self.environment.request 3007 session = self.environment.session 3008 if next == DEFAULT: 3009 next = request.get_vars._next \ 3010 or request.post_vars._next \ 3011 or self.settings.delete_next 3012 if message == DEFAULT: 3013 message = self.messages.record_deleted 3014 record = table[record_id] 3015 if record: 3016 callback(self.settings.delete_onvalidation,record) 3017 del table[record_id] 3018 callback(self.settings.delete_onaccept,record,table._tablename) 3019 session.flash = message 3020 if next: # Only redirect when explicit 3021 redirect(next)
3022
3023 - def select( 3024 self, 3025 table, 3026 query=None, 3027 fields=None, 3028 orderby=None, 3029 limitby=None, 3030 headers={}, 3031 **attr 3032 ):
3033 request = self.environment.request 3034 if not (isinstance(table, self.db.Table) or table in self.db.tables): 3035 raise HTTP(404) 3036 if not self.has_permission('select', table): 3037 redirect(self.settings.auth.settings.on_failed_authorization) 3038 #if record_id and not self.has_permission('select', table): 3039 # redirect(self.settings.auth.settings.on_failed_authorization) 3040 if not isinstance(table, self.db.Table): 3041 table = self.db[table] 3042 if not query: 3043 query = table.id > 0 3044 if not fields: 3045 fields = [field for field in table if field.readable] 3046 rows = self.db(query).select(*fields, **dict(orderby=orderby, 3047 limitby=limitby)) 3048 if not rows: 3049 return None # Nicer than an empty table. 3050 if not 'upload' in attr: 3051 attr['upload'] = self.url('download') 3052 if not request.extension in ('html','load'): 3053 return rows.as_list() 3054 if not headers: 3055 headers = dict((str(k),k.label) for k in table) 3056 return SQLTABLE(rows, headers=headers, **attr)
3057
3058 - def get_format(self, field):
3059 rtable = field._db[field.type[10:]] 3060 format = rtable.get('_format', None) 3061 if format and isinstance(format, str): 3062 return format[2:-2] 3063 return field.name
3064
3065 - def get_query(self, field, op, value, refsearch=False):
3066 try: 3067 if refsearch: format = self.get_format(field) 3068 if op == 'equals': 3069 if not refsearch: 3070 return field == value 3071 else: 3072 return lambda row: row[field.name][format] == value 3073 elif op == 'not equal': 3074 if not refsearch: 3075 return field != value 3076 else: 3077 return lambda row: row[field.name][format] != value 3078 elif op == 'greater than': 3079 if not refsearch: 3080 return field > value 3081 else: 3082 return lambda row: row[field.name][format] > value 3083 elif op == 'less than': 3084 if not refsearch: 3085 return field < value 3086 else: 3087 return lambda row: row[field.name][format] < value 3088 elif op == 'starts with': 3089 if not refsearch: 3090 return field.like(value+'%') 3091 else: 3092 return lambda row: str(row[field.name][format]).startswith(value) 3093 elif op == 'ends with': 3094 if not refsearch: 3095 return field.like('%'+value) 3096 else: 3097 return lambda row: str(row[field.name][format]).endswith(value) 3098 elif op == 'contains': 3099 if not refsearch: 3100 return field.like('%'+value+'%') 3101 else: 3102 return lambda row: value in row[field.name][format] 3103 except: 3104 return None
3105 3106
3107 - def search(self, *tables, **args):
3108 """ 3109 Creates a search form and its results for a table 3110 Example usage: 3111 form, results = crud.search(db.test, 3112 queries = ['equals', 'not equal', 'contains'], 3113 query_labels={'equals':'Equals', 3114 'not equal':'Not equal'}, 3115 fields = [db.test.id, db.test.children], 3116 field_labels = {'id':'ID','children':'Children'}, 3117 zero='Please choose', 3118 query = (db.test.id > 0)&(db.test.id != 3) ) 3119 """ 3120 table = tables[0] 3121 fields = args.get('fields', table.fields) 3122 request = self.environment.request 3123 db = self.db 3124 if not (isinstance(table, db.Table) or table in db.tables): 3125 raise HTTP(404) 3126 tbl = TABLE() 3127 selected = []; refsearch = []; results = [] 3128 ops = args.get('queries', []) 3129 zero = args.get('zero', '') 3130 if not ops: 3131 ops = ['equals', 'not equal', 'greater than', 3132 'less than', 'starts with', 3133 'ends with', 'contains'] 3134 ops.insert(0,zero) 3135 query_labels = args.get('query_labels', {}) 3136 query = args.get('query',table.id > 0) 3137 field_labels = args.get('field_labels',{}) 3138 for field in fields: 3139 field = table[field] 3140 if not field.readable: continue 3141 fieldname = field.name 3142 chkval = request.vars.get('chk' + fieldname, None) 3143 txtval = request.vars.get('txt' + fieldname, None) 3144 opval = request.vars.get('op' + fieldname, None) 3145 row = TR(TD(INPUT(_type = "checkbox", _name = "chk" + fieldname, 3146 _disabled = (field.type == 'id'), 3147 value = (field.type == 'id' or chkval == 'on'))), 3148 TD(field_labels.get(fieldname,field.label)), 3149 TD(SELECT([OPTION(query_labels.get(op,op), 3150 _value=op) for op in ops], 3151 _name = "op" + fieldname, 3152 value = opval)), 3153 TD(INPUT(_type = "text", _name = "txt" + fieldname, 3154 _value = txtval, _id='txt' + fieldname, 3155 _class = str(field.type)))) 3156 tbl.append(row) 3157 if request.post_vars and (chkval or field.type=='id'): 3158 if txtval and opval != '': 3159 if field.type[0:10] == 'reference ': 3160 refsearch.append(self.get_query(field, 3161 opval, txtval, refsearch=True)) 3162 else: 3163 value, error = field.validate(txtval) 3164 if not error: 3165 ### TODO deal with 'starts with', 'ends with', 'contains' on GAE 3166 query &= self.get_query(field, opval, value) 3167 else: 3168 row[3].append(DIV(error,_class='error')) 3169 selected.append(field) 3170 form = FORM(tbl,INPUT(_type="submit")) 3171 if selected: 3172 try: 3173 results = db(query).select(*selected) 3174 for r in refsearch: 3175 results = results.find(r) 3176 except: # hmmm, we should do better here 3177 results = None 3178 return form, results
3179 3180 3181 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor())) 3182
3183 -def fetch(url, data=None, headers={}, 3184 cookie=Cookie.SimpleCookie(), 3185 user_agent='Mozilla/5.0'):
3186 if data != None: 3187 data = urllib.urlencode(data) 3188 if user_agent: headers['User-agent'] = user_agent 3189 headers['Cookie'] = ' '.join(['%s=%s;'%(c.key,c.value) for c in cookie.values()]) 3190 try: 3191 from google.appengine.api import urlfetch 3192 except ImportError: 3193 req = urllib2.Request(url, data, headers) 3194 html = urllib2.urlopen(req).read() 3195 else: 3196 method = ((data==None) and urlfetch.GET) or urlfetch.POST 3197 while url is not None: 3198 response = urlfetch.fetch(url=url, payload=data, 3199 method=method, headers=headers, 3200 allow_truncated=False,follow_redirects=False, 3201 deadline=10) 3202 # next request will be a get, so no need to send the data again 3203 data = None 3204 method = urlfetch.GET 3205 # load cookies from the response 3206 cookie.load(response.headers.get('set-cookie', '')) 3207 url = response.headers.get('location') 3208 html = response.content 3209 return html
3210 3211 regex_geocode = \ 3212 re.compile('\<coordinates\>(?P<la>[^,]*),(?P<lo>[^,]*).*?\</coordinates\>') 3213 3214
3215 -def geocode(address):
3216 try: 3217 a = urllib.quote(address) 3218 txt = fetch('http://maps.google.com/maps/geo?q=%s&output=xml' 3219 % a) 3220 item = regex_geocode.search(txt) 3221 (la, lo) = (float(item.group('la')), float(item.group('lo'))) 3222 return (la, lo) 3223 except: 3224 return (0.0, 0.0)
3225 3226
3227 -def universal_caller(f, *a, **b):
3228 c = f.func_code.co_argcount 3229 n = f.func_code.co_varnames[:c] 3230 b = dict([(k, v) for k, v in b.items() if k in n]) 3231 if len(b) == c: 3232 return f(**b) 3233 elif len(a) >= c: 3234 return f(*a[:c]) 3235 raise HTTP(404, "Object does not exist")
3236 3237
3238 -class Service(object):
3239
3240 - def __init__(self, environment):
3241 self.environment = environment 3242 self.run_procedures = {} 3243 self.csv_procedures = {} 3244 self.xml_procedures = {} 3245 self.rss_procedures = {} 3246 self.json_procedures = {} 3247 self.jsonrpc_procedures = {} 3248 self.xmlrpc_procedures = {} 3249 self.amfrpc_procedures = {} 3250 self.amfrpc3_procedures = {} 3251 self.soap_procedures = {}
3252
3253 - def run(self, f):
3254 """ 3255 example:: 3256 3257 service = Service(globals()) 3258 @service.run 3259 def myfunction(a, b): 3260 return a + b 3261 def call(): 3262 return service() 3263 3264 Then call it with:: 3265 3266 wget http://..../app/default/call/run/myfunction?a=3&b=4 3267 3268 """ 3269 self.run_procedures[f.__name__] = f 3270 return f
3271
3272 - def csv(self, f):
3273 """ 3274 example:: 3275 3276 service = Service(globals()) 3277 @service.csv 3278 def myfunction(a, b): 3279 return a + b 3280 def call(): 3281 return service() 3282 3283 Then call it with:: 3284 3285 wget http://..../app/default/call/csv/myfunction?a=3&b=4 3286 3287 """ 3288 self.run_procedures[f.__name__] = f 3289 return f
3290
3291 - def xml(self, f):
3292 """ 3293 example:: 3294 3295 service = Service(globals()) 3296 @service.xml 3297 def myfunction(a, b): 3298 return a + b 3299 def call(): 3300 return service() 3301 3302 Then call it with:: 3303 3304 wget http://..../app/default/call/xml/myfunction?a=3&b=4 3305 3306 """ 3307 self.run_procedures[f.__name__] = f 3308 return f
3309
3310 - def rss(self, f):
3311 """ 3312 example:: 3313 3314 service = Service(globals()) 3315 @service.rss 3316 def myfunction(): 3317 return dict(title=..., link=..., description=..., 3318 created_on=..., entries=[dict(title=..., link=..., 3319 description=..., created_on=...]) 3320 def call(): 3321 return service() 3322 3323 Then call it with:: 3324 3325 wget http://..../app/default/call/rss/myfunction 3326 3327 """ 3328 self.rss_procedures[f.__name__] = f 3329 return f
3330
3331 - def json(self, f):
3332 """ 3333 example:: 3334 3335 service = Service(globals()) 3336 @service.json 3337 def myfunction(a, b): 3338 return [{a: b}] 3339 def call(): 3340 return service() 3341 3342 Then call it with:: 3343 3344 wget http://..../app/default/call/json/myfunction?a=hello&b=world 3345 3346 """ 3347 self.json_procedures[f.__name__] = f 3348 return f
3349
3350 - def jsonrpc(self, f):
3351 """ 3352 example:: 3353 3354 service = Service(globals()) 3355 @service.jsonrpc 3356 def myfunction(a, b): 3357 return a + b 3358 def call(): 3359 return service() 3360 3361 Then call it with:: 3362 3363 wget http://..../app/default/call/jsonrpc/myfunction?a=hello&b=world 3364 3365 """ 3366 self.jsonrpc_procedures[f.__name__] = f 3367 return f
3368
3369 - def xmlrpc(self, f):
3370 """ 3371 example:: 3372 3373 service = Service(globals()) 3374 @service.xmlrpc 3375 def myfunction(a, b): 3376 return a + b 3377 def call(): 3378 return service() 3379 3380 The call it with:: 3381 3382 wget http://..../app/default/call/xmlrpc/myfunction?a=hello&b=world 3383 3384 """ 3385 self.xmlrpc_procedures[f.__name__] = f 3386 return f
3387
3388 - def amfrpc(self, f):
3389 """ 3390 example:: 3391 3392 service = Service(globals()) 3393 @service.amfrpc 3394 def myfunction(a, b): 3395 return a + b 3396 def call(): 3397 return service() 3398 3399 The call it with:: 3400 3401 wget http://..../app/default/call/amfrpc/myfunction?a=hello&b=world 3402 3403 """ 3404 self.amfrpc_procedures[f.__name__] = f 3405 return f
3406
3407 - def amfrpc3(self, domain='default'):
3408 """ 3409 example:: 3410 3411 service = Service(globals()) 3412 @service.amfrpc3('domain') 3413 def myfunction(a, b): 3414 return a + b 3415 def call(): 3416 return service() 3417 3418 The call it with:: 3419 3420 wget http://..../app/default/call/amfrpc3/myfunction?a=hello&b=world 3421 3422 """ 3423 if not isinstance(domain, str): 3424 raise SyntaxError, "AMF3 requires a domain for function" 3425 3426 def _amfrpc3(f): 3427 if domain: 3428 self.amfrpc3_procedures[domain+'.'+f.__name__] = f 3429 else: 3430 self.amfrpc3_procedures[f.__name__] = f 3431 return f
3432 return _amfrpc3
3433
3434 - def soap(self, name=None, returns=None, args=None,doc=None):
3435 """ 3436 example:: 3437 3438 service = Service(globals()) 3439 @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,}) 3440 def myfunction(a, b): 3441 return a + b 3442 def call(): 3443 return service() 3444 3445 The call it with:: 3446 3447 from gluon.contrib.pysimplesoap.client import SoapClient 3448 client = SoapClient(wsdl="http://..../app/default/call/soap?WSDL") 3449 response = client.MyFunction(a=1,b=2) 3450 return response['result'] 3451 3452 Exposes online generated documentation and xml example messages at: 3453 - http://..../app/default/call/soap 3454 """ 3455 3456 def _soap(f): 3457 self.soap_procedures[name or f.__name__] = f, returns, args, doc 3458 return f
3459 return _soap 3460
3461 - def serve_run(self, args=None):
3462 request = self.environment['request'] 3463 if not args: 3464 args = request.args 3465 if args and args[0] in self.run_procedures: 3466 return str(universal_caller(self.run_procedures[args[0]], 3467 *args[1:], **dict(request.vars))) 3468 self.error()
3469
3470 - def serve_csv(self, args=None):
3471 request = self.environment['request'] 3472 response = self.environment['response'] 3473 response.headers['Content-Type'] = 'text/x-csv' 3474 if not args: 3475 args = request.args 3476 3477 def none_exception(value): 3478 if isinstance(value, unicode): 3479 return value.encode('utf8') 3480 if hasattr(value, 'isoformat'): 3481 return value.isoformat()[:19].replace('T', ' ') 3482 if value == None: 3483 return '<NULL>' 3484 return value
3485 if args and args[0] in self.run_procedures: 3486 r = universal_caller(self.run_procedures[args[0]], 3487 *args[1:], **dict(request.vars)) 3488 s = cStringIO.StringIO() 3489 if hasattr(r, 'export_to_csv_file'): 3490 r.export_to_csv_file(s) 3491 elif r and isinstance(r[0], (dict, Storage)): 3492 import csv 3493 writer = csv.writer(s) 3494 writer.writerow(r[0].keys()) 3495 for line in r: 3496 writer.writerow([none_exception(v) \ 3497 for v in line.values()]) 3498 else: 3499 import csv 3500 writer = csv.writer(s) 3501 for line in r: 3502 writer.writerow(line) 3503 return s.getvalue() 3504 self.error() 3505
3506 - def serve_xml(self, args=None):
3507 request = self.environment['request'] 3508 response = self.environment['response'] 3509 response.headers['Content-Type'] = 'text/xml' 3510 if not args: 3511 args = request.args 3512 if args and args[0] in self.run_procedures: 3513 s = universal_caller(self.run_procedures[args[0]], 3514 *args[1:], **dict(request.vars)) 3515 if hasattr(s, 'as_list'): 3516 s = s.as_list() 3517 return serializers.xml(s) 3518 self.error()
3519
3520 - def serve_rss(self, args=None):
3521 request = self.environment['request'] 3522 response = self.environment['response'] 3523 if not args: 3524 args = request.args 3525 if args and args[0] in self.rss_procedures: 3526 feed = universal_caller(self.rss_procedures[args[0]], 3527 *args[1:], **dict(request.vars)) 3528 else: 3529 self.error() 3530 response.headers['Content-Type'] = 'application/rss+xml' 3531 return serializers.rss(feed)
3532
3533 - def serve_json(self, args=None):
3534 request = self.environment['request'] 3535 response = self.environment['response'] 3536 response.headers['Content-Type'] = 'text/x-json' 3537 if not args: 3538 args = request.args 3539 d = dict(request.vars) 3540 if args and args[0] in self.json_procedures: 3541 s = universal_caller(self.json_procedures[args[0]],*args[1:],**d) 3542 if hasattr(s, 'as_list'): 3543 s = s.as_list() 3544 return response.json(s) 3545 self.error()
3546
3547 - class JsonRpcException(Exception):
3548 - def __init__(self,code,info):
3549 self.code,self.info = code,info
3550
3551 - def serve_jsonrpc(self):
3552 import contrib.simplejson as simplejson 3553 def return_response(id, result): 3554 return simplejson.dumps({'version': '1.1', 3555 'id': id, 'result': result, 'error': None})
3556 3557 def return_error(id, code, message): 3558 return simplejson.dumps({'id': id, 3559 'version': '1.1', 3560 'error': {'name': 'JSONRPCError', 3561 'code': code, 'message': message} 3562 }) 3563 3564 request = self.environment['request'] 3565 methods = self.jsonrpc_procedures 3566 data = simplejson.loads(request.body.read()) 3567 id, method, params = data['id'], data['method'], data.get('params','') 3568 if not method in methods: 3569 return return_error(id, 100, 'method "%s" does not exist' % method) 3570 try: 3571 s = methods[method](*params) 3572 if hasattr(s, 'as_list'): 3573 s = s.as_list() 3574 return return_response(id, s) 3575 except Service.JsonRpcException, e: 3576 return return_error(id, e.code, e.info) 3577 except BaseException: 3578 etype, eval, etb = sys.exc_info() 3579 return return_error(id, 100, '%s: %s' % (etype.__name__, eval)) 3580 except: 3581 etype, eval, etb = sys.exc_info() 3582 return return_error(id, 100, 'Exception %s: %s' % (etype, eval)) 3583
3584 - def serve_xmlrpc(self):
3585 request = self.environment['request'] 3586 response = self.environment['response'] 3587 services = self.xmlrpc_procedures.values() 3588 return response.xmlrpc(request, services)
3589
3590 - def serve_amfrpc(self, version=0):
3591 try: 3592 import pyamf 3593 import pyamf.remoting.gateway 3594 except: 3595 return "pyamf not installed or not in Python sys.path" 3596 request = self.environment['request'] 3597 response = self.environment['response'] 3598 if version == 3: 3599 services = self.amfrpc3_procedures 3600 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 3601 pyamf_request = pyamf.remoting.decode(request.body) 3602 else: 3603 services = self.amfrpc_procedures 3604 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 3605 context = pyamf.get_context(pyamf.AMF0) 3606 pyamf_request = pyamf.remoting.decode(request.body, context) 3607 pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion, 3608 pyamf_request.clientType) 3609 for name, message in pyamf_request: 3610 pyamf_response[name] = base_gateway.getProcessor(message)(message) 3611 response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE 3612 if version==3: 3613 return pyamf.remoting.encode(pyamf_response).getvalue() 3614 else: 3615 return pyamf.remoting.encode(pyamf_response, context).getvalue()
3616
3617 - def serve_soap(self, version="1.1"):
3618 try: 3619 from contrib.pysimplesoap.server import SoapDispatcher 3620 except: 3621 return "pysimplesoap not installed in contrib" 3622 request = self.environment['request'] 3623 response = self.environment['response'] 3624 procedures = self.soap_procedures 3625 3626 location = "%s://%s%s" % ( 3627 request.env.wsgi_url_scheme, 3628 request.env.http_host, 3629 URL(r=request,f="call/soap",vars={})) 3630 namespace = 'namespace' in response and response.namespace or location 3631 documentation = response.description or '' 3632 dispatcher = SoapDispatcher( 3633 name = response.title, 3634 location = location, 3635 action = location, # SOAPAction 3636 namespace = namespace, 3637 prefix='pys', 3638 documentation = documentation, 3639 ns = True) 3640 for method, (function, returns, args, doc) in procedures.items(): 3641 dispatcher.register_function(method, function, returns, args, doc) 3642 if request.env.request_method == 'POST': 3643 # Process normal Soap Operation 3644 response.headers['Content-Type'] = 'text/xml' 3645 return dispatcher.dispatch(request.body.read()) 3646 elif 'WSDL' in request.vars: 3647 # Return Web Service Description 3648 response.headers['Content-Type'] = 'text/xml' 3649 return dispatcher.wsdl() 3650 elif 'op' in request.vars: 3651 # Return method help webpage 3652 response.headers['Content-Type'] = 'text/html' 3653 method = request.vars['op'] 3654 sample_req_xml, sample_res_xml, doc = dispatcher.help(method) 3655 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 3656 A("See all webservice operations", 3657 _href=URL(r=request,f="call/soap",vars={})), 3658 H2(method), 3659 P(doc), 3660 UL(LI("Location: %s" % dispatcher.location), 3661 LI("Namespace: %s" % dispatcher.namespace), 3662 LI("SoapAction: %s" % dispatcher.action), 3663 ), 3664 H3("Sample SOAP XML Request Message:"), 3665 CODE(sample_req_xml,language="xml"), 3666 H3("Sample SOAP XML Response Message:"), 3667 CODE(sample_res_xml,language="xml"), 3668 ] 3669 return {'body': body} 3670 else: 3671 # Return general help and method list webpage 3672 response.headers['Content-Type'] = 'text/html' 3673 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 3674 P(response.description), 3675 P("The following operations are available"), 3676 A("See WSDL for webservice description", 3677 _href=URL(r=request,f="call/soap",vars={"WSDL":None})), 3678 UL([LI(A("%s: %s" % (method, doc or ''), 3679 _href=URL(r=request,f="call/soap",vars={'op': method}))) 3680 for method, doc in dispatcher.list_methods()]), 3681 ] 3682 return {'body': body}
3683
3684 - def __call__(self):
3685 """ 3686 register services with: 3687 service = Service(globals()) 3688 @service.run 3689 @service.rss 3690 @service.json 3691 @service.jsonrpc 3692 @service.xmlrpc 3693 @service.jsonrpc 3694 @service.amfrpc 3695 @service.amfrpc3('domain') 3696 @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,}) 3697 3698 expose services with 3699 3700 def call(): return service() 3701 3702 call services with 3703 http://..../app/default/call/run?[parameters] 3704 http://..../app/default/call/rss?[parameters] 3705 http://..../app/default/call/json?[parameters] 3706 http://..../app/default/call/jsonrpc 3707 http://..../app/default/call/xmlrpc 3708 http://..../app/default/call/amfrpc 3709 http://..../app/default/call/amfrpc3 3710 http://..../app/default/call/soap 3711 """ 3712 3713 request = self.environment['request'] 3714 if len(request.args) < 1: 3715 raise HTTP(404, "Not Found") 3716 arg0 = request.args(0) 3717 if arg0 == 'run': 3718 return self.serve_run(request.args[1:]) 3719 elif arg0 == 'rss': 3720 return self.serve_rss(request.args[1:]) 3721 elif arg0 == 'csv': 3722 return self.serve_csv(request.args[1:]) 3723 elif arg0 == 'xml': 3724 return self.serve_xml(request.args[1:]) 3725 elif arg0 == 'json': 3726 return self.serve_json(request.args[1:]) 3727 elif arg0 == 'jsonrpc': 3728 return self.serve_jsonrpc() 3729 elif arg0 == 'xmlrpc': 3730 return self.serve_xmlrpc() 3731 elif arg0 == 'amfrpc': 3732 return self.serve_amfrpc() 3733 elif arg0 == 'amfrpc3': 3734 return self.serve_amfrpc(3) 3735 elif arg0 == 'soap': 3736 return self.serve_soap() 3737 else: 3738 self.error()
3739
3740 - def error(self):
3741 raise HTTP(404, "Object does not exist")
3742 3743
3744 -def completion(callback):
3745 """ 3746 Executes a task on completion of the called action. For example: 3747 3748 from gluon.tools import completion 3749 @completion(lambda d: logging.info(repr(d))) 3750 def index(): 3751 return dict(message='hello') 3752 3753 It logs the output of the function every time input is called. 3754 The argument of completion is executed in a new thread. 3755 """ 3756 def _completion(f): 3757 def __completion(*a,**b): 3758 d = None 3759 try: 3760 d = f(*a,**b) 3761 return d 3762 finally: 3763 thread.start_new_thread(callback,(d,))
3764 return __completion 3765 return _completion 3766
3767 -def prettydate(d,T=lambda x:x):
3768 try: 3769 dt = datetime.datetime.now() - d 3770 except: 3771 return '' 3772 if dt.days >= 2*365: 3773 return T('%d years ago') % int(dt.days / 365) 3774 elif dt.days >= 365: 3775 return T('1 year ago') 3776 elif dt.days >= 60: 3777 return T('%d months ago') % int(dt.days / 30) 3778 elif dt.days > 21: 3779 return T('1 month ago') 3780 elif dt.days >= 14: 3781 return T('%d weeks ago') % int(dt.days / 7) 3782 elif dt.days >= 7: 3783 return T('1 week ago') 3784 elif dt.days > 1: 3785 return T('%d days ago') % dt.days 3786 elif dt.days == 1: 3787 return T('1 day ago') 3788 elif dt.seconds >= 2*60*60: 3789 return T('%d hours ago') % int(dt.seconds / 3600) 3790 elif dt.seconds >= 60*60: 3791 return T('1 hour ago') 3792 elif dt.seconds >= 2*60: 3793 return T('%d minutes ago') % int(dt.seconds / 60) 3794 elif dt.seconds >= 60: 3795 return T('1 minute ago') 3796 elif dt.seconds > 1: 3797 return T('%d seconds ago') % dt.seconds 3798 elif dt.seconds == 1: 3799 return T('1 second ago') 3800 else: 3801 return T('now')
3802
3803 -def test_thread_separation():
3804 def f(): 3805 c=PluginManager() 3806 lock1.acquire() 3807 lock2.acquire() 3808 c.x=7 3809 lock1.release() 3810 lock2.release()
3811 lock1=thread.allocate_lock() 3812 lock2=thread.allocate_lock() 3813 lock1.acquire() 3814 thread.start_new_thread(f,()) 3815 a=PluginManager() 3816 a.x=5 3817 lock1.release() 3818 lock2.acquire() 3819 return a.x 3820
3821 -class PluginManager(object):
3822 """ 3823 3824 Plugin Manager is similar to a storage object but it is a single level singleton 3825 this means that multiple instances within the same thread share the same attributes 3826 Its constructor is also special. The first argument is the name of the plugin you are defining. 3827 The named arguments are parameters needed by the plugin with default values. 3828 If the parameters were previous defined, the old values are used. 3829 3830 For example: 3831 3832 ### in some general configuration file: 3833 >>> plugins = PluginManager() 3834 >>> plugins.me.param1=3 3835 3836 ### within the plugin model 3837 >>> _ = PluginManager('me',param1=5,param2=6,param3=7) 3838 3839 ### where the plugin is used 3840 >>> print plugins.me.param1 3841 3 3842 >>> print plugins.me.param2 3843 6 3844 >>> plugins.me.param3 = 8 3845 >>> print plugins.me.param3 3846 8 3847 3848 Here are some tests: 3849 3850 >>> a=PluginManager() 3851 >>> a.x=6 3852 >>> b=PluginManager('check') 3853 >>> print b.x 3854 6 3855 >>> b=PluginManager() # reset settings 3856 >>> print b.x 3857 <Storage {}> 3858 >>> b.x=7 3859 >>> print a.x 3860 7 3861 >>> a.y.z=8 3862 >>> print b.y.z 3863 8 3864 >>> test_thread_separation() 3865 5 3866 >>> plugins=PluginManager('me',db='mydb') 3867 >>> print plugins.me.db 3868 mydb 3869 >>> print 'me' in plugins 3870 True 3871 >>> print plugins.me.installed 3872 True 3873 """ 3874 instances = {}
3875 - def __new__(cls,*a,**b):
3876 id = thread.get_ident() 3877 lock = thread.allocate_lock() 3878 try: 3879 lock.acquire() 3880 try: 3881 return cls.instances[id] 3882 except KeyError: 3883 instance = object.__new__(cls,*a,**b) 3884 cls.instances[id] = instance 3885 return instance 3886 finally: 3887 lock.release()
3888 - def __init__(self,plugin=None,**defaults):
3889 if not plugin: 3890 self.__dict__.clear() 3891 settings = self.__getattr__(plugin) 3892 settings.installed = True 3893 [settings.update({key:value}) for key,value in defaults.items() if not key in settings]
3894 - def __getattr__(self, key):
3895 if not key in self.__dict__: 3896 self.__dict__[key] = Storage() 3897 return self.__dict__[key]
3898 - def keys(self):
3899 return self.__dict__.keys()
3900 - def __contains__(self,key):
3901 return key in self.__dict__
3902 3903 if __name__ == '__main__': 3904 import doctest 3905 doctest.testmod() 3906