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