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)\ 810 > request.now: 811 self.user = auth.user 812 auth.last_visit = request.now 813 else: 814 self.user = None 815 session.auth = None 816 self.settings = Settings() 817 818 # ## what happens after login? 819 820 # ## what happens after registration? 821 822 self.settings.hideerror = False 823 self.settings.actions_disabled = [] 824 self.settings.reset_password_requires_verification = False 825 self.settings.registration_requires_verification = False 826 self.settings.registration_requires_approval = False 827 self.settings.alternate_requires_registration = False 828 self.settings.create_user_groups = True 829 830 self.settings.controller = controller 831 self.settings.login_url = self.url('user', args='login') 832 self.settings.logged_url = self.url('user', args='profile') 833 self.settings.download_url = self.url('download') 834 self.settings.mailer = None 835 self.settings.login_captcha = None 836 self.settings.register_captcha = None 837 self.settings.retrieve_username_captcha = None 838 self.settings.retrieve_password_captcha = None 839 self.settings.captcha = None 840 self.settings.expiration = 3600 # one hour 841 self.settings.long_expiration = 3600*30*24 # one month 842 self.settings.remember_me_form = True 843 self.settings.allow_basic_login = False 844 self.settings.allow_basic_login_only = False 845 self.settings.on_failed_authorization = \ 846 self.url('user',args='not_authorized') 847 848 self.settings.on_failed_authentication = lambda x: redirect(x) 849 850 self.settings.formstyle = 'table3cols' 851 852 # ## table names to be used 853 854 self.settings.password_field = 'password' 855 self.settings.table_user_name = 'auth_user' 856 self.settings.table_group_name = 'auth_group' 857 self.settings.table_membership_name = 'auth_membership' 858 self.settings.table_permission_name = 'auth_permission' 859 self.settings.table_event_name = 'auth_event' 860 861 # ## if none, they will be created 862 863 self.settings.table_user = None 864 self.settings.table_group = None 865 self.settings.table_membership = None 866 self.settings.table_permission = None 867 self.settings.table_event = None 868 869 # ## 870 871 self.settings.showid = False 872 873 # ## these should be functions or lambdas 874 875 self.settings.login_next = self.url('index') 876 self.settings.login_onvalidation = [] 877 self.settings.login_onaccept = [] 878 self.settings.login_methods = [self] 879 self.settings.login_form = self 880 self.settings.login_email_validate = True 881 self.settings.login_userfield = None 882 883 self.settings.logout_next = self.url('index') 884 self.settings.logout_onlogout = None 885 886 self.settings.register_next = self.url('index') 887 self.settings.register_onvalidation = [] 888 self.settings.register_onaccept = [] 889 self.settings.register_fields = None 890 891 self.settings.verify_email_next = self.url('user', args='login') 892 self.settings.verify_email_onaccept = [] 893 894 self.settings.profile_next = self.url('index') 895 self.settings.profile_onvalidation = [] 896 self.settings.profile_onaccept = [] 897 self.settings.profile_fields = None 898 self.settings.retrieve_username_next = self.url('index') 899 self.settings.retrieve_password_next = self.url('index') 900 self.settings.request_reset_password_next = self.url('user', args='login') 901 self.settings.reset_password_next = self.url('user', args='login') 902 self.settings.change_password_next = self.url('index') 903 904 self.settings.retrieve_password_onvalidation = [] 905 self.settings.reset_password_onvalidation = [] 906 907 self.settings.hmac_key = None 908 self.settings.lock_keys = True 909 910 911 # ## these are messages that can be customized 912 self.messages = Messages(self.environment.T) 913 self.messages.submit_button = 'Submit' 914 self.messages.verify_password = 'Verify Password' 915 self.messages.delete_label = 'Check to delete:' 916 self.messages.function_disabled = 'Function disabled' 917 self.messages.access_denied = 'Insufficient privileges' 918 self.messages.registration_verifying = 'Registration needs verification' 919 self.messages.registration_pending = 'Registration is pending approval' 920 self.messages.login_disabled = 'Login disabled by administrator' 921 self.messages.logged_in = 'Logged in' 922 self.messages.email_sent = 'Email sent' 923 self.messages.unable_to_send_email = 'Unable to send email' 924 self.messages.email_verified = 'Email verified' 925 self.messages.logged_out = 'Logged out' 926 self.messages.registration_successful = 'Registration successful' 927 self.messages.invalid_email = 'Invalid email' 928 self.messages.unable_send_email = 'Unable to send email' 929 self.messages.invalid_login = 'Invalid login' 930 self.messages.invalid_user = 'Invalid user' 931 self.messages.invalid_password = 'Invalid password' 932 self.messages.is_empty = "Cannot be empty" 933 self.messages.mismatched_password = "Password fields don't match" 934 self.messages.verify_email = \ 935 'Click on the link http://...verify_email/%(key)s to verify your email' 936 self.messages.verify_email_subject = 'Email verification' 937 self.messages.username_sent = 'Your username was emailed to you' 938 self.messages.new_password_sent = 'A new password was emailed to you' 939 self.messages.password_changed = 'Password changed' 940 self.messages.retrieve_username = 'Your username is: %(username)s' 941 self.messages.retrieve_username_subject = 'Username retrieve' 942 self.messages.retrieve_password = 'Your password is: %(password)s' 943 self.messages.retrieve_password_subject = 'Password retrieve' 944 self.messages.reset_password = \ 945 'Click on the link http://...reset_password/%(key)s to reset your password' 946 self.messages.reset_password_subject = 'Password reset' 947 self.messages.invalid_reset_password = 'Invalid reset password' 948 self.messages.profile_updated = 'Profile updated' 949 self.messages.new_password = 'New password' 950 self.messages.old_password = 'Old password' 951 self.messages.group_description = \ 952 'Group uniquely assigned to user %(id)s' 953 954 self.messages.register_log = 'User %(id)s Registered' 955 self.messages.login_log = 'User %(id)s Logged-in' 956 self.messages.login_failed_log = None 957 self.messages.logout_log = 'User %(id)s Logged-out' 958 self.messages.profile_log = 'User %(id)s Profile updated' 959 self.messages.verify_email_log = 'User %(id)s Verification email sent' 960 self.messages.retrieve_username_log = 'User %(id)s Username retrieved' 961 self.messages.retrieve_password_log = 'User %(id)s Password retrieved' 962 self.messages.reset_password_log = 'User %(id)s Password reset' 963 self.messages.change_password_log = 'User %(id)s Password changed' 964 self.messages.add_group_log = 'Group %(group_id)s created' 965 self.messages.del_group_log = 'Group %(group_id)s deleted' 966 self.messages.add_membership_log = None 967 self.messages.del_membership_log = None 968 self.messages.has_membership_log = None 969 self.messages.add_permission_log = None 970 self.messages.del_permission_log = None 971 self.messages.has_permission_log = None 972 self.messages.impersonate_log = 'User %(id)s is impersonating %(other_id)s' 973 974 self.messages.label_first_name = 'First name' 975 self.messages.label_last_name = 'Last name' 976 self.messages.label_username = 'Username' 977 self.messages.label_email = 'E-mail' 978 self.messages.label_password = 'Password' 979 self.messages.label_registration_key = 'Registration key' 980 self.messages.label_reset_password_key = 'Reset Password key' 981 self.messages.label_registration_id = 'Registration identifier' 982 self.messages.label_role = 'Role' 983 self.messages.label_description = 'Description' 984 self.messages.label_user_id = 'User ID' 985 self.messages.label_group_id = 'Group ID' 986 self.messages.label_name = 'Name' 987 self.messages.label_table_name = 'Table name' 988 self.messages.label_record_id = 'Record ID' 989 self.messages.label_time_stamp = 'Timestamp' 990 self.messages.label_client_ip = 'Client IP' 991 self.messages.label_origin = 'Origin' 992 self.messages.label_remember_me = "Remember me (for 30 days)" 993 self.messages['T'] = self.environment.T 994 self.messages.verify_password_comment = 'please input your password again' 995 self.messages.lock_keys = True 996 997 # for "remember me" option 998 response = self.environment.response 999 if auth and auth.remember: #when user wants to be logged in for longer 1000 #import time 1001 #t = time.strftime( 1002 # "%a, %d-%b-%Y %H:%M:%S %Z", 1003 # time.gmtime(time.time() + auth.expiration) # one month longer 1004 #) 1005 # sets for appropriate cookie an appropriate expiration time 1006 response.cookies[response.session_id_name]["expires"] = auth.expiration
1007
1008 - def _get_user_id(self):
1009 "accessor for auth.user_id" 1010 return self.user and self.user.id or None
1011 user_id = property(_get_user_id, doc="user.id or None") 1012
1013 - def _HTTP(self, *a, **b):
1014 """ 1015 only used in lambda: self._HTTP(404) 1016 """ 1017 1018 raise HTTP(*a, **b)
1019
1020 - def __call__(self):
1021 """ 1022 usage: 1023 1024 def authentication(): return dict(form=auth()) 1025 """ 1026 1027 request = self.environment.request 1028 args = request.args 1029 if not args: 1030 redirect(self.url(args='login',vars=request.vars)) 1031 elif args[0] in self.settings.actions_disabled: 1032 raise HTTP(404) 1033 if args[0] == 'login': 1034 return self.login() 1035 elif args[0] == 'logout': 1036 return self.logout() 1037 elif args[0] == 'register': 1038 return self.register() 1039 elif args[0] == 'verify_email': 1040 return self.verify_email() 1041 elif args[0] == 'retrieve_username': 1042 return self.retrieve_username() 1043 elif args[0] == 'retrieve_password': 1044 return self.retrieve_password() 1045 elif args[0] == 'reset_password': 1046 return self.reset_password() 1047 elif args[0] == 'request_reset_password': 1048 return self.request_reset_password() 1049 elif args[0] == 'change_password': 1050 return self.change_password() 1051 elif args[0] == 'profile': 1052 return self.profile() 1053 elif args[0] == 'groups': 1054 return self.groups() 1055 elif args[0] == 'impersonate': 1056 return self.impersonate() 1057 elif args[0] == 'not_authorized': 1058 return self.not_authorized() 1059 else: 1060 raise HTTP(404)
1061
1062 - def navbar(self,prefix='Welcome',action=None):
1063 request = self.environment.request 1064 T = self.environment.T 1065 if isinstance(prefix,str): 1066 prefix = T(prefix) 1067 if not action: 1068 action=URL(request.application,request.controller,'user') 1069 if prefix: 1070 prefix = prefix.strip()+' ' 1071 if self.user_id: 1072 logout=A(T('logout'),_href=action+'/logout') 1073 profile=A(T('profile'),_href=action+'/profile') 1074 password=A(T('password'),_href=action+'/change_password') 1075 bar = SPAN(prefix,self.user.first_name,' [ ', logout, ']',_class='auth_navbar') 1076 if not 'profile' in self.settings.actions_disabled: 1077 bar.insert(4, ' | ') 1078 bar.insert(5, profile) 1079 if not 'change_password' in self.settings.actions_disabled: 1080 bar.insert(-1, ' | ') 1081 bar.insert(-1, password) 1082 else: 1083 login=A(T('login'),_href=action+'/login') 1084 register=A(T('register'),_href=action+'/register') 1085 retrieve_username=A(T('forgot username?'), 1086 _href=action+'/retrieve_username') 1087 lost_password=A(T('lost password?'), 1088 _href=action+'/request_reset_password') 1089 bar = SPAN('[ ',login,' ]',_class='auth_navbar') 1090 1091 if not 'register' in self.settings.actions_disabled: 1092 bar.insert(2, ' | ') 1093 bar.insert(3, register) 1094 if 'username' in self.settings.table_user.fields() and \ 1095 not 'retrieve_username' in self.settings.actions_disabled: 1096 bar.insert(-1, ' | ') 1097 bar.insert(-1, retrieve_username) 1098 if not 'request_reset_password' in self.settings.actions_disabled: 1099 bar.insert(-1, ' | ') 1100 bar.insert(-1, lost_password) 1101 return bar
1102
1103 - def __get_migrate(self, tablename, migrate=True):
1104 1105 if type(migrate).__name__ == 'str': 1106 return (migrate + tablename + '.table') 1107 elif migrate == False: 1108 return False 1109 else: 1110 return True
1111
1112 - def define_tables(self, username=False, migrate=True, fake_migrate=False):
1113 """ 1114 to be called unless tables are defined manually 1115 1116 usages:: 1117 1118 # defines all needed tables and table files 1119 # 'myprefix_auth_user.table', ... 1120 auth.define_tables(migrate='myprefix_') 1121 1122 # defines all needed tables without migration/table files 1123 auth.define_tables(migrate=False) 1124 1125 """ 1126 1127 db = self.db 1128 if not self.settings.table_user_name in db.tables: 1129 passfield = self.settings.password_field 1130 if username: 1131 table = db.define_table( 1132 self.settings.table_user_name, 1133 Field('first_name', length=128, default='', 1134 label=self.messages.label_first_name), 1135 Field('last_name', length=128, default='', 1136 label=self.messages.label_last_name), 1137 Field('username', length=128, default='', 1138 label=self.messages.label_username), 1139 Field('email', length=512, default='', 1140 label=self.messages.label_email), 1141 Field(passfield, 'password', length=512, 1142 readable=False, label=self.messages.label_password), 1143 Field('registration_key', length=512, 1144 writable=False, readable=False, default='', 1145 label=self.messages.label_registration_key), 1146 Field('reset_password_key', length=512, 1147 writable=False, readable=False, default='', 1148 label=self.messages.label_reset_password_key), 1149 Field('registration_id', length=512, 1150 writable=False, readable=False, default='', 1151 label=self.messages.label_registration_id), 1152 migrate=\ 1153 self.__get_migrate(self.settings.table_user_name, migrate), 1154 fake_migrate=fake_migrate, 1155 format='%(username)s') 1156 table.username.requires = IS_NOT_IN_DB(db, table.username) 1157 else: 1158 table = db.define_table( 1159 self.settings.table_user_name, 1160 Field('first_name', length=128, default='', 1161 label=self.messages.label_first_name), 1162 Field('last_name', length=128, default='', 1163 label=self.messages.label_last_name), 1164 Field('email', length=512, default='', 1165 label=self.messages.label_email), 1166 Field(passfield, 'password', length=512, 1167 readable=False, label=self.messages.label_password), 1168 Field('registration_key', length=512, 1169 writable=False, readable=False, default='', 1170 label=self.messages.label_registration_key), 1171 Field('reset_password_key', length=512, 1172 writable=False, readable=False, default='', 1173 label=self.messages.label_reset_password_key), 1174 migrate=\ 1175 self.__get_migrate(self.settings.table_user_name, migrate), 1176 fake_migrate=fake_migrate, 1177 format='%(first_name)s %(last_name)s (%(id)s)') 1178 table.first_name.requires = \ 1179 IS_NOT_EMPTY(error_message=self.messages.is_empty) 1180 table.last_name.requires = \ 1181 IS_NOT_EMPTY(error_message=self.messages.is_empty) 1182 table[passfield].requires = [CRYPT(key=self.settings.hmac_key)] 1183 table.email.requires = \ 1184 [IS_EMAIL(error_message=self.messages.invalid_email), 1185 IS_NOT_IN_DB(db, table.email)] 1186 table.registration_key.default = '' 1187 self.settings.table_user = db[self.settings.table_user_name] 1188 if not self.settings.table_group_name in db.tables: 1189 table = db.define_table( 1190 self.settings.table_group_name, 1191 Field('role', length=512, default='', 1192 label=self.messages.label_role), 1193 Field('description', 'text', 1194 label=self.messages.label_description), 1195 migrate=self.__get_migrate( 1196 self.settings.table_group_name, migrate), 1197 fake_migrate=fake_migrate, 1198 format = '%(role)s (%(id)s)') 1199 table.role.requires = IS_NOT_IN_DB(db, '%s.role' 1200 % self.settings.table_group_name) 1201 self.settings.table_group = db[self.settings.table_group_name] 1202 if not self.settings.table_membership_name in db.tables: 1203 table = db.define_table( 1204 self.settings.table_membership_name, 1205 Field('user_id', self.settings.table_user, 1206 label=self.messages.label_user_id), 1207 Field('group_id', self.settings.table_group, 1208 label=self.messages.label_group_id), 1209 migrate=self.__get_migrate( 1210 self.settings.table_membership_name, migrate), 1211 fake_migrate=fake_migrate) 1212 table.user_id.requires = IS_IN_DB(db, '%s.id' % 1213 self.settings.table_user_name, 1214 '%(first_name)s %(last_name)s (%(id)s)') 1215 table.group_id.requires = IS_IN_DB(db, '%s.id' % 1216 self.settings.table_group_name, 1217 '%(role)s (%(id)s)') 1218 self.settings.table_membership = db[self.settings.table_membership_name] 1219 if not self.settings.table_permission_name in db.tables: 1220 table = db.define_table( 1221 self.settings.table_permission_name, 1222 Field('group_id', self.settings.table_group, 1223 label=self.messages.label_group_id), 1224 Field('name', default='default', length=512, 1225 label=self.messages.label_name), 1226 Field('table_name', length=512, 1227 label=self.messages.label_table_name), 1228 Field('record_id', 'integer', 1229 label=self.messages.label_record_id), 1230 migrate=self.__get_migrate( 1231 self.settings.table_permission_name, migrate), 1232 fake_migrate=fake_migrate) 1233 table.group_id.requires = IS_IN_DB(db, '%s.id' % 1234 self.settings.table_group_name, 1235 '%(role)s (%(id)s)') 1236 table.name.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1237 table.table_name.requires = IS_IN_SET(self.db.tables) 1238 table.record_id.requires = IS_INT_IN_RANGE(0, 10 ** 9) 1239 self.settings.table_permission = db[self.settings.table_permission_name] 1240 if not self.settings.table_event_name in db.tables: 1241 table = db.define_table( 1242 self.settings.table_event_name, 1243 Field('time_stamp', 'datetime', 1244 default=self.environment.request.now, 1245 label=self.messages.label_time_stamp), 1246 Field('client_ip', 1247 default=self.environment.request.client, 1248 label=self.messages.label_client_ip), 1249 Field('user_id', self.settings.table_user, default=None, 1250 label=self.messages.label_user_id), 1251 Field('origin', default='auth', length=512, 1252 label=self.messages.label_origin), 1253 Field('description', 'text', default='', 1254 label=self.messages.label_description), 1255 migrate=self.__get_migrate( 1256 self.settings.table_event_name, migrate), 1257 fake_migrate=fake_migrate) 1258 table.user_id.requires = IS_IN_DB(db, '%s.id' % 1259 self.settings.table_user_name, 1260 '%(first_name)s %(last_name)s (%(id)s)') 1261 table.origin.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1262 table.description.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1263 self.settings.table_event = db[self.settings.table_event_name] 1264 def lazy_user (auth = self): return auth.user_id 1265 now = self.environment.request.now 1266 self.signature = db.Table(self.db,'auth_signature', 1267 Field('is_active','boolean',default=True), 1268 Field('created_on','datetime',default=now, 1269 writable=False,readable=False), 1270 Field('created_by',self.settings.table_user,default=lazy_user, 1271 writable=False,readable=False), 1272 Field('modified_on','datetime',update=now,default=now, 1273 writable=False,readable=False), 1274 Field('modified_by',self.settings.table_user, 1275 default=lazy_user,update=lazy_user, 1276 writable=False,readable=False))
1277 1278
1279 - def log_event(self, description, origin='auth'):
1280 """ 1281 usage:: 1282 1283 auth.log_event(description='this happened', origin='auth') 1284 """ 1285 1286 if self.is_logged_in(): 1287 user_id = self.user.id 1288 else: 1289 user_id = None # user unknown 1290 self.settings.table_event.insert(description=description, 1291 origin=origin, user_id=user_id)
1292
1293 - def get_or_create_user(self, keys):
1294 """ 1295 Used for alternate login methods: 1296 If the user exists already then password is updated. 1297 If the user doesn't yet exist, then they are created. 1298 """ 1299 table_user = self.settings.table_user 1300 if 'registration_id' in table_user.fields() and 'registration_id' in keys: 1301 username = 'registration_id' 1302 elif 'username' in table_user.fields(): 1303 username = 'username' 1304 elif 'email' in table_user.fields(): 1305 username = 'email' 1306 else: 1307 raise SyntaxError, "user must have username or email" 1308 passfield = self.settings.password_field 1309 user = self.db(table_user[username] == keys[username]).select().first() 1310 if user: 1311 if passfield in keys and keys[passfield]: 1312 user.update_record(**{passfield: keys[passfield], 1313 'registration_key': ''}) 1314 else: 1315 d = {username: keys[username], 1316 'first_name': keys.get('first_name', keys[username]), 1317 'last_name': keys.get('last_name', ''), 1318 'registration_key': ''} 1319 keys = dict([(k, v) for (k, v) in keys.items() \ 1320 if k in table_user.fields]) 1321 d.update(keys) 1322 user_id = table_user.insert(**d) 1323 user = self.user = table_user[user_id] 1324 if self.settings.create_user_groups: 1325 group_id = self.add_group("user_%s" % user_id) 1326 self.add_membership(group_id, user_id) 1327 return user
1328
1329 - def basic(self):
1330 if not self.settings.allow_basic_login: 1331 return False 1332 basic = self.environment.request.env.http_authorization 1333 if not basic or not basic[:6].lower() == 'basic ': 1334 return False 1335 (username, password) = base64.b64decode(basic[6:]).split(':') 1336 return self.login_bare(username, password)
1337
1338 - def login_bare(self, username, password):
1339 """ 1340 logins user 1341 """ 1342 1343 request = self.environment.request 1344 session = self.environment.session 1345 table_user = self.settings.table_user 1346 if self.settings.login_userfield: 1347 userfield = self.settings.login_userfield 1348 elif 'username' in table_user.fields: 1349 userfield = 'username' 1350 else: 1351 userfield = 'email' 1352 passfield = self.settings.password_field 1353 user = self.db(table_user[userfield] == username).select().first() 1354 password = table_user[passfield].validate(password)[0] 1355 if user: 1356 if not user.registration_key and user[passfield] == password: 1357 user = Storage(table_user._filter_fields(user, id=True)) 1358 session.auth = Storage(user=user, last_visit=request.now, 1359 expiration=self.settings.expiration) 1360 self.user = user 1361 return user 1362 return False
1363
1364 - def login( 1365 self, 1366 next=DEFAULT, 1367 onvalidation=DEFAULT, 1368 onaccept=DEFAULT, 1369 log=DEFAULT, 1370 ):
1371 """ 1372 returns a login form 1373 1374 .. method:: Auth.login([next=DEFAULT [, onvalidation=DEFAULT 1375 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1376 1377 """ 1378 1379 table_user = self.settings.table_user 1380 if self.settings.login_userfield: 1381 username = self.settings.login_userfield 1382 elif 'username' in table_user.fields: 1383 username = 'username' 1384 else: 1385 username = 'email' 1386 if 'username' in table_user.fields or not self.settings.login_email_validate: 1387 tmpvalidator = IS_NOT_EMPTY(error_message=self.messages.is_empty) 1388 else: 1389 tmpvalidator = IS_EMAIL(error_message=self.messages.invalid_email) 1390 old_requires = table_user[username].requires 1391 table_user[username].requires = tmpvalidator 1392 request = self.environment.request 1393 response = self.environment.response 1394 session = self.environment.session 1395 passfield = self.settings.password_field 1396 if next == DEFAULT: 1397 next = request.get_vars._next \ 1398 or request.post_vars._next \ 1399 or self.settings.login_next 1400 if onvalidation == DEFAULT: 1401 onvalidation = self.settings.login_onvalidation 1402 if onaccept == DEFAULT: 1403 onaccept = self.settings.login_onaccept 1404 if log == DEFAULT: 1405 log = self.messages.login_log 1406 1407 user = None # default 1408 1409 # do we use our own login form, or from a central source? 1410 if self.settings.login_form == self: 1411 form = SQLFORM( 1412 table_user, 1413 fields=[username, passfield], 1414 hidden=dict(_next=next), 1415 showid=self.settings.showid, 1416 submit_button=self.messages.submit_button, 1417 delete_label=self.messages.delete_label, 1418 formstyle=self.settings.formstyle 1419 ) 1420 1421 if self.settings.remember_me_form: 1422 ## adds a new input checkbox "remember me for longer" 1423 addrow(form,XML("&nbsp;"), 1424 DIV(XML("&nbsp;"), 1425 INPUT(_type='checkbox', 1426 _class='checkbox', 1427 _id="auth_user_remember", 1428 _name="remember", 1429 ), 1430 XML("&nbsp;&nbsp;"), 1431 LABEL( 1432 self.messages.label_remember_me, 1433 _for="auth_user_remember", 1434 )),"", 1435 self.settings.formstyle, 1436 'auth_user_remember__row') 1437 1438 captcha = self.settings.login_captcha or \ 1439 (self.settings.login_captcha!=False and self.settings.captcha) 1440 if captcha: 1441 addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') 1442 accepted_form = False 1443 if form.accepts(request, session, 1444 formname='login', dbio=False, 1445 onvalidation=onvalidation, 1446 hideerror=self.settings.hideerror): 1447 accepted_form = True 1448 # check for username in db 1449 user = self.db(table_user[username] == form.vars[username]).select().first() 1450 if user: 1451 # user in db, check if registration pending or disabled 1452 temp_user = user 1453 if temp_user.registration_key == 'pending': 1454 response.flash = self.messages.registration_pending 1455 return form 1456 elif temp_user.registration_key in ('disabled','blocked'): 1457 response.flash = self.messages.login_disabled 1458 return form 1459 elif temp_user.registration_key!=None and temp_user.registration_key.strip(): 1460 response.flash = \ 1461 self.messages.registration_verifying 1462 return form 1463 # try alternate logins 1st as these have the current version of the password 1464 user = None 1465 for login_method in self.settings.login_methods: 1466 if login_method != self and \ 1467 login_method(request.vars[username], 1468 request.vars[passfield]): 1469 if not self in self.settings.login_methods: 1470 # do not store password in db 1471 form.vars[passfield] = None 1472 user = self.get_or_create_user(form.vars) 1473 break 1474 if not user: 1475 # alternates have failed, maybe because service inaccessible 1476 if self.settings.login_methods[0] == self: 1477 # try logging in locally using cached credentials 1478 if temp_user[passfield] == form.vars.get(passfield, ''): 1479 # success 1480 user = temp_user 1481 else: 1482 # user not in db 1483 if not self.settings.alternate_requires_registration: 1484 # we're allowed to auto-register users from external systems 1485 for login_method in self.settings.login_methods: 1486 if login_method != self and \ 1487 login_method(request.vars[username], 1488 request.vars[passfield]): 1489 if not self in self.settings.login_methods: 1490 # do not store password in db 1491 form.vars[passfield] = None 1492 user = self.get_or_create_user(form.vars) 1493 break 1494 if not user: 1495 if self.settings.login_failed_log: 1496 self.log_event(self.settings.login_failed_log % request.post_vars) 1497 # invalid login 1498 session.flash = self.messages.invalid_login 1499 redirect(self.url(args=request.args,vars=request.get_vars)) 1500 1501 else: 1502 # use a central authentication server 1503 cas = self.settings.login_form 1504 cas_user = cas.get_user() 1505 1506 if cas_user: 1507 cas_user[passfield] = None 1508 user = self.get_or_create_user(cas_user) 1509 elif hasattr(cas,'login_form'): 1510 return cas.login_form() 1511 else: 1512 # we need to pass through login again before going on 1513 next = self.url('user',args='login',vars=dict(_next=next)) 1514 redirect(cas.login_url(next)) 1515 1516 1517 # process authenticated users 1518 if user: 1519 user = Storage(table_user._filter_fields(user, id=True)) 1520 1521 if request.vars.has_key("remember"): 1522 # user wants to be logged in for longer 1523 session.auth = Storage( 1524 user = user, 1525 last_visit = request.now, 1526 expiration = self.settings.long_expiration, 1527 remember = True, 1528 ) 1529 else: 1530 # user doesn't want to be logged in for longer 1531 session.auth = Storage( 1532 user = user, 1533 last_visit = request.now, 1534 expiration = self.settings.expiration, 1535 remember = False, 1536 ) 1537 1538 self.user = user 1539 session.flash = self.messages.logged_in 1540 if log and self.user: 1541 self.log_event(log % self.user) 1542 1543 # how to continue 1544 if self.settings.login_form == self: 1545 if accepted_form: 1546 callback(onaccept,form) 1547 if isinstance(next, (list, tuple)): 1548 # fix issue with 2.6 1549 next = next[0] 1550 if next and not next[0] == '/' and next[:4] != 'http': 1551 next = self.url(next.replace('[id]', str(form.vars.id))) 1552 redirect(next) 1553 table_user[username].requires = old_requires 1554 return form 1555 else: 1556 redirect(next)
1557
1558 - def logout(self, next=DEFAULT, onlogout=DEFAULT, log=DEFAULT):
1559 """ 1560 logout and redirects to login 1561 1562 .. method:: Auth.logout ([next=DEFAULT[, onlogout=DEFAULT[, 1563 log=DEFAULT]]]) 1564 1565 """ 1566 1567 if next == DEFAULT: 1568 next = self.settings.logout_next 1569 if onlogout == DEFAULT: 1570 onlogout = self.settings.logout_onlogout 1571 if onlogout: 1572 onlogout(self.user) 1573 if log == DEFAULT: 1574 log = self.messages.logout_log 1575 if log and self.user: 1576 self.log_event(log % self.user) 1577 1578 if self.settings.login_form != self: 1579 cas = self.settings.login_form 1580 cas_user = cas.get_user() 1581 if cas_user: 1582 next = cas.logout_url(next) 1583 1584 self.environment.session.auth = None 1585 self.environment.session.flash = self.messages.logged_out 1586 if next: 1587 redirect(next)
1588
1589 - def register( 1590 self, 1591 next=DEFAULT, 1592 onvalidation=DEFAULT, 1593 onaccept=DEFAULT, 1594 log=DEFAULT, 1595 ):
1596 """ 1597 returns a registration form 1598 1599 .. method:: Auth.register([next=DEFAULT [, onvalidation=DEFAULT 1600 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1601 1602 """ 1603 1604 table_user = self.settings.table_user 1605 request = self.environment.request 1606 response = self.environment.response 1607 session = self.environment.session 1608 if self.is_logged_in(): 1609 redirect(self.settings.logged_url) 1610 if next == DEFAULT: 1611 next = request.get_vars._next \ 1612 or request.post_vars._next \ 1613 or self.settings.register_next 1614 if onvalidation == DEFAULT: 1615 onvalidation = self.settings.register_onvalidation 1616 if onaccept == DEFAULT: 1617 onaccept = self.settings.register_onaccept 1618 if log == DEFAULT: 1619 log = self.messages.register_log 1620 1621 passfield = self.settings.password_field 1622 formstyle = self.settings.formstyle 1623 form = SQLFORM(table_user, 1624 fields = self.settings.register_fields, 1625 hidden=dict(_next=next), 1626 showid=self.settings.showid, 1627 submit_button=self.messages.submit_button, 1628 delete_label=self.messages.delete_label, 1629 formstyle=formstyle 1630 ) 1631 for i, row in enumerate(form[0].components): 1632 item = row.element('input',_name=passfield) 1633 if item: 1634 form.custom.widget.password_two = \ 1635 INPUT(_name="password_two", _type="password", 1636 requires=IS_EXPR('value==%s' % \ 1637 repr(request.vars.get(passfield, None)), 1638 error_message=self.messages.mismatched_password)) 1639 1640 addrow(form, self.messages.verify_password + ':', 1641 form.custom.widget.password_two, 1642 self.messages.verify_password_comment, 1643 formstyle, 1644 '%s_%s__row' % (table_user, 'password_two'), 1645 position=i+1) 1646 break 1647 captcha = self.settings.register_captcha or self.settings.captcha 1648 if captcha: 1649 addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') 1650 1651 table_user.registration_key.default = key = web2py_uuid() 1652 if form.accepts(request, session, formname='register', 1653 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1654 description = self.messages.group_description % form.vars 1655 if self.settings.create_user_groups: 1656 group_id = self.add_group("user_%s" % form.vars.id, description) 1657 self.add_membership(group_id, form.vars.id) 1658 if self.settings.registration_requires_verification: 1659 if not self.settings.mailer or \ 1660 not self.settings.mailer.send(to=form.vars.email, 1661 subject=self.messages.verify_email_subject, 1662 message=self.messages.verify_email 1663 % dict(key=key)): 1664 self.db.rollback() 1665 response.flash = self.messages.unable_send_email 1666 return form 1667 session.flash = self.messages.email_sent 1668 elif self.settings.registration_requires_approval: 1669 table_user[form.vars.id] = dict(registration_key='pending') 1670 session.flash = self.messages.registration_pending 1671 else: 1672 table_user[form.vars.id] = dict(registration_key='') 1673 session.flash = self.messages.registration_successful 1674 table_user = self.settings.table_user 1675 if 'username' in table_user.fields: 1676 username = 'username' 1677 else: 1678 username = 'email' 1679 user = self.db(table_user[username] == form.vars[username]).select().first() 1680 user = Storage(table_user._filter_fields(user, id=True)) 1681 session.auth = Storage(user=user, last_visit=request.now, 1682 expiration=self.settings.expiration) 1683 self.user = user 1684 session.flash = self.messages.logged_in 1685 if log: 1686 self.log_event(log % form.vars) 1687 callback(onaccept,form) 1688 if not next: 1689 next = self.url(args = request.args) 1690 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1691 next = next[0] 1692 elif next and not next[0] == '/' and next[:4] != 'http': 1693 next = self.url(next.replace('[id]', str(form.vars.id))) 1694 redirect(next) 1695 return form
1696
1697 - def is_logged_in(self):
1698 """ 1699 checks if the user is logged in and returns True/False. 1700 if so user is in auth.user as well as in session.auth.user 1701 """ 1702 1703 if self.user: 1704 return True 1705 return False
1706
1707 - def verify_email( 1708 self, 1709 next=DEFAULT, 1710 onaccept=DEFAULT, 1711 log=DEFAULT, 1712 ):
1713 """ 1714 action user to verify the registration email, XXXXXXXXXXXXXXXX 1715 1716 .. method:: Auth.verify_email([next=DEFAULT [, onvalidation=DEFAULT 1717 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1718 1719 """ 1720 1721 key = self.environment.request.args[-1] 1722 table_user = self.settings.table_user 1723 user = self.db(table_user.registration_key == key).select().first() 1724 if not user: 1725 raise HTTP(404) 1726 if self.settings.registration_requires_approval: 1727 user.update_record(registration_key = 'pending') 1728 self.environment.session.flash = self.messages.registration_pending 1729 else: 1730 user.update_record(registration_key = '') 1731 self.environment.session.flash = self.messages.email_verified 1732 if log == DEFAULT: 1733 log = self.messages.verify_email_log 1734 if next == DEFAULT: 1735 next = self.settings.verify_email_next 1736 if onaccept == DEFAULT: 1737 onaccept = self.settings.verify_email_onaccept 1738 if log: 1739 self.log_event(log % user) 1740 callback(onaccept,user) 1741 redirect(next)
1742
1743 - def retrieve_username( 1744 self, 1745 next=DEFAULT, 1746 onvalidation=DEFAULT, 1747 onaccept=DEFAULT, 1748 log=DEFAULT, 1749 ):
1750 """ 1751 returns a form to retrieve the user username 1752 (only if there is a username field) 1753 1754 .. method:: Auth.retrieve_username([next=DEFAULT 1755 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1756 1757 """ 1758 1759 table_user = self.settings.table_user 1760 if not 'username' in table_user.fields: 1761 raise HTTP(404) 1762 request = self.environment.request 1763 response = self.environment.response 1764 session = self.environment.session 1765 captcha = self.settings.retrieve_username_captcha or \ 1766 (self.settings.retrieve_username_captcha!=False and self.settings.captcha) 1767 if not self.settings.mailer: 1768 response.flash = self.messages.function_disabled 1769 return '' 1770 if next == DEFAULT: 1771 next = request.get_vars._next \ 1772 or request.post_vars._next \ 1773 or self.settings.retrieve_username_next 1774 if onvalidation == DEFAULT: 1775 onvalidation = self.settings.retrieve_username_onvalidation 1776 if onaccept == DEFAULT: 1777 onaccept = self.settings.retrieve_username_onaccept 1778 if log == DEFAULT: 1779 log = self.messages.retrieve_username_log 1780 old_requires = table_user.email.requires 1781 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 1782 error_message=self.messages.invalid_email)] 1783 form = SQLFORM(table_user, 1784 fields=['email'], 1785 hidden=dict(_next=next), 1786 showid=self.settings.showid, 1787 submit_button=self.messages.submit_button, 1788 delete_label=self.messages.delete_label, 1789 formstyle=self.settings.formstyle 1790 ) 1791 if captcha: 1792 addrow(form, captcha.label, captcha, captcha.comment,self.settings.formstyle, 'captcha__row') 1793 1794 if form.accepts(request, session, 1795 formname='retrieve_username', dbio=False, 1796 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1797 user = self.db(table_user.email == form.vars.email).select().first() 1798 if not user: 1799 self.environment.session.flash = \ 1800 self.messages.invalid_email 1801 redirect(self.url(args=request.args)) 1802 username = user.username 1803 self.settings.mailer.send(to=form.vars.email, 1804 subject=self.messages.retrieve_username_subject, 1805 message=self.messages.retrieve_username 1806 % dict(username=username)) 1807 session.flash = self.messages.email_sent 1808 if log: 1809 self.log_event(log % user) 1810 callback(onaccept,form) 1811 if not next: 1812 next = self.url(args = request.args) 1813 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1814 next = next[0] 1815 elif next and not next[0] == '/' and next[:4] != 'http': 1816 next = self.url(next.replace('[id]', str(form.vars.id))) 1817 redirect(next) 1818 table_user.email.requires = old_requires 1819 return form
1820
1821 - def random_password(self):
1822 import string 1823 import random 1824 password = '' 1825 specials=r'!#$*' 1826 for i in range(0,3): 1827 password += random.choice(string.lowercase) 1828 password += random.choice(string.uppercase) 1829 password += random.choice(string.digits) 1830 password += random.choice(specials) 1831 return ''.join(random.sample(password,len(password)))
1832
1833 - def reset_password_deprecated( 1834 self, 1835 next=DEFAULT, 1836 onvalidation=DEFAULT, 1837 onaccept=DEFAULT, 1838 log=DEFAULT, 1839 ):
1840 """ 1841 returns a form to reset the user password (deprecated) 1842 1843 .. method:: Auth.reset_password_deprecated([next=DEFAULT 1844 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1845 1846 """ 1847 1848 table_user = self.settings.table_user 1849 request = self.environment.request 1850 response = self.environment.response 1851 session = self.environment.session 1852 if not self.settings.mailer: 1853 response.flash = self.messages.function_disabled 1854 return '' 1855 if next == DEFAULT: 1856 next = request.get_vars._next \ 1857 or request.post_vars._next \ 1858 or self.settings.retrieve_password_next 1859 if onvalidation == DEFAULT: 1860 onvalidation = self.settings.retrieve_password_onvalidation 1861 if onaccept == DEFAULT: 1862 onaccept = self.settings.retrieve_password_onaccept 1863 if log == DEFAULT: 1864 log = self.messages.retrieve_password_log 1865 old_requires = table_user.email.requires 1866 table_user.email.requires = [IS_IN_DB(self.db, table_user.email, 1867 error_message=self.messages.invalid_email)] 1868 form = SQLFORM(table_user, 1869 fields=['email'], 1870 hidden=dict(_next=next), 1871 showid=self.settings.showid, 1872 submit_button=self.messages.submit_button, 1873 delete_label=self.messages.delete_label, 1874 formstyle=self.settings.formstyle 1875 ) 1876 if form.accepts(request, session, 1877 formname='retrieve_password', dbio=False, 1878 onvalidation=onvalidation,hideerror=self.settings.hideerror): 1879 user = self.db(table_user.email == form.vars.email).select().first() 1880 if not user: 1881 self.environment.session.flash = \ 1882 self.messages.invalid_email 1883 redirect(self.url(args=request.args)) 1884 elif user.registration_key in ('pending','disabled','blocked'): 1885 self.environment.session.flash = \ 1886 self.messages.registration_pending 1887 redirect(self.url(args=request.args)) 1888 password = self.random_password() 1889 passfield = self.settings.password_field 1890 d = {passfield: table_user[passfield].validate(password)[0], 1891 'registration_key': ''} 1892 user.update_record(**d) 1893 if self.settings.mailer and \ 1894 self.settings.mailer.send(to=form.vars.email, 1895 subject=self.messages.retrieve_password_subject, 1896 message=self.messages.retrieve_password \ 1897 % dict(password=password)): 1898 session.flash = self.messages.email_sent 1899 else: 1900 session.flash = self.messages.unable_to_send_email 1901 if log: 1902 self.log_event(log % user) 1903 callback(onaccept,form) 1904 if not next: 1905 next = self.url(args = request.args) 1906 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 1907 next = next[0] 1908 elif next and not next[0] == '/' and next[:4] != 'http': 1909 next = self.url(next.replace('[id]', str(form.vars.id))) 1910 redirect(next) 1911 table_user.email.requires = old_requires 1912 return form
1913
1914 - def reset_password( 1915 self, 1916 next=DEFAULT, 1917 onvalidation=DEFAULT, 1918 onaccept=DEFAULT, 1919 log=DEFAULT, 1920 ):
1921 """ 1922 returns a form to reset the user password 1923 1924 .. method:: Auth.reset_password([next=DEFAULT 1925 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1926 1927 """ 1928 1929 table_user = self.settings.table_user 1930 request = self.environment.request 1931 # response = self.environment.response 1932 session = self.environment.session 1933 1934 if next == DEFAULT: 1935 next = request.get_vars._next \ 1936 or request.post_vars._next \ 1937 or self.settings.reset_password_next 1938 1939 try: 1940 key = request.vars.key or request.args[-1] 1941 t0 = int(key.split('-')[0]) 1942 if time.time()-t0 > 60*60*24: raise Exception 1943 user = self.db(table_user.reset_password_key == key).select().first() 1944 if not user: raise Exception 1945 except Exception: 1946 session.flash = self.messages.invalid_reset_password 1947 redirect(next) 1948 passfield = self.settings.password_field 1949 form = SQLFORM.factory( 1950 Field('new_password', 'password', 1951 label=self.messages.new_password, 1952 requires=self.settings.table_user[passfield].requires), 1953 Field('new_password2', 'password', 1954 label=self.messages.verify_password, 1955 requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), 1956 self.messages.mismatched_password)]), 1957 submit_button=self.messages.submit_button, 1958 formstyle=self.settings.formstyle, 1959 ) 1960 if form.accepts(request,session,hideerror=self.settings.hideerror): 1961 user.update_record(**{passfield:form.vars.new_password, 1962 'registration_key':'', 1963 'reset_password_key':''}) 1964 session.flash = self.messages.password_changed 1965 redirect(next) 1966 return form
1967
1968 - def request_reset_password( 1969 self, 1970 next=DEFAULT, 1971 onvalidation=DEFAULT, 1972 onaccept=DEFAULT, 1973 log=DEFAULT, 1974 ):
1975 """ 1976 returns a form to reset the user password 1977 1978 .. method:: Auth.reset_password([next=DEFAULT 1979 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) 1980 1981 """ 1982 1983 table_user = self.settings.table_user 1984 request = self.environment.request 1985 response = self.environment.response 1986 session = self.environment.session 1987 captcha = self.settings.retrieve_password_captcha or \ 1988 (self.settings.retrieve_password_captcha!=False and self.settings.captcha) 1989 1990 if next == DEFAULT: 1991 next = request.get_vars._next \ 1992 or request.post_vars._next \ 1993 or self.settings.request_reset_password_next 1994 1995 if not self.settings.mailer: 1996 response.flash = self.messages.function_disabled 1997 return '' 1998 if onvalidation == DEFAULT: 1999 onvalidation = self.settings.reset_password_onvalidation 2000 if onaccept == DEFAULT: 2001 onaccept = self.settings.reset_password_onaccept 2002 if log == DEFAULT: 2003 log = self.messages.reset_password_log 2004 # old_requires = table_user.email.requires <<< perhaps should be restored 2005 table_user.email.requires = [ 2006 IS_EMAIL(error_message=self.messages.invalid_email), 2007 IS_IN_DB(self.db, table_user.email, 2008 error_message=self.messages.invalid_email)] 2009 form = SQLFORM(table_user, 2010 fields=['email'], 2011 hidden=dict(_next=next), 2012 showid=self.settings.showid, 2013 submit_button=self.messages.submit_button, 2014 delete_label=self.messages.delete_label, 2015 formstyle=self.settings.formstyle 2016 ) 2017 if captcha: 2018 addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') 2019 if form.accepts(request, session, 2020 formname='reset_password', dbio=False, 2021 onvalidation=onvalidation, 2022 hideerror=self.settings.hideerror): 2023 user = self.db(table_user.email == form.vars.email).select().first() 2024 if not user: 2025 session.flash = self.messages.invalid_email 2026 redirect(self.url(args=request.args)) 2027 elif user.registration_key in ('pending','disabled','blocked'): 2028 session.flash = self.messages.registration_pending 2029 redirect(self.url(args=request.args)) 2030 reset_password_key = str(int(time.time()))+'-' + web2py_uuid() 2031 2032 if self.settings.mailer.send(to=form.vars.email, 2033 subject=self.messages.reset_password_subject, 2034 message=self.messages.reset_password % \ 2035 dict(key=reset_password_key)): 2036 session.flash = self.messages.email_sent 2037 user.update_record(reset_password_key=reset_password_key) 2038 else: 2039 session.flash = self.messages.unable_to_send_email 2040 if log: 2041 self.log_event(log % user) 2042 callback(onaccept,form) 2043 if not next: 2044 next = self.url(args = request.args) 2045 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2046 next = next[0] 2047 elif next and not next[0] == '/' and next[:4] != 'http': 2048 next = self.url(next.replace('[id]', str(form.vars.id))) 2049 redirect(next) 2050 # old_requires = table_user.email.requires 2051 return form
2052
2053 - def retrieve_password( 2054 self, 2055 next=DEFAULT, 2056 onvalidation=DEFAULT, 2057 onaccept=DEFAULT, 2058 log=DEFAULT, 2059 ):
2060 if self.settings.reset_password_requires_verification: 2061 return self.request_reset_password(next,onvalidation,onaccept,log) 2062 else: 2063 return self.reset_password_deprecated(next,onvalidation,onaccept,log)
2064
2065 - def change_password( 2066 self, 2067 next=DEFAULT, 2068 onvalidation=DEFAULT, 2069 onaccept=DEFAULT, 2070 log=DEFAULT, 2071 ):
2072 """ 2073 returns a form that lets the user change password 2074 2075 .. method:: Auth.change_password([next=DEFAULT[, onvalidation=DEFAULT[, 2076 onaccept=DEFAULT[, log=DEFAULT]]]]) 2077 """ 2078 2079 if not self.is_logged_in(): 2080 redirect(self.settings.login_url) 2081 db = self.db 2082 table_user = self.settings.table_user 2083 usern = self.settings.table_user_name 2084 s = db(table_user.id == self.user.id) 2085 2086 request = self.environment.request 2087 session = self.environment.session 2088 if next == DEFAULT: 2089 next = request.get_vars._next \ 2090 or request.post_vars._next \ 2091 or self.settings.change_password_next 2092 if onvalidation == DEFAULT: 2093 onvalidation = self.settings.change_password_onvalidation 2094 if onaccept == DEFAULT: 2095 onaccept = self.settings.change_password_onaccept 2096 if log == DEFAULT: 2097 log = self.messages.change_password_log 2098 passfield = self.settings.password_field 2099 form = SQLFORM.factory( 2100 Field('old_password', 'password', 2101 label=self.messages.old_password, 2102 requires=validators( 2103 table_user[passfield].requires, 2104 IS_IN_DB(s, '%s.%s' % (usern, passfield), 2105 error_message=self.messages.invalid_password))), 2106 Field('new_password', 'password', 2107 label=self.messages.new_password, 2108 requires=table_user[passfield].requires), 2109 Field('new_password2', 'password', 2110 label=self.messages.verify_password, 2111 requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), 2112 self.messages.mismatched_password)]), 2113 submit_button=self.messages.submit_button, 2114 formstyle = self.settings.formstyle 2115 ) 2116 if form.accepts(request, session, 2117 formname='change_password', 2118 onvalidation=onvalidation, 2119 hideerror=self.settings.hideerror): 2120 d = {passfield: form.vars.new_password} 2121 s.update(**d) 2122 session.flash = self.messages.password_changed 2123 if log: 2124 self.log_event(log % self.user) 2125 callback(onaccept,form) 2126 if not next: 2127 next = self.url(args=request.args) 2128 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2129 next = next[0] 2130 elif next and not next[0] == '/' and next[:4] != 'http': 2131 next = self.url(next.replace('[id]', str(form.vars.id))) 2132 redirect(next) 2133 return form
2134
2135 - def profile( 2136 self, 2137 next=DEFAULT, 2138 onvalidation=DEFAULT, 2139 onaccept=DEFAULT, 2140 log=DEFAULT, 2141 ):
2142 """ 2143 returns a form that lets the user change his/her profile 2144 2145 .. method:: Auth.profile([next=DEFAULT [, onvalidation=DEFAULT 2146 [, onaccept=DEFAULT [, log=DEFAULT]]]]) 2147 2148 """ 2149 2150 table_user = self.settings.table_user 2151 if not self.is_logged_in(): 2152 redirect(self.settings.login_url) 2153 passfield = self.settings.password_field 2154 self.settings.table_user[passfield].writable = False 2155 request = self.environment.request 2156 session = self.environment.session 2157 if next == DEFAULT: 2158 next = request.get_vars._next \ 2159 or request.post_vars._next \ 2160 or self.settings.profile_next 2161 if onvalidation == DEFAULT: 2162 onvalidation = self.settings.profile_onvalidation 2163 if onaccept == DEFAULT: 2164 onaccept = self.settings.profile_onaccept 2165 if log == DEFAULT: 2166 log = self.messages.profile_log 2167 form = SQLFORM( 2168 table_user, 2169 self.user.id, 2170 fields = self.settings.profile_fields, 2171 hidden = dict(_next=next), 2172 showid = self.settings.showid, 2173 submit_button = self.messages.submit_button, 2174 delete_label = self.messages.delete_label, 2175 upload = self.settings.download_url, 2176 formstyle = self.settings.formstyle 2177 ) 2178 if form.accepts(request, session, 2179 formname='profile', 2180 onvalidation=onvalidation,hideerror=self.settings.hideerror): 2181 self.user.update(table_user._filter_fields(form.vars)) 2182 session.flash = self.messages.profile_updated 2183 if log: 2184 self.log_event(log % self.user) 2185 callback(onaccept,form) 2186 if not next: 2187 next = self.url(args=request.args) 2188 elif isinstance(next, (list, tuple)): ### fix issue with 2.6 2189 next = next[0] 2190 elif next and not next[0] == '/' and next[:4] != 'http': 2191 next = self.url(next.replace('[id]', str(form.vars.id))) 2192 redirect(next) 2193 return form
2194
2195 - def is_impersonating(self):
2196 return self.environment.session.auth.impersonator
2197
2198 - def impersonate(self, user_id=DEFAULT):
2199 """ 2200 usage: POST TO http://..../impersonate request.post_vars.user_id=<id> 2201 set request.post_vars.user_id to 0 to restore original user. 2202 2203 requires impersonator is logged in and 2204 has_permission('impersonate', 'auth_user', user_id) 2205 """ 2206 request = self.environment.request 2207 session = self.environment.session 2208 auth = session.auth 2209 if not self.is_logged_in() or not self.environment.request.post_vars: 2210 raise HTTP(401, "Not Authorized") 2211 current_id = auth.user.id 2212 requested_id = user_id 2213 if user_id == DEFAULT: 2214 user_id = self.environment.request.post_vars.user_id 2215 if user_id and user_id != self.user.id and user_id != '0': 2216 if not self.has_permission('impersonate', 2217 self.settings.table_user_name, 2218 user_id): 2219 raise HTTP(403, "Forbidden") 2220 user = self.settings.table_user(user_id) 2221 if not user: 2222 raise HTTP(401, "Not Authorized") 2223 auth.impersonator = cPickle.dumps(session) 2224 auth.user.update( 2225 self.settings.table_user._filter_fields(user, True)) 2226 self.user = auth.user 2227 if self.settings.login_onaccept: 2228 form = Storage(dict(vars=self.user)) 2229 self.settings.login_onaccept(form) 2230 log = self.messages.impersonate_log 2231 if log: 2232 self.log_event(log % dict(id=current_id,other_id=auth.user.id)) 2233 elif user_id in (0, '0') and self.is_impersonating(): 2234 session.clear() 2235 session.update(cPickle.loads(auth.impersonator)) 2236 self.user = session.auth.user 2237 if requested_id == DEFAULT and not request.post_vars: 2238 return SQLFORM.factory(Field('user_id','integer')) 2239 return self.user
2240
2241 - def groups(self):
2242 """ 2243 displays the groups and their roles for the logged in user 2244 """ 2245 2246 if not self.is_logged_in(): 2247 redirect(self.settings.login_url) 2248 memberships = self.db(self.settings.table_membership.user_id 2249 == self.user.id).select() 2250 table = TABLE() 2251 for membership in memberships: 2252 groups = self.db(self.settings.table_group.id 2253 == membership.group_id).select() 2254 if groups: 2255 group = groups[0] 2256 table.append(TR(H3(group.role, '(%s)' % group.id))) 2257 table.append(TR(P(group.description))) 2258 if not memberships: 2259 return None 2260 return table
2261
2262 - def not_authorized(self):
2263 """ 2264 you can change the view for this page to make it look as you like 2265 """ 2266 2267 return 'ACCESS DENIED'
2268
2269 - def requires(self, condition):
2270 """ 2271 decorator that prevents access to action if not logged in 2272 """ 2273 2274 def decorator(action): 2275 2276 def f(*a, **b): 2277 2278 if self.settings.allow_basic_login_only and not self.basic(): 2279 if self.environment.request.is_restful: 2280 raise HTTP(403,"Not authorized") 2281 return call_or_redirect(self.settings.on_failed_authorization) 2282 2283 if not condition: 2284 if self.environment.request.is_restful: 2285 raise HTTP(403,"Not authorized") 2286 if not self.basic() and not self.is_logged_in(): 2287 request = self.environment.request 2288 next = URL(r=request,args=request.args, 2289 vars=request.get_vars) 2290 self.environment.session.flash = self.environment.response.flash 2291 return call_or_redirect(self.settings.on_failed_authentication, 2292 self.settings.login_url + \ 2293 '?_next='+urllib.quote(next)) 2294 else: 2295 self.environment.session.flash = \ 2296 self.messages.access_denied 2297 return call_or_redirect(self.settings.on_failed_authorization) 2298 return action(*a, **b)
2299 f.__doc__ = action.__doc__ 2300 f.__name__ = action.__name__ 2301 f.__dict__.update(action.__dict__) 2302 return f
2303 2304 return decorator 2305
2306 - def requires_login(self):
2307 """ 2308 decorator that prevents access to action if not logged in 2309 """ 2310 2311 def decorator(action): 2312 2313 def f(*a, **b): 2314 2315 if self.settings.allow_basic_login_only and not self.basic(): 2316 if self.environment.request.is_restful: 2317 raise HTTP(403,"Not authorized") 2318 return call_or_redirect(self.settings.on_failed_authorization) 2319 2320 if not self.basic() and not self.is_logged_in(): 2321 if self.environment.request.is_restful: 2322 raise HTTP(403,"Not authorized") 2323 request = self.environment.request 2324 next = URL(r=request,args=request.args, 2325 vars=request.get_vars) 2326 self.environment.session.flash = self.environment.response.flash 2327 return call_or_redirect(self.settings.on_failed_authentication, 2328 self.settings.login_url + \ 2329 '?_next='+urllib.quote(next) 2330 ) 2331 return action(*a, **b)
2332 f.__doc__ = action.__doc__ 2333 f.__name__ = action.__name__ 2334 f.__dict__.update(action.__dict__) 2335 return f 2336 2337 return decorator 2338
2339 - def requires_membership(self, role=None, group_id=None):
2340 """ 2341 decorator that prevents access to action if not logged in or 2342 if user logged in is not a member of group_id. 2343 If role is provided instead of group_id then the 2344 group_id is calculated. 2345 """ 2346 2347 def decorator(action): 2348 def f(*a, **b): 2349 if self.settings.allow_basic_login_only and not self.basic(): 2350 if self.environment.request.is_restful: 2351 raise HTTP(403,"Not authorized") 2352 return call_or_redirect(self.settings.on_failed_authorization) 2353 2354 if not self.basic() and not self.is_logged_in(): 2355 if self.environment.request.is_restful: 2356 raise HTTP(403,"Not authorized") 2357 request = self.environment.request 2358 next = URL(r=request,args=request.args, 2359 vars=request.get_vars) 2360 self.environment.session.flash = self.environment.response.flash 2361 return call_or_redirect(self.settings.on_failed_authentication, 2362 self.settings.login_url + \ 2363 '?_next='+urllib.quote(next) 2364 ) 2365 if not self.has_membership(group_id=group_id, role=role): 2366 self.environment.session.flash = \ 2367 self.messages.access_denied 2368 return call_or_redirect(self.settings.on_failed_authorization) 2369 return action(*a, **b)
2370 f.__doc__ = action.__doc__ 2371 f.__name__ = action.__name__ 2372 f.__dict__.update(action.__dict__) 2373 return f 2374 2375 return decorator 2376
2377 - def requires_permission( 2378 self, 2379 name, 2380 table_name='', 2381 record_id=0, 2382 ):
2383 """ 2384 decorator that prevents access to action if not logged in or 2385 if user logged in is not a member of any group (role) that 2386 has 'name' access to 'table_name', 'record_id'. 2387 """ 2388 2389 def decorator(action): 2390 2391 def f(*a, **b): 2392 if self.settings.allow_basic_login_only and not self.basic(): 2393 if self.environment.request.is_restful: 2394 raise HTTP(403,"Not authorized") 2395 return call_or_redirect(self.settings.on_failed_authorization) 2396 2397 if not self.basic() and not self.is_logged_in(): 2398 if self.environment.request.is_restful: 2399 raise HTTP(403,"Not authorized") 2400 request = self.environment.request 2401 next = URL(r=request,args=request.args, 2402 vars=request.get_vars) 2403 self.environment.session.flash = self.environment.response.flash 2404 return call_or_redirect(self.settings.on_failed_authentication, 2405 self.settings.login_url + 2406 '?_next='+urllib.quote(next) 2407 ) 2408 2409 if not self.has_permission(name, table_name, record_id): 2410 self.environment.session.flash = \ 2411 self.messages.access_denied 2412 return call_or_redirect(self.settings.on_failed_authorization) 2413 return action(*a, **b)
2414 f.__doc__ = action.__doc__ 2415 f.__name__ = action.__name__ 2416 f.__dict__.update(action.__dict__) 2417 return f 2418 2419 return decorator 2420
2421 - def add_group(self, role, description=''):
2422 """ 2423 creates a group associated to a role 2424 """ 2425 2426 group_id = self.settings.table_group.insert(role=role, 2427 description=description) 2428 log = self.messages.add_group_log 2429 if log: 2430 self.log_event(log % dict(group_id=group_id, role=role)) 2431 return group_id
2432
2433 - def del_group(self, group_id):
2434 """ 2435 deletes a group 2436 """ 2437 2438 self.db(self.settings.table_group.id == group_id).delete() 2439 self.db(self.settings.table_membership.group_id 2440 == group_id).delete() 2441 self.db(self.settings.table_permission.group_id 2442 == group_id).delete() 2443 log = self.messages.del_group_log 2444 if log: 2445 self.log_event(log % dict(group_id=group_id))
2446
2447 - def id_group(self, role):
2448 """ 2449 returns the group_id of the group specified by the role 2450 """ 2451 rows = self.db(self.settings.table_group.role == role).select() 2452 if not rows: 2453 return None 2454 return rows[0].id
2455
2456 - def user_group(self, user_id = None):
2457 """ 2458 returns the group_id of the group uniquely associated to this user 2459 i.e. role=user:[user_id] 2460 """ 2461 if not user_id and self.user: 2462 user_id = self.user.id 2463 role = 'user_%s' % user_id 2464 return self.id_group(role)
2465
2466 - def has_membership(self, group_id=None, user_id=None, role=None):
2467 """ 2468 checks if user is member of group_id or role 2469 """ 2470 2471 group_id = group_id or self.id_group(role) 2472 try: 2473 group_id = int(group_id) 2474 except: 2475 group_id = self.id_group(group_id) # interpret group_id as a role 2476 if not user_id and self.user: 2477 user_id = self.user.id 2478 membership = self.settings.table_membership 2479 if self.db((membership.user_id == user_id) 2480 & (membership.group_id == group_id)).select(): 2481 r = True 2482 else: 2483 r = False 2484 log = self.messages.has_membership_log 2485 if log: 2486 self.log_event(log % dict(user_id=user_id, 2487 group_id=group_id, check=r)) 2488 return r
2489
2490 - def add_membership(self, group_id=None, user_id=None, role=None):
2491 """ 2492 gives user_id membership of group_id or role 2493 if user_id==None than user_id is that of current logged in user 2494 """ 2495 2496 group_id = group_id or self.id_group(role) 2497 try: 2498 group_id = int(group_id) 2499 except: 2500 group_id = self.id_group(group_id) # interpret group_id as a role 2501 if not user_id and self.user: 2502 user_id = self.user.id 2503 membership = self.settings.table_membership 2504 record = membership(user_id = user_id,group_id = group_id) 2505 if record: 2506 return record.id 2507 else: 2508 id = membership.insert(group_id=group_id, user_id=user_id) 2509 log = self.messages.add_membership_log 2510 if log: 2511 self.log_event(log % dict(user_id=user_id, 2512 group_id=group_id)) 2513 return id
2514
2515 - def del_membership(self, group_id, user_id=None, role=None):
2516 """ 2517 revokes membership from group_id to user_id 2518 if user_id==None than user_id is that of current logged in user 2519 """ 2520 2521 group_id = group_id or self.id_group(role) 2522 if not user_id and self.user: 2523 user_id = self.user.id 2524 membership = self.settings.table_membership 2525 log = self.messages.del_membership_log 2526 if log: 2527 self.log_event(log % dict(user_id=user_id, 2528 group_id=group_id)) 2529 return self.db(membership.user_id 2530 == user_id)(membership.group_id 2531 == group_id).delete()
2532
2533 - def has_permission( 2534 self, 2535 name='any', 2536 table_name='', 2537 record_id=0, 2538 user_id=None, 2539 group_id=None, 2540 ):
2541 """ 2542 checks if user_id or current logged in user is member of a group 2543 that has 'name' permission on 'table_name' and 'record_id' 2544 if group_id is passed, it checks whether the group has the permission 2545 """ 2546 2547 if not user_id and not group_id and self.user: 2548 user_id = self.user.id 2549 if user_id: 2550 membership = self.settings.table_membership 2551 rows = self.db(membership.user_id 2552 == user_id).select(membership.group_id) 2553 groups = set([row.group_id for row in rows]) 2554 if group_id and not group_id in groups: 2555 return False 2556 else: 2557 groups = set([group_id]) 2558 permission = self.settings.table_permission 2559 rows = self.db(permission.name == name)(permission.table_name 2560 == str(table_name))(permission.record_id 2561 == record_id).select(permission.group_id) 2562 groups_required = set([row.group_id for row in rows]) 2563 if record_id: 2564 rows = self.db(permission.name 2565 == name)(permission.table_name 2566 == str(table_name))(permission.record_id 2567 == 0).select(permission.group_id) 2568 groups_required = groups_required.union(set([row.group_id 2569 for row in rows])) 2570 if groups.intersection(groups_required): 2571 r = True 2572 else: 2573 r = False 2574 log = self.messages.has_permission_log 2575 if log and user_id: 2576 self.log_event(log % dict(user_id=user_id, name=name, 2577 table_name=table_name, record_id=record_id)) 2578 return r
2579
2580 - def add_permission( 2581 self, 2582 group_id, 2583 name='any', 2584 table_name='', 2585 record_id=0, 2586 ):
2587 """ 2588 gives group_id 'name' access to 'table_name' and 'record_id' 2589 """ 2590 2591 permission = self.settings.table_permission 2592 if group_id == 0: 2593 group_id = self.user_group() 2594 id = permission.insert(group_id=group_id, name=name, 2595 table_name=str(table_name), 2596 record_id=long(record_id)) 2597 log = self.messages.add_permission_log 2598 if log: 2599 self.log_event(log % dict(permission_id=id, group_id=group_id, 2600 name=name, table_name=table_name, 2601 record_id=record_id)) 2602 return id
2603
2604 - def del_permission( 2605 self, 2606 group_id, 2607 name='any', 2608 table_name='', 2609 record_id=0, 2610 ):
2611 """ 2612 revokes group_id 'name' access to 'table_name' and 'record_id' 2613 """ 2614 2615 permission = self.settings.table_permission 2616 log = self.messages.del_permission_log 2617 if log: 2618 self.log_event(log % dict(group_id=group_id, name=name, 2619 table_name=table_name, record_id=record_id)) 2620 return self.db(permission.group_id == group_id)(permission.name 2621 == name)(permission.table_name 2622 == str(table_name))(permission.record_id 2623 == long(record_id)).delete()
2624
2625 - def accessible_query(self, name, table, user_id=None):
2626 """ 2627 returns a query with all accessible records for user_id or 2628 the current logged in user 2629 this method does not work on GAE because uses JOIN and IN 2630 2631 example:: 2632 2633 db(auth.accessible_query('read', db.mytable)).select(db.mytable.ALL) 2634 2635 """ 2636 if not user_id: 2637 user_id = self.user.id 2638 if self.has_permission(name, table, 0, user_id): 2639 return table.id > 0 2640 db = self.db 2641 membership = self.settings.table_membership 2642 permission = self.settings.table_permission 2643 return table.id.belongs(db(membership.user_id == user_id)\ 2644 (membership.group_id == permission.group_id)\ 2645 (permission.name == name)\ 2646 (permission.table_name == table)\ 2647 ._select(permission.record_id))
2648 2649
2650 -class Crud(object):
2651
2652 - def url(self, f=None, args=[], vars={}):
2653 """ 2654 this should point to the controller that exposes 2655 download and crud 2656 """ 2657 return self.environment.URL(r=self.environment.request, 2658 c=self.settings.controller, 2659 f=f, args=args, vars=vars)
2660
2661 - def __init__(self, environment, db, controller='default'):
2662 self.environment = Storage(environment) 2663 self.db = db 2664 self.settings = Settings() 2665 self.settings.auth = None 2666 self.settings.logger = None 2667 2668 self.settings.create_next = None 2669 self.settings.update_next = None 2670 self.settings.controller = controller 2671 self.settings.delete_next = self.url() 2672 self.settings.download_url = self.url('download') 2673 self.settings.create_onvalidation = StorageList() 2674 self.settings.update_onvalidation = StorageList() 2675 self.settings.delete_onvalidation = StorageList() 2676 self.settings.create_onaccept = StorageList() 2677 self.settings.update_onaccept = StorageList() 2678 self.settings.update_ondelete = StorageList() 2679 self.settings.delete_onaccept = StorageList() 2680 self.settings.update_deletable = True 2681 self.settings.showid = False 2682 self.settings.keepvalues = False 2683 self.settings.create_captcha = None 2684 self.settings.update_captcha = None 2685 self.settings.captcha = None 2686 self.settings.formstyle = 'table3cols' 2687 self.settings.hideerror = False 2688 self.settings.detect_record_change = True 2689 self.settings.lock_keys = True 2690 2691 self.messages = Messages(self.environment.T) 2692 self.messages.submit_button = 'Submit' 2693 self.messages.delete_label = 'Check to delete:' 2694 self.messages.record_created = 'Record Created' 2695 self.messages.record_updated = 'Record Updated' 2696 self.messages.record_deleted = 'Record Deleted' 2697 2698 self.messages.update_log = 'Record %(id)s updated' 2699 self.messages.create_log = 'Record %(id)s created' 2700 self.messages.read_log = 'Record %(id)s read' 2701 self.messages.delete_log = 'Record %(id)s deleted' 2702 2703 self.messages.lock_keys = True
2704
2705 - def __call__(self):
2706 2707 args = self.environment.request.args 2708 if len(args) < 1: 2709 redirect(self.url(args='tables')) 2710 elif args[0] == 'tables': 2711 return self.tables() 2712 elif args[0] == 'create': 2713 return self.create(args(1)) 2714 elif args[0] == 'select': 2715 return self.select(args(1),linkto=self.url(args='read')) 2716 elif args[0] == 'read': 2717 return self.read(args(1), args(2)) 2718 elif args[0] == 'update': 2719 return self.update(args(1), args(2)) 2720 elif args[0] == 'delete': 2721 return self.delete(args(1), args(2)) 2722 else: 2723 raise HTTP(404)
2724
2725 - def log_event(self, message):
2726 if self.settings.logger: 2727 self.settings.logger.log_event(message, 'crud')
2728
2729 - def has_permission(self, name, table, record=0):
2730 if not self.settings.auth: 2731 return True 2732 try: 2733 record_id = record.id 2734 except: 2735 record_id = record 2736 return self.settings.auth.has_permission(name, str(table), record_id)
2737
2738 - def tables(self):
2739 return TABLE(*[TR(A(name, 2740 _href=self.url(args=('select',name)))) \ 2741 for name in self.db.tables])
2742 2743 2744 @staticmethod
2745 - def archive(form,archive_table=None,current_record='current_record'):
2746 """ 2747 If you have a table (db.mytable) that needs full revision history you can just do:: 2748 2749 form=crud.update(db.mytable,myrecord,onaccept=crud.archive) 2750 2751 crud.archive will define a new table "mytable_archive" and store the 2752 previous record in the newly created table including a reference 2753 to the current record. 2754 2755 If you want to access such table you need to define it yourself in a model:: 2756 2757 db.define_table('mytable_archive', 2758 Field('current_record',db.mytable), 2759 db.mytable) 2760 2761 Notice such table includes all fields of db.mytable plus one: current_record. 2762 crud.archive does not timestamp the stored record unless your original table 2763 has a fields like:: 2764 2765 db.define_table(..., 2766 Field('saved_on','datetime', 2767 default=request.now,update=request.now,writable=False), 2768 Field('saved_by',auth.user, 2769 default=auth.user_id,update=auth.user_id,writable=False), 2770 2771 there is nothing special about these fields since they are filled before 2772 the record is archived. 2773 2774 If you want to change the archive table name and the name of the reference field 2775 you can do, for example:: 2776 2777 db.define_table('myhistory', 2778 Field('parent_record',db.mytable), 2779 db.mytable) 2780 2781 and use it as:: 2782 2783 form=crud.update(db.mytable,myrecord, 2784 onaccept=lambda form:crud.archive(form, 2785 archive_table=db.myhistory, 2786 current_record='parent_record')) 2787 2788 """ 2789 old_record = form.record 2790 if not old_record: 2791 return None 2792 table = form.table 2793 if not archive_table: 2794 archive_table_name = '%s_archive' % table 2795 if archive_table_name in table._db: 2796 archive_table = table._db[archive_table_name] 2797 else: 2798 archive_table = table._db.define_table(archive_table_name, 2799 Field(current_record,table), 2800 table) 2801 new_record = {current_record:old_record.id} 2802 for fieldname in archive_table.fields: 2803 if not fieldname in ['id',current_record] and fieldname in old_record: 2804 new_record[fieldname]=old_record[fieldname] 2805 id = archive_table.insert(**new_record) 2806 return id
2807
2808 - def update( 2809 self, 2810 table, 2811 record, 2812 next=DEFAULT, 2813 onvalidation=DEFAULT, 2814 onaccept=DEFAULT, 2815 ondelete=DEFAULT, 2816 log=DEFAULT, 2817 message=DEFAULT, 2818 deletable=DEFAULT, 2819 formname=DEFAULT, 2820 ):
2821 """ 2822 .. method:: Crud.update(table, record, [next=DEFAULT 2823 [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT 2824 [, message=DEFAULT[, deletable=DEFAULT]]]]]]) 2825 2826 """ 2827 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 2828 or (isinstance(record, str) and not str(record).isdigit()): 2829 raise HTTP(404) 2830 if not isinstance(table, self.db.Table): 2831 table = self.db[table] 2832 try: 2833 record_id = record.id 2834 except: 2835 record_id = record or 0 2836 if record_id and not self.has_permission('update', table, record_id): 2837 redirect(self.settings.auth.settings.on_failed_authorization) 2838 if not record_id \ 2839 and not self.has_permission('create', table, record_id): 2840 redirect(self.settings.auth.settings.on_failed_authorization) 2841 2842 request = self.environment.request 2843 response = self.environment.response 2844 session = self.environment.session 2845 if request.extension == 'json' and request.vars.json: 2846 request.vars.update(simplejson.loads(request.vars.json)) 2847 if next == DEFAULT: 2848 next = request.get_vars._next \ 2849 or request.post_vars._next \ 2850 or self.settings.update_next 2851 if onvalidation == DEFAULT: 2852 onvalidation = self.settings.update_onvalidation 2853 if onaccept == DEFAULT: 2854 onaccept = self.settings.update_onaccept 2855 if ondelete == DEFAULT: 2856 ondelete = self.settings.update_ondelete 2857 if log == DEFAULT: 2858 log = self.messages.update_log 2859 if deletable == DEFAULT: 2860 deletable = self.settings.update_deletable 2861 if message == DEFAULT: 2862 message = self.messages.record_updated 2863 form = SQLFORM( 2864 table, 2865 record, 2866 hidden=dict(_next=next), 2867 showid=self.settings.showid, 2868 submit_button=self.messages.submit_button, 2869 delete_label=self.messages.delete_label, 2870 deletable=deletable, 2871 upload=self.settings.download_url, 2872 formstyle=self.settings.formstyle 2873 ) 2874 self.accepted = False 2875 self.deleted = False 2876 captcha = self.settings.update_captcha or \ 2877 self.settings.captcha 2878 if record and captcha: 2879 addrow(form, captcha.label, captcha, captcha.comment, 2880 self.settings.formstyle,'captcha__row') 2881 captcha = self.settings.create_captcha or \ 2882 self.settings.captcha 2883 if not record and captcha: 2884 addrow(form, captcha.label, captcha, captcha.comment, 2885 self.settings.formstyle,'captcha__row') 2886 if not request.extension in ('html','load'): 2887 (_session, _formname) = (None, None) 2888 else: 2889 (_session, _formname) = \ 2890 (session, '%s/%s' % (table._tablename, form.record_id)) 2891 if formname!=DEFAULT: 2892 _formname = formname 2893 keepvalues = self.settings.keepvalues 2894 if request.vars.delete_this_record: 2895 keepvalues = False 2896 if isinstance(onvalidation,StorageList): 2897 onvalidation=onvalidation.get(table._tablename, []) 2898 if form.accepts(request, _session, formname=_formname, 2899 onvalidation=onvalidation, keepvalues=keepvalues, 2900 hideerror=self.settings.hideerror, 2901 detect_record_change = self.settings.detect_record_change): 2902 self.accepted = True 2903 response.flash = message 2904 if log: 2905 self.log_event(log % form.vars) 2906 if request.vars.delete_this_record: 2907 self.deleted = True 2908 message = self.messages.record_deleted 2909 callback(ondelete,form,table._tablename) 2910 response.flash = message 2911 callback(onaccept,form,table._tablename) 2912 if not request.extension in ('html','load'): 2913 raise HTTP(200, 'RECORD CREATED/UPDATED') 2914 if isinstance(next, (list, tuple)): ### fix issue with 2.6 2915 next = next[0] 2916 if next: # Only redirect when explicit 2917 if next[0] != '/' and next[:4] != 'http': 2918 next = URL(r=request, 2919 f=next.replace('[id]', str(form.vars.id))) 2920 session.flash = response.flash 2921 redirect(next) 2922 elif not request.extension in ('html','load'): 2923 raise HTTP(401) 2924 return form
2925
2926 - def create( 2927 self, 2928 table, 2929 next=DEFAULT, 2930 onvalidation=DEFAULT, 2931 onaccept=DEFAULT, 2932 log=DEFAULT, 2933 message=DEFAULT, 2934 formname=DEFAULT, 2935 ):
2936 """ 2937 .. method:: Crud.create(table, [next=DEFAULT [, onvalidation=DEFAULT 2938 [, onaccept=DEFAULT [, log=DEFAULT[, message=DEFAULT]]]]]) 2939 """ 2940 2941 if next == DEFAULT: 2942 next = self.settings.create_next 2943 if onvalidation == DEFAULT: 2944 onvalidation = self.settings.create_onvalidation 2945 if onaccept == DEFAULT: 2946 onaccept = self.settings.create_onaccept 2947 if log == DEFAULT: 2948 log = self.messages.create_log 2949 if message == DEFAULT: 2950 message = self.messages.record_created 2951 return self.update( 2952 table, 2953 None, 2954 next=next, 2955 onvalidation=onvalidation, 2956 onaccept=onaccept, 2957 log=log, 2958 message=message, 2959 deletable=False, 2960 formname=formname, 2961 )
2962
2963 - def read(self, table, record):
2964 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 2965 or (isinstance(record, str) and not str(record).isdigit()): 2966 raise HTTP(404) 2967 if not isinstance(table, self.db.Table): 2968 table = self.db[table] 2969 if not self.has_permission('read', table, record): 2970 redirect(self.settings.auth.settings.on_failed_authorization) 2971 form = SQLFORM( 2972 table, 2973 record, 2974 readonly=True, 2975 comments=False, 2976 upload=self.settings.download_url, 2977 showid=self.settings.showid, 2978 formstyle=self.settings.formstyle 2979 ) 2980 if not self.environment.request.extension in ('html','load'): 2981 return table._filter_fields(form.record, id=True) 2982 return form
2983
2984 - def delete( 2985 self, 2986 table, 2987 record_id, 2988 next=DEFAULT, 2989 message=DEFAULT, 2990 ):
2991 """ 2992 .. method:: Crud.delete(table, record_id, [next=DEFAULT 2993 [, message=DEFAULT]]) 2994 """ 2995 if not (isinstance(table, self.db.Table) or table in self.db.tables) \ 2996 or not str(record_id).isdigit(): 2997 raise HTTP(404) 2998 if not isinstance(table, self.db.Table): 2999 table = self.db[table] 3000 if not self.has_permission('delete', table, record_id): 3001 redirect(self.settings.auth.settings.on_failed_authorization) 3002 request = self.environment.request 3003 session = self.environment.session 3004 if next == DEFAULT: 3005 next = request.get_vars._next \ 3006 or request.post_vars._next \ 3007 or self.settings.delete_next 3008 if message == DEFAULT: 3009 message = self.messages.record_deleted 3010 record = table[record_id] 3011 if record: 3012 callback(self.settings.delete_onvalidation,record) 3013 del table[record_id] 3014 callback(self.settings.delete_onaccept,record,table._tablename) 3015 session.flash = message 3016 if next: # Only redirect when explicit 3017 redirect(next)
3018
3019 - def select( 3020 self, 3021 table, 3022 query=None, 3023 fields=None, 3024 orderby=None, 3025 limitby=None, 3026 headers={}, 3027 **attr 3028 ):
3029 request = self.environment.request 3030 if not (isinstance(table, self.db.Table) or table in self.db.tables): 3031 raise HTTP(404) 3032 if not self.has_permission('select', table): 3033 redirect(self.settings.auth.settings.on_failed_authorization) 3034 #if record_id and not self.has_permission('select', table): 3035 # redirect(self.settings.auth.settings.on_failed_authorization) 3036 if not isinstance(table, self.db.Table): 3037 table = self.db[table] 3038 if not query: 3039 query = table.id > 0 3040 if not fields: 3041 fields = [field for field in table if field.readable] 3042 rows = self.db(query).select(*fields, **dict(orderby=orderby, 3043 limitby=limitby)) 3044 if not rows: 3045 return None # Nicer than an empty table. 3046 if not 'upload' in attr: 3047 attr['upload'] = self.url('download') 3048 if not request.extension in ('html','load'): 3049 return rows.as_list() 3050 if not headers: 3051 headers = dict((str(k),k.label) for k in table) 3052 return SQLTABLE(rows, headers=headers, **attr)
3053
3054 - def get_format(self, field):
3055 rtable = field._db[field.type[10:]] 3056 format = rtable.get('_format', None) 3057 if format and isinstance(format, str): 3058 return format[2:-2] 3059 return field.name
3060
3061 - def get_query(self, field, op, value, refsearch=False):
3062 try: 3063 if refsearch: format = self.get_format(field) 3064 if op == 'equals': 3065 if not refsearch: 3066 return field == value 3067 else: 3068 return lambda row: row[field.name][format] == value 3069 elif op == 'not equal': 3070 if not refsearch: 3071 return field != value 3072 else: 3073 return lambda row: row[field.name][format] != value 3074 elif op == 'greater than': 3075 if not refsearch: 3076 return field > value 3077 else: 3078 return lambda row: row[field.name][format] > value 3079 elif op == 'less than': 3080 if not refsearch: 3081 return field < value 3082 else: 3083 return lambda row: row[field.name][format] < value 3084 elif op == 'starts with': 3085 if not refsearch: 3086 return field.like(value+'%') 3087 else: 3088 return lambda row: str(row[field.name][format]).startswith(value) 3089 elif op == 'ends with': 3090 if not refsearch: 3091 return field.like('%'+value) 3092 else: 3093 return lambda row: str(row[field.name][format]).endswith(value) 3094 elif op == 'contains': 3095 if not refsearch: 3096 return field.like('%'+value+'%') 3097 else: 3098 return lambda row: value in row[field.name][format] 3099 except: 3100 return None
3101 3102
3103 - def search(self, *tables, **args):
3104 """ 3105 Creates a search form and its results for a table 3106 Example usage: 3107 form, results = crud.search(db.test, 3108 queries = ['equals', 'not equal', 'contains'], 3109 query_labels={'equals':'Equals', 3110 'not equal':'Not equal'}, 3111 fields = [db.test.id, db.test.children], 3112 field_labels = {'id':'ID','children':'Children'}, 3113 zero='Please choose', 3114 query = (db.test.id > 0)&(db.test.id != 3) ) 3115 """ 3116 table = tables[0] 3117 fields = args.get('fields', table.fields) 3118 request = self.environment.request 3119 db = self.db 3120 if not (isinstance(table, db.Table) or table in db.tables): 3121 raise HTTP(404) 3122 tbl = TABLE() 3123 selected = []; refsearch = []; results = [] 3124 ops = args.get('queries', []) 3125 zero = args.get('zero', '') 3126 if not ops: 3127 ops = ['equals', 'not equal', 'greater than', 3128 'less than', 'starts with', 3129 'ends with', 'contains'] 3130 ops.insert(0,zero) 3131 query_labels = args.get('query_labels', {}) 3132 query = args.get('query',table.id > 0) 3133 field_labels = args.get('field_labels',{}) 3134 for field in fields: 3135 field = table[field] 3136 if not field.readable: continue 3137 fieldname = field.name 3138 chkval = request.vars.get('chk' + fieldname, None) 3139 txtval = request.vars.get('txt' + fieldname, None) 3140 opval = request.vars.get('op' + fieldname, None) 3141 row = TR(TD(INPUT(_type = "checkbox", _name = "chk" + fieldname, 3142 _disabled = (field.type == 'id'), 3143 value = (field.type == 'id' or chkval == 'on'))), 3144 TD(field_labels.get(fieldname,field.label)), 3145 TD(SELECT([OPTION(query_labels.get(op,op), 3146 _value=op) for op in ops], 3147 _name = "op" + fieldname, 3148 value = opval)), 3149 TD(INPUT(_type = "text", _name = "txt" + fieldname, 3150 _value = txtval, _id='txt' + fieldname, 3151 _class = str(field.type)))) 3152 tbl.append(row) 3153 if request.post_vars and (chkval or field.type=='id'): 3154 if txtval and opval != '': 3155 if field.type[0:10] == 'reference ': 3156 refsearch.append(self.get_query(field, 3157 opval, txtval, refsearch=True)) 3158 else: 3159 value, error = field.validate(txtval) 3160 if not error: 3161 ### TODO deal with 'starts with', 'ends with', 'contains' on GAE 3162 query &= self.get_query(field, opval, value) 3163 else: 3164 row[3].append(DIV(error,_class='error')) 3165 selected.append(field) 3166 form = FORM(tbl,INPUT(_type="submit")) 3167 if selected: 3168 try: 3169 results = db(query).select(*selected) 3170 for r in refsearch: 3171 results = results.find(r) 3172 except: # hmmm, we should do better here 3173 results = None 3174 return form, results
3175 3176 3177 urllib2.install_opener(urllib2.build_opener(urllib2.HTTPCookieProcessor())) 3178
3179 -def fetch(url, data=None, headers={}, 3180 cookie=Cookie.SimpleCookie(), 3181 user_agent='Mozilla/5.0'):
3182 if data != None: 3183 data = urllib.urlencode(data) 3184 if user_agent: headers['User-agent'] = user_agent 3185 headers['Cookie'] = ' '.join(['%s=%s;'%(c.key,c.value) for c in cookie.values()]) 3186 try: 3187 from google.appengine.api import urlfetch 3188 except ImportError: 3189 req = urllib2.Request(url, data, headers) 3190 html = urllib2.urlopen(req).read() 3191 else: 3192 method = ((data==None) and urlfetch.GET) or urlfetch.POST 3193 while url is not None: 3194 response = urlfetch.fetch(url=url, payload=data, 3195 method=method, headers=headers, 3196 allow_truncated=False,follow_redirects=False, 3197 deadline=10) 3198 # next request will be a get, so no need to send the data again 3199 data = None 3200 method = urlfetch.GET 3201 # load cookies from the response 3202 cookie.load(response.headers.get('set-cookie', '')) 3203 url = response.headers.get('location') 3204 html = response.content 3205 return html
3206 3207 regex_geocode = \ 3208 re.compile('\<coordinates\>(?P<la>[^,]*),(?P<lo>[^,]*).*?\</coordinates\>') 3209 3210
3211 -def geocode(address):
3212 try: 3213 a = urllib.quote(address) 3214 txt = fetch('http://maps.google.com/maps/geo?q=%s&output=xml' 3215 % a) 3216 item = regex_geocode.search(txt) 3217 (la, lo) = (float(item.group('la')), float(item.group('lo'))) 3218 return (la, lo) 3219 except: 3220 return (0.0, 0.0)
3221 3222
3223 -def universal_caller(f, *a, **b):
3224 c = f.func_code.co_argcount 3225 n = f.func_code.co_varnames[:c] 3226 b = dict([(k, v) for k, v in b.items() if k in n]) 3227 if len(b) == c: 3228 return f(**b) 3229 elif len(a) >= c: 3230 return f(*a[:c]) 3231 raise HTTP(404, "Object does not exist")
3232 3233
3234 -class Service(object):
3235
3236 - def __init__(self, environment):
3237 self.environment = environment 3238 self.run_procedures = {} 3239 self.csv_procedures = {} 3240 self.xml_procedures = {} 3241 self.rss_procedures = {} 3242 self.json_procedures = {} 3243 self.jsonrpc_procedures = {} 3244 self.xmlrpc_procedures = {} 3245 self.amfrpc_procedures = {} 3246 self.amfrpc3_procedures = {} 3247 self.soap_procedures = {}
3248
3249 - def run(self, f):
3250 """ 3251 example:: 3252 3253 service = Service(globals()) 3254 @service.run 3255 def myfunction(a, b): 3256 return a + b 3257 def call(): 3258 return service() 3259 3260 Then call it with:: 3261 3262 wget http://..../app/default/call/run/myfunction?a=3&b=4 3263 3264 """ 3265 self.run_procedures[f.__name__] = f 3266 return f
3267
3268 - def csv(self, f):
3269 """ 3270 example:: 3271 3272 service = Service(globals()) 3273 @service.csv 3274 def myfunction(a, b): 3275 return a + b 3276 def call(): 3277 return service() 3278 3279 Then call it with:: 3280 3281 wget http://..../app/default/call/csv/myfunction?a=3&b=4 3282 3283 """ 3284 self.run_procedures[f.__name__] = f 3285 return f
3286
3287 - def xml(self, f):
3288 """ 3289 example:: 3290 3291 service = Service(globals()) 3292 @service.xml 3293 def myfunction(a, b): 3294 return a + b 3295 def call(): 3296 return service() 3297 3298 Then call it with:: 3299 3300 wget http://..../app/default/call/xml/myfunction?a=3&b=4 3301 3302 """ 3303 self.run_procedures[f.__name__] = f 3304 return f
3305
3306 - def rss(self, f):
3307 """ 3308 example:: 3309 3310 service = Service(globals()) 3311 @service.rss 3312 def myfunction(): 3313 return dict(title=..., link=..., description=..., 3314 created_on=..., entries=[dict(title=..., link=..., 3315 description=..., created_on=...]) 3316 def call(): 3317 return service() 3318 3319 Then call it with:: 3320 3321 wget http://..../app/default/call/rss/myfunction 3322 3323 """ 3324 self.rss_procedures[f.__name__] = f 3325 return f
3326
3327 - def json(self, f):
3328 """ 3329 example:: 3330 3331 service = Service(globals()) 3332 @service.json 3333 def myfunction(a, b): 3334 return [{a: b}] 3335 def call(): 3336 return service() 3337 3338 Then call it with:: 3339 3340 wget http://..../app/default/call/json/myfunction?a=hello&b=world 3341 3342 """ 3343 self.json_procedures[f.__name__] = f 3344 return f
3345
3346 - def jsonrpc(self, f):
3347 """ 3348 example:: 3349 3350 service = Service(globals()) 3351 @service.jsonrpc 3352 def myfunction(a, b): 3353 return a + b 3354 def call(): 3355 return service() 3356 3357 Then call it with:: 3358 3359 wget http://..../app/default/call/jsonrpc/myfunction?a=hello&b=world 3360 3361 """ 3362 self.jsonrpc_procedures[f.__name__] = f 3363 return f
3364
3365 - def xmlrpc(self, f):
3366 """ 3367 example:: 3368 3369 service = Service(globals()) 3370 @service.xmlrpc 3371 def myfunction(a, b): 3372 return a + b 3373 def call(): 3374 return service() 3375 3376 The call it with:: 3377 3378 wget http://..../app/default/call/xmlrpc/myfunction?a=hello&b=world 3379 3380 """ 3381 self.xmlrpc_procedures[f.__name__] = f 3382 return f
3383
3384 - def amfrpc(self, f):
3385 """ 3386 example:: 3387 3388 service = Service(globals()) 3389 @service.amfrpc 3390 def myfunction(a, b): 3391 return a + b 3392 def call(): 3393 return service() 3394 3395 The call it with:: 3396 3397 wget http://..../app/default/call/amfrpc/myfunction?a=hello&b=world 3398 3399 """ 3400 self.amfrpc_procedures[f.__name__] = f 3401 return f
3402
3403 - def amfrpc3(self, domain='default'):
3404 """ 3405 example:: 3406 3407 service = Service(globals()) 3408 @service.amfrpc3('domain') 3409 def myfunction(a, b): 3410 return a + b 3411 def call(): 3412 return service() 3413 3414 The call it with:: 3415 3416 wget http://..../app/default/call/amfrpc3/myfunction?a=hello&b=world 3417 3418 """ 3419 if not isinstance(domain, str): 3420 raise SyntaxError, "AMF3 requires a domain for function" 3421 3422 def _amfrpc3(f): 3423 if domain: 3424 self.amfrpc3_procedures[domain+'.'+f.__name__] = f 3425 else: 3426 self.amfrpc3_procedures[f.__name__] = f 3427 return f
3428 return _amfrpc3
3429
3430 - def soap(self, name=None, returns=None, args=None,doc=None):
3431 """ 3432 example:: 3433 3434 service = Service(globals()) 3435 @service.soap('MyFunction',returns={'result':int},args={'a':int,'b':int,}) 3436 def myfunction(a, b): 3437 return a + b 3438 def call(): 3439 return service() 3440 3441 The call it with:: 3442 3443 from gluon.contrib.pysimplesoap.client import SoapClient 3444 client = SoapClient(wsdl="http://..../app/default/call/soap?WSDL") 3445 response = client.MyFunction(a=1,b=2) 3446 return response['result'] 3447 3448 Exposes online generated documentation and xml example messages at: 3449 - http://..../app/default/call/soap 3450 """ 3451 3452 def _soap(f): 3453 self.soap_procedures[name or f.__name__] = f, returns, args, doc 3454 return f
3455 return _soap 3456
3457 - def serve_run(self, args=None):
3458 request = self.environment['request'] 3459 if not args: 3460 args = request.args 3461 if args and args[0] in self.run_procedures: 3462 return str(universal_caller(self.run_procedures[args[0]], 3463 *args[1:], **dict(request.vars))) 3464 self.error()
3465
3466 - def serve_csv(self, args=None):
3467 request = self.environment['request'] 3468 response = self.environment['response'] 3469 response.headers['Content-Type'] = 'text/x-csv' 3470 if not args: 3471 args = request.args 3472 3473 def none_exception(value): 3474 if isinstance(value, unicode): 3475 return value.encode('utf8') 3476 if hasattr(value, 'isoformat'): 3477 return value.isoformat()[:19].replace('T', ' ') 3478 if value == None: 3479 return '<NULL>' 3480 return value
3481 if args and args[0] in self.run_procedures: 3482 r = universal_caller(self.run_procedures[args[0]], 3483 *args[1:], **dict(request.vars)) 3484 s = cStringIO.StringIO() 3485 if hasattr(r, 'export_to_csv_file'): 3486 r.export_to_csv_file(s) 3487 elif r and isinstance(r[0], (dict, Storage)): 3488 import csv 3489 writer = csv.writer(s) 3490 writer.writerow(r[0].keys()) 3491 for line in r: 3492 writer.writerow([none_exception(v) \ 3493 for v in line.values()]) 3494 else: 3495 import csv 3496 writer = csv.writer(s) 3497 for line in r: 3498 writer.writerow(line) 3499 return s.getvalue() 3500 self.error() 3501
3502 - def serve_xml(self, args=None):
3503 request = self.environment['request'] 3504 response = self.environment['response'] 3505 response.headers['Content-Type'] = 'text/xml' 3506 if not args: 3507 args = request.args 3508 if args and args[0] in self.run_procedures: 3509 s = universal_caller(self.run_procedures[args[0]], 3510 *args[1:], **dict(request.vars)) 3511 if hasattr(s, 'as_list'): 3512 s = s.as_list() 3513 return serializers.xml(s) 3514 self.error()
3515
3516 - def serve_rss(self, args=None):
3517 request = self.environment['request'] 3518 response = self.environment['response'] 3519 if not args: 3520 args = request.args 3521 if args and args[0] in self.rss_procedures: 3522 feed = universal_caller(self.rss_procedures[args[0]], 3523 *args[1:], **dict(request.vars)) 3524 else: 3525 self.error() 3526 response.headers['Content-Type'] = 'application/rss+xml' 3527 return serializers.rss(feed)
3528
3529 - def serve_json(self, args=None):
3530 request = self.environment['request'] 3531 response = self.environment['response'] 3532 response.headers['Content-Type'] = 'text/x-json' 3533 if not args: 3534 args = request.args 3535 d = dict(request.vars) 3536 if args and args[0] in self.json_procedures: 3537 s = universal_caller(self.json_procedures[args[0]],*args[1:],**d) 3538 if hasattr(s, 'as_list'): 3539 s = s.as_list() 3540 return response.json(s) 3541 self.error()
3542
3543 - class JsonRpcException(Exception):
3544 - def __init__(self,code,info):
3545 self.code,self.info = code,info
3546
3547 - def serve_jsonrpc(self):
3548 import contrib.simplejson as simplejson 3549 def return_response(id, result): 3550 return simplejson.dumps({'version': '1.1', 3551 'id': id, 'result': result, 'error': None})
3552 3553 def return_error(id, code, message): 3554 return simplejson.dumps({'id': id, 3555 'version': '1.1', 3556 'error': {'name': 'JSONRPCError', 3557 'code': code, 'message': message} 3558 }) 3559 3560 request = self.environment['request'] 3561 methods = self.jsonrpc_procedures 3562 data = simplejson.loads(request.body.read()) 3563 id, method, params = data['id'], data['method'], data.get('params','') 3564 if not method in methods: 3565 return return_error(id, 100, 'method "%s" does not exist' % method) 3566 try: 3567 s = methods[method](*params) 3568 if hasattr(s, 'as_list'): 3569 s = s.as_list() 3570 return return_response(id, s) 3571 except Service.JsonRpcException, e: 3572 return return_error(id, e.code, e.info) 3573 except BaseException: 3574 etype, eval, etb = sys.exc_info() 3575 return return_error(id, 100, '%s: %s' % (etype.__name__, eval)) 3576 except: 3577 etype, eval, etb = sys.exc_info() 3578 return return_error(id, 100, 'Exception %s: %s' % (etype, eval)) 3579
3580 - def serve_xmlrpc(self):
3581 request = self.environment['request'] 3582 response = self.environment['response'] 3583 services = self.xmlrpc_procedures.values() 3584 return response.xmlrpc(request, services)
3585
3586 - def serve_amfrpc(self, version=0):
3587 try: 3588 import pyamf 3589 import pyamf.remoting.gateway 3590 except: 3591 return "pyamf not installed or not in Python sys.path" 3592 request = self.environment['request'] 3593 response = self.environment['response'] 3594 if version == 3: 3595 services = self.amfrpc3_procedures 3596 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 3597 pyamf_request = pyamf.remoting.decode(request.body) 3598 else: 3599 services = self.amfrpc_procedures 3600 base_gateway = pyamf.remoting.gateway.BaseGateway(services) 3601 context = pyamf.get_context(pyamf.AMF0) 3602 pyamf_request = pyamf.remoting.decode(request.body, context) 3603 pyamf_response = pyamf.remoting.Envelope(pyamf_request.amfVersion, 3604 pyamf_request.clientType) 3605 for name, message in pyamf_request: 3606 pyamf_response[name] = base_gateway.getProcessor(message)(message) 3607 response.headers['Content-Type'] = pyamf.remoting.CONTENT_TYPE 3608 if version==3: 3609 return pyamf.remoting.encode(pyamf_response).getvalue() 3610 else: 3611 return pyamf.remoting.encode(pyamf_response, context).getvalue()
3612
3613 - def serve_soap(self, version="1.1"):
3614 try: 3615 from contrib.pysimplesoap.server import SoapDispatcher 3616 except: 3617 return "pysimplesoap not installed in contrib" 3618 request = self.environment['request'] 3619 response = self.environment['response'] 3620 procedures = self.soap_procedures 3621 3622 location = "%s://%s%s" % ( 3623 request.env.wsgi_url_scheme, 3624 request.env.http_host, 3625 URL(r=request,f="call/soap",vars={})) 3626 namespace = 'namespace' in response and response.namespace or location 3627 documentation = response.description or '' 3628 dispatcher = SoapDispatcher( 3629 name = response.title, 3630 location = location, 3631 action = location, # SOAPAction 3632 namespace = namespace, 3633 prefix='pys', 3634 documentation = documentation, 3635 ns = True) 3636 for method, (function, returns, args, doc) in procedures.items(): 3637 dispatcher.register_function(method, function, returns, args, doc) 3638 if request.env.request_method == 'POST': 3639 # Process normal Soap Operation 3640 response.headers['Content-Type'] = 'text/xml' 3641 return dispatcher.dispatch(request.body.read()) 3642 elif 'WSDL' in request.vars: 3643 # Return Web Service Description 3644 response.headers['Content-Type'] = 'text/xml' 3645 return dispatcher.wsdl() 3646 elif 'op' in request.vars: 3647 # Return method help webpage 3648 response.headers['Content-Type'] = 'text/html' 3649 method = request.vars['op'] 3650 sample_req_xml, sample_res_xml, doc = dispatcher.help(method) 3651 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 3652 A("See all webservice operations", 3653 _href=URL(r=request,f="call/soap",vars={})), 3654 H2(method), 3655 P(doc), 3656 UL(LI("Location: %s" % dispatcher.location), 3657 LI("Namespace: %s" % dispatcher.namespace), 3658 LI("SoapAction: %s" % dispatcher.action), 3659 ), 3660 H3("Sample SOAP XML Request Message:"), 3661 CODE(sample_req_xml,language="xml"), 3662 H3("Sample SOAP XML Response Message:"), 3663 CODE(sample_res_xml,language="xml"), 3664 ] 3665 return {'body': body} 3666 else: 3667 # Return general help and method list webpage 3668 response.headers['Content-Type'] = 'text/html' 3669 body = [H1("Welcome to Web2Py SOAP webservice gateway"), 3670 P(response.description), 3671 P("The following operations are available"), 3672 A("See WSDL for webservice description", 3673 _href=URL(r=request,f="call/soap",vars={"WSDL":None})), 3674 UL([LI(A("%s: %s" % (method, doc or ''), 3675 _href=URL(r=request,f="call/soap",vars={'op': method}))) 3676 for method, doc in dispatcher.list_methods()]), 3677 ] 3678 return {'body': body}
3679
3680 - def __call__(self):
3681 """ 3682 register services with: 3683 service = Service(globals()) 3684 @service.run 3685 @service.rss 3686 @service.json 3687 @service.jsonrpc 3688 @service.xmlrpc 3689 @service.jsonrpc 3690 @service.amfrpc 3691 @service.amfrpc3('domain') 3692 @service.soap('Method', returns={'Result':int}, args={'a':int,'b':int,}) 3693 3694 expose services with 3695 3696 def call(): return service() 3697 3698 call services with 3699 http://..../app/default/call/run?[parameters] 3700 http://..../app/default/call/rss?[parameters] 3701 http://..../app/default/call/json?[parameters] 3702 http://..../app/default/call/jsonrpc 3703 http://..../app/default/call/xmlrpc 3704 http://..../app/default/call/amfrpc 3705 http://..../app/default/call/amfrpc3 3706 http://..../app/default/call/soap 3707 """ 3708 3709 request = self.environment['request'] 3710 if len(request.args) < 1: 3711 raise HTTP(404, "Not Found") 3712 arg0 = request.args(0) 3713 if arg0 == 'run': 3714 return self.serve_run(request.args[1:]) 3715 elif arg0 == 'rss': 3716 return self.serve_rss(request.args[1:]) 3717 elif arg0 == 'csv': 3718 return self.serve_csv(request.args[1:]) 3719 elif arg0 == 'xml': 3720 return self.serve_xml(request.args[1:]) 3721 elif arg0 == 'json': 3722 return self.serve_json(request.args[1:]) 3723 elif arg0 == 'jsonrpc': 3724 return self.serve_jsonrpc() 3725 elif arg0 == 'xmlrpc': 3726 return self.serve_xmlrpc() 3727 elif arg0 == 'amfrpc': 3728 return self.serve_amfrpc() 3729 elif arg0 == 'amfrpc3': 3730 return self.serve_amfrpc(3) 3731 elif arg0 == 'soap': 3732 return self.serve_soap() 3733 else: 3734 self.error()
3735
3736 - def error(self):
3737 raise HTTP(404, "Object does not exist")
3738 3739
3740 -def completion(callback):
3741 """ 3742 Executes a task on completion of the called action. For example: 3743 3744 from gluon.tools import completion 3745 @completion(lambda d: logging.info(repr(d))) 3746 def index(): 3747 return dict(message='hello') 3748 3749 It logs the output of the function every time input is called. 3750 The argument of completion is executed in a new thread. 3751 """ 3752 def _completion(f): 3753 def __completion(*a,**b): 3754 d = None 3755 try: 3756 d = f(*a,**b) 3757 return d 3758 finally: 3759 thread.start_new_thread(callback,(d,))
3760 return __completion 3761 return _completion 3762
3763 -def prettydate(d,T=lambda x:x):
3764 try: 3765 dt = datetime.datetime.now() - d 3766 except: 3767 return '' 3768 if dt.days >= 2*365: 3769 return T('%d years ago') % int(dt.days / 365) 3770 elif dt.days >= 365: 3771 return T('1 year ago') 3772 elif dt.days >= 60: 3773 return T('%d months ago') % int(dt.days / 30) 3774 elif dt.days > 21: 3775 return T('1 month ago') 3776 elif dt.days >= 14: 3777 return T('%d weeks ago') % int(dt.days / 7) 3778 elif dt.days >= 7: 3779 return T('1 week ago') 3780 elif dt.days > 1: 3781 return T('%d days ago') % dt.days 3782 elif dt.days == 1: 3783 return T('1 day ago') 3784 elif dt.seconds >= 2*60*60: 3785 return T('%d hours ago') % int(dt.seconds / 3600) 3786 elif dt.seconds >= 60*60: 3787 return T('1 hour ago') 3788 elif dt.seconds >= 2*60: 3789 return T('%d minutes ago') % int(dt.seconds / 60) 3790 elif dt.seconds >= 60: 3791 return T('1 minute ago') 3792 elif dt.seconds > 1: 3793 return T('%d seconds ago') % dt.seconds 3794 elif dt.seconds == 1: 3795 return T('1 second ago') 3796 else: 3797 return T('now')
3798
3799 -def test_thread_separation():
3800 def f(): 3801 c=PluginManager() 3802 lock1.acquire() 3803 lock2.acquire() 3804 c.x=7 3805 lock1.release() 3806 lock2.release()
3807 lock1=thread.allocate_lock() 3808 lock2=thread.allocate_lock() 3809 lock1.acquire() 3810 thread.start_new_thread(f,()) 3811 a=PluginManager() 3812 a.x=5 3813 lock1.release() 3814 lock2.acquire() 3815 return a.x 3816
3817 -class PluginManager(object):
3818 """ 3819 3820 Plugin Manager is similar to a storage object but it is a single level singleton 3821 this means that multiple instances within the same thread share the same attributes 3822 Its constructor is also special. The first argument is the name of the plugin you are defining. 3823 The named arguments are parameters needed by the plugin with default values. 3824 If the parameters were previous defined, the old values are used. 3825 3826 For example: 3827 3828 ### in some general configuration file: 3829 >>> plugins = PluginManager() 3830 >>> plugins.me.param1=3 3831 3832 ### within the plugin model 3833 >>> _ = PluginManager('me',param1=5,param2=6,param3=7) 3834 3835 ### where the plugin is used 3836 >>> print plugins.me.param1 3837 3 3838 >>> print plugins.me.param2 3839 6 3840 >>> plugins.me.param3 = 8 3841 >>> print plugins.me.param3 3842 8 3843 3844 Here are some tests: 3845 3846 >>> a=PluginManager() 3847 >>> a.x=6 3848 >>> b=PluginManager('check') 3849 >>> print b.x 3850 6 3851 >>> b=PluginManager() # reset settings 3852 >>> print b.x 3853 <Storage {}> 3854 >>> b.x=7 3855 >>> print a.x 3856 7 3857 >>> a.y.z=8 3858 >>> print b.y.z 3859 8 3860 >>> test_thread_separation() 3861 5 3862 >>> plugins=PluginManager('me',db='mydb') 3863 >>> print plugins.me.db 3864 mydb 3865 >>> print 'me' in plugins 3866 True 3867 >>> print plugins.me.installed 3868 True 3869 """ 3870 instances = {}
3871 - def __new__(cls,*a,**b):
3872 id = thread.get_ident() 3873 lock = thread.allocate_lock() 3874 try: 3875 lock.acquire() 3876 try: 3877 return cls.instances[id] 3878 except KeyError: 3879 instance = object.__new__(cls,*a,**b) 3880 cls.instances[id] = instance 3881 return instance 3882 finally: 3883 lock.release()
3884 - def __init__(self,plugin=None,**defaults):
3885 if not plugin: 3886 self.__dict__.clear() 3887 settings = self.__getattr__(plugin) 3888 settings.installed = True 3889 [settings.update({key:value}) for key,value in defaults.items() if not key in settings]
3890 - def __getattr__(self, key):
3891 if not key in self.__dict__: 3892 self.__dict__[key] = Storage() 3893 return self.__dict__[key]
3894 - def keys(self):
3895 return self.__dict__.keys()
3896 - def __contains__(self,key):
3897 return key in self.__dict__
3898 3899 if __name__ == '__main__': 3900 import doctest 3901 doctest.testmod() 3902