From 02f0bdb8d3815e313f03a239b719fa02244fada7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Nov 2016 16:37:22 +0000 Subject: [PATCH 001/149] Auth refactor, extracted many methods into a base class for more generic auth mechanisms. Partially addresses #1526 Includes a solution for IS_LOWER and IS_UPPER validator problems I mentioned in #1353 --- gluon/authapi.py | 1020 +++++++++++++++++++++++++++++++ gluon/tests/__init__.py | 1 + gluon/tests/test_authapi.py | 171 ++++++ gluon/tests/test_validators.py | 8 +- gluon/tools.py | 1034 +++++++------------------------- gluon/validators.py | 17 +- 6 files changed, 1438 insertions(+), 813 deletions(-) create mode 100644 gluon/authapi.py create mode 100644 gluon/tests/test_authapi.py diff --git a/gluon/authapi.py b/gluon/authapi.py new file mode 100644 index 00000000..981a314c --- /dev/null +++ b/gluon/authapi.py @@ -0,0 +1,1020 @@ +# -*- coding: utf-8 -*- +""" +| This file is part of the web2py Web Framework +| Copyrighted by Massimo Di Pierro +| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) +""" +from gluon import current +from gluon.storage import Messages, Settings, Storage +from gluon.utils import web2py_uuid +from gluon.validators import CRYPT, IS_EMAIL, IS_EQUAL_TO, IS_INT_IN_RANGE, IS_LOWER, IS_MATCH, IS_NOT_EMPTY, IS_NOT_IN_DB +from pydal.objects import Table, Field, Row +import datetime +from gluon.settings import global_settings + +DEFAULT = lambda: None + + +class AuthAPI(object): + """ + AuthAPI is a barebones Auth implementation which does not have a concept of + HTML forms or redirects, emailing or even an URL, you are responsible for + all that if you use it. + The main Auth functions such as login, logout, register, profile are designed + in a Dict In -> Dict Out logic so, for instance, if you set + registration_requires_verification you are responsible for sending the key to + the user and even rolling back the transaction if you can't do it. + + NOTES: * It does not support all the callbacks Traditional Auth does yet. + Some of the callbacks will not be supported. + Check the method signatures to find out which ones are supported. + * register_fields and profile_fields settings are ignored for now. + + WARNING: No builtin CSRF protection whatsoever. + """ + + default_settings = { + 'create_user_groups': 'user_%(id)s', + 'email_case_sensitive': False, + 'everybody_group_id': None, + 'expiration': 3600, + 'keep_session_onlogin': True, + 'keep_session_onlogout': False, + 'logging_enabled': True, + 'login_after_registration': False, + 'login_email_validate': True, + 'login_userfield': None, + 'logout_onlogout': None, + 'long_expiration': 3600 * 24 * 30, + 'ondelete': 'CASCADE', + 'password_field': 'password', + 'password_min_length': 4, + 'registration_requires_approval': False, + 'registration_requires_verification': False, + 'renew_session_onlogin': True, + 'renew_session_onlogout': True, + 'table_event_name': 'auth_event', + 'table_group_name': 'auth_group', + 'table_membership_name': 'auth_membership', + 'table_permission_name': 'auth_permission', + 'table_user_name': 'auth_user', + 'use_username': False, + 'username_case_sensitive': True + } + + default_messages = { + 'add_group_log': 'Group %(group_id)s created', + 'add_membership_log': None, + 'add_permission_log': None, + 'change_password_log': 'User %(id)s Password changed', + 'del_group_log': 'Group %(group_id)s deleted', + 'del_membership_log': None, + 'del_permission_log': None, + 'email_taken': 'This email already has an account', + 'group_description': 'Group uniquely assigned to user %(id)s', + 'has_membership_log': None, + 'has_permission_log': None, + 'invalid_email': 'Invalid email', + 'key_verified': 'Key verified', + 'invalid_login': 'Invalid login', + 'invalid_password': 'Invalid password', + 'invalid_user': 'Invalid user', + 'invalid_key': 'Invalid key', + 'invalid_username': 'Invalid username', + 'logged_in': 'Logged in', + 'logged_out': 'Logged out', + 'login_failed_log': None, + 'login_log': 'User %(id)s Logged-in', + 'logout_log': 'User %(id)s Logged-out', + 'mismatched_password': "Password fields don't match", + 'password_changed': 'Password changed', + 'profile_log': 'User %(id)s Profile updated', + 'profile_updated': 'Profile updated', + 'register_log': 'User %(id)s Registered', + 'registration_pending': 'Registration is pending approval', + 'registration_successful': 'Registration successful', + 'registration_verifying': 'Registration needs verification', + 'username_taken': 'Username already taken', + 'verify_log': 'User %(id)s verified registration key' + } + + def __init__(self, db=None, hmac_key=None, signature=True): + self.db = db + session = current.session + auth = session.auth + self.user_groups = auth and auth.user_groups or {} + now = current.request.now + # if we have auth info + # if not expired it, used it + # if expired, clear the session + # else, only clear auth info in the session + if auth: + delta = datetime.timedelta(days=0, seconds=auth.expiration) + if auth.last_visit and auth.last_visit + delta > now: + self.user = auth.user + # this is a trick to speed up sessions to avoid many writes + if (now - auth.last_visit).seconds > (auth.expiration / 10): + auth.last_visit = now + else: + self.user = None + if session.auth: + del session.auth + session.renew(clear_session=True) + else: + self.user = None + if session.auth: + del session.auth + + settings = self.settings = Settings(self.__class__.default_settings) + settings.update( + extra_fields={}, + hmac_key=hmac_key, + ) + settings.lock_keys = True + messages = self.messages = Messages(current.T) + messages.update(self.default_messages) + messages.lock_keys = True + if signature: + self.define_signature() + else: + self.signature = None + + def __validate(self, value, requires): + if not isinstance(requires, (list, tuple)): + requires = [requires] + for validator in requires: + (value, error) = validator(value) + if error: + return (value, error) + return (value, None) + + def _get_migrate(self, tablename, migrate=True): + + if type(migrate).__name__ == 'str': + return (migrate + tablename + '.table') + elif migrate == False: + return False + else: + return True + + def _get_user_id(self): + """accessor for auth.user_id""" + return self.user and self.user.id or None + + user_id = property(_get_user_id, doc="user.id or None") + + def table_user(self): + return self.db[self.settings.table_user_name] + + def table_group(self): + return self.db[self.settings.table_group_name] + + def table_membership(self): + return self.db[self.settings.table_membership_name] + + def table_permission(self): + return self.db[self.settings.table_permission_name] + + def table_event(self): + return self.db[self.settings.table_event_name] + + def define_signature(self): + db = self.db + settings = self.settings + request = current.request + T = current.T + reference_user = 'reference %s' % settings.table_user_name + + def lazy_user(auth=self): + return auth.user_id + + def represent(id, record=None, s=settings): + try: + user = s.table_user(id) + return '%s %s' % (user.get("first_name", user.get("email")), + user.get("last_name", '')) + except: + return id + ondelete = self.settings.ondelete + self.signature = Table( + self.db, 'auth_signature', + Field('is_active', 'boolean', + default=True, + readable=False, writable=False, + label=T('Is Active')), + Field('created_on', 'datetime', + default=request.now, + writable=False, readable=False, + label=T('Created On')), + Field('created_by', + reference_user, + default=lazy_user, represent=represent, + writable=False, readable=False, + label=T('Created By'), ondelete=ondelete), + Field('modified_on', 'datetime', + update=request.now, default=request.now, + writable=False, readable=False, + label=T('Modified On')), + Field('modified_by', + reference_user, represent=represent, + default=lazy_user, update=lazy_user, + writable=False, readable=False, + label=T('Modified By'), ondelete=ondelete)) + + def define_tables(self, username=None, signature=None, migrate=None, + fake_migrate=None): + """ + To be called unless tables are defined manually + + Examples: + Use as:: + + # defines all needed tables and table files + # 'myprefix_auth_user.table', ... + auth.define_tables(migrate='myprefix_') + + # defines all needed tables without migration/table files + auth.define_tables(migrate=False) + + """ + + db = self.db + if migrate is None: + migrate = db._migrate + if fake_migrate is None: + fake_migrate = db._fake_migrate + settings = self.settings + if username is None: + username = settings.use_username + else: + settings.use_username = username + if not self.signature: + self.define_signature() + if signature: + signature_list = [self.signature] + elif not signature: + signature_list = [] + elif isinstance(signature, Table): + signature_list = [signature] + else: + signature_list = signature + is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) + is_crypted = CRYPT(key=settings.hmac_key, + min_length=settings.password_min_length) + is_unique_email = [ + IS_EMAIL(error_message=self.messages.invalid_email), + IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name, + error_message=self.messages.email_taken)] + if not settings.email_case_sensitive: + is_unique_email.insert(1, IS_LOWER()) + if settings.table_user_name not in db.tables: + passfield = settings.password_field + extra_fields = settings.extra_fields.get( + settings.table_user_name, []) + signature_list + if username or settings.cas_provider: # cas_provider Will always be None here but we compare it anyway so subclasses can use our define_tables + is_unique_username = \ + [IS_MATCH('[\w\.\-]+', strict=True, + error_message=self.messages.invalid_username), + IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name, + error_message=self.messages.username_taken)] + if not settings.username_case_sensitive: + is_unique_username.insert(1, IS_LOWER()) + db.define_table( + settings.table_user_name, + Field('first_name', length=128, default='', + label=self.messages.label_first_name, + requires=is_not_empty), + Field('last_name', length=128, default='', + label=self.messages.label_last_name, + requires=is_not_empty), + Field('email', length=512, default='', + label=self.messages.label_email, + requires=is_unique_email), + Field('username', length=128, default='', + label=self.messages.label_username, + requires=is_unique_username), + Field(passfield, 'password', length=512, + readable=False, label=self.messages.label_password, + requires=[is_crypted]), + Field('registration_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_key), + Field('reset_password_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_reset_password_key), + Field('registration_id', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_id), + *extra_fields, + **dict( + migrate=self._get_migrate(settings.table_user_name, + migrate), + fake_migrate=fake_migrate, + format='%(username)s')) + else: + db.define_table( + settings.table_user_name, + Field('first_name', length=128, default='', + label=self.messages.label_first_name, + requires=is_not_empty), + Field('last_name', length=128, default='', + label=self.messages.label_last_name, + requires=is_not_empty), + Field('email', length=512, default='', + label=self.messages.label_email, + requires=is_unique_email), + Field(passfield, 'password', length=512, + readable=False, label=self.messages.label_password, + requires=[is_crypted]), + Field('registration_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_key), + Field('reset_password_key', length=512, + writable=False, readable=False, default='', + label=self.messages.label_reset_password_key), + Field('registration_id', length=512, + writable=False, readable=False, default='', + label=self.messages.label_registration_id), + *extra_fields, + **dict( + migrate=self._get_migrate(settings.table_user_name, + migrate), + fake_migrate=fake_migrate, + format='%(first_name)s %(last_name)s (%(id)s)')) + reference_table_user = 'reference %s' % settings.table_user_name + if settings.table_group_name not in db.tables: + extra_fields = settings.extra_fields.get( + settings.table_group_name, []) + signature_list + db.define_table( + settings.table_group_name, + Field('role', length=512, default='', + label=self.messages.label_role, + requires=IS_NOT_IN_DB(db, '%s.role' % settings.table_group_name)), + Field('description', 'text', + label=self.messages.label_description), + *extra_fields, + **dict( + migrate=self._get_migrate( + settings.table_group_name, migrate), + fake_migrate=fake_migrate, + format='%(role)s (%(id)s)')) + reference_table_group = 'reference %s' % settings.table_group_name + if settings.table_membership_name not in db.tables: + extra_fields = settings.extra_fields.get( + settings.table_membership_name, []) + signature_list + db.define_table( + settings.table_membership_name, + Field('user_id', reference_table_user, + label=self.messages.label_user_id), + Field('group_id', reference_table_group, + label=self.messages.label_group_id), + *extra_fields, + **dict( + migrate=self._get_migrate( + settings.table_membership_name, migrate), + fake_migrate=fake_migrate)) + if settings.table_permission_name not in db.tables: + extra_fields = settings.extra_fields.get( + settings.table_permission_name, []) + signature_list + db.define_table( + settings.table_permission_name, + Field('group_id', reference_table_group, + label=self.messages.label_group_id), + Field('name', default='default', length=512, + label=self.messages.label_name, + requires=is_not_empty), + Field('table_name', length=512, + label=self.messages.label_table_name), + Field('record_id', 'integer', default=0, + label=self.messages.label_record_id, + requires=IS_INT_IN_RANGE(0, 10 ** 9)), + *extra_fields, + **dict( + migrate=self._get_migrate( + settings.table_permission_name, migrate), + fake_migrate=fake_migrate)) + if settings.table_event_name not in db.tables: + db.define_table( + settings.table_event_name, + Field('time_stamp', 'datetime', + default=current.request.now, + label=self.messages.label_time_stamp), + Field('client_ip', + default=current.request.client, + label=self.messages.label_client_ip), + Field('user_id', reference_table_user, default=None, + label=self.messages.label_user_id), + Field('origin', default='auth', length=512, + label=self.messages.label_origin, + requires=is_not_empty), + Field('description', 'text', default='', + label=self.messages.label_description, + requires=is_not_empty), + *settings.extra_fields.get(settings.table_event_name, []), + **dict( + migrate=self._get_migrate( + settings.table_event_name, migrate), + fake_migrate=fake_migrate)) + + return self + + def log_event(self, description, vars=None, origin='auth'): + """ + Examples: + Use as:: + + auth.log_event(description='this happened', origin='auth') + + """ + if not self.settings.logging_enabled or not description: + return + elif self.is_logged_in(): + user_id = self.user.id + else: + user_id = None # user unknown + vars = vars or {} + # log messages should not be translated + if type(description).__name__ == 'lazyT': + description = description.m + self.table_event().insert(description=str(description % vars), origin=origin, user_id=user_id) + + def id_group(self, role): + """ + Returns the group_id of the group specified by the role + """ + rows = self.db(self.table_group().role == role).select() + if not rows: + return None + return rows[0].id + + def user_group(self, user_id=None): + """ + Returns the group_id of the group uniquely associated to this user + i.e. `role=user:[user_id]` + """ + return self.id_group(self.user_group_role(user_id)) + + def user_group_role(self, user_id=None): + if not self.settings.create_user_groups: + return None + if user_id: + user = self.table_user()[user_id] + else: + user = self.user + return self.settings.create_user_groups % user + + def add_group(self, role, description=''): + """ + Creates a group associated to a role + """ + group_id = self.table_group().insert(role=role, description=description) + self.log_event(self.messages['add_group_log'], dict(group_id=group_id, role=role)) + return group_id + + def del_group(self, group_id): + """ + Deletes a group + """ + self.db(self.table_group().id == group_id).delete() + self.db(self.table_membership().group_id == group_id).delete() + self.db(self.table_permission().group_id == group_id).delete() + if group_id in self.user_groups: + del self.user_groups[group_id] + self.log_event(self.messages.del_group_log, dict(group_id=group_id)) + + def update_groups(self): + if not self.user: + return + user_groups = self.user_groups = {} + if current.session.auth: + current.session.auth.user_groups = self.user_groups + table_group = self.table_group() + table_membership = self.table_membership() + memberships = self.db( + table_membership.user_id == self.user.id).select() + for membership in memberships: + group = table_group(membership.group_id) + if group: + user_groups[membership.group_id] = group.role + + def add_membership(self, group_id=None, user_id=None, role=None): + """ + Gives user_id membership of group_id or role + if user is None than user_id is that of current logged in user + """ + + group_id = group_id or self.id_group(role) + try: + group_id = int(group_id) + except: + group_id = self.id_group(group_id) # interpret group_id as a role + if not user_id and self.user: + user_id = self.user.id + if not group_id: + raise ValueError('group_id not provided or invalid') + if not user_id: + raise ValueError('user_id not provided or invalid') + membership = self.table_membership() + db = membership._db + record = db((membership.user_id == user_id) & + (membership.group_id == group_id), + ignore_common_filters=True).select().first() + if record: + if hasattr(record, 'is_active') and not record.is_active: + record.update_record(is_active=True) + return record.id + else: + id = membership.insert(group_id=group_id, user_id=user_id) + if role: + self.user_groups[group_id] = role + else: + self.update_groups() + self.log_event(self.messages['add_membership_log'], + dict(user_id=user_id, group_id=group_id)) + return id + + def del_membership(self, group_id=None, user_id=None, role=None): + """ + Revokes membership from group_id to user_id + if user_id is None than user_id is that of current logged in user + """ + + group_id = group_id or self.id_group(role) + try: + group_id = int(group_id) + except: + group_id = self.id_group(group_id) # interpret group_id as a role + if not user_id and self.user: + user_id = self.user.id + membership = self.table_membership() + self.log_event(self.messages['del_membership_log'], + dict(user_id=user_id, group_id=group_id)) + ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete() + if group_id in self.user_groups: + del self.user_groups[group_id] + return ret + + def has_membership(self, group_id=None, user_id=None, role=None): + """ + Checks if user is member of group_id or role + """ + group_id = group_id or self.id_group(role) + try: + group_id = int(group_id) + except: + group_id = self.id_group(group_id) # interpret group_id as a role + if not user_id and self.user: + user_id = self.user.id + membership = self.table_membership() + if group_id and user_id and self.db((membership.user_id == user_id) & + (membership.group_id == group_id)).select(): + r = True + else: + r = False + self.log_event(self.messages['has_membership_log'], + dict(user_id=user_id, group_id=group_id, check=r)) + return r + + def add_permission(self, + group_id, + name='any', + table_name='', + record_id=0, + ): + """ + Gives group_id 'name' access to 'table_name' and 'record_id' + """ + + permission = self.table_permission() + if group_id == 0: + group_id = self.user_group() + record = self.db((permission.group_id == group_id) & + (permission.name == name) & + (permission.table_name == str(table_name)) & + (permission.record_id == long(record_id)), + ignore_common_filters=True + ).select(limitby=(0, 1), orderby_on_limitby=False).first() + if record: + if hasattr(record, 'is_active') and not record.is_active: + record.update_record(is_active=True) + id = record.id + else: + id = permission.insert(group_id=group_id, name=name, + table_name=str(table_name), + record_id=long(record_id)) + self.log_event(self.messages['add_permission_log'], + dict(permission_id=id, group_id=group_id, + name=name, table_name=table_name, + record_id=record_id)) + return id + + def del_permission(self, + group_id, + name='any', + table_name='', + record_id=0, + ): + """ + Revokes group_id 'name' access to 'table_name' and 'record_id' + """ + + permission = self.table_permission() + self.log_event(self.messages['del_permission_log'], + dict(group_id=group_id, name=name, + table_name=table_name, record_id=record_id)) + return self.db(permission.group_id == + group_id)(permission.name == + name)(permission.table_name == + str(table_name))(permission.record_id == + long(record_id)).delete() + + def has_permission(self, + name='any', + table_name='', + record_id=0, + user_id=None, + group_id=None, + ): + """ + Checks if user_id or current logged in user is member of a group + that has 'name' permission on 'table_name' and 'record_id' + if group_id is passed, it checks whether the group has the permission + """ + + if not group_id and self.settings.everybody_group_id and \ + self.has_permission(name, table_name, record_id, user_id=None, + group_id=self.settings.everybody_group_id): + return True + + if not user_id and not group_id and self.user: + user_id = self.user.id + if user_id: + membership = self.table_membership() + rows = self.db(membership.user_id == user_id).select(membership.group_id) + groups = set([row.group_id for row in rows]) + if group_id and group_id not in groups: + return False + else: + groups = set([group_id]) + permission = self.table_permission() + rows = self.db(permission.name == + name)(permission.table_name == + str(table_name))(permission.record_id == + record_id).select(permission.group_id) + groups_required = set([row.group_id for row in rows]) + if record_id: + rows = self.db(permission.name == + name)(permission.table_name == + str(table_name))(permission.record_id == + 0).select(permission.group_id) + groups_required = groups_required.union(set([row.group_id for row in rows])) + if groups.intersection(groups_required): + r = True + else: + r = False + if user_id: + self.log_event(self.messages['has_permission_log'], + dict(user_id=user_id, name=name, + table_name=table_name, record_id=record_id)) + return r + + def is_logged_in(self): + """ + Checks if the user is logged in and returns True/False. + If so user is in auth.user as well as in session.auth.user + """ + if self.user: + return True + return False + + def login_user(self, user): + """ + Logins the `user = db.auth_user(id)` + """ + if global_settings.web2py_runtime_gae: + user = Row(self.table_user()._filter_fields(user, id=True)) + delattr(user, 'password') + else: + user = Row(user) + for key in list(user.keys()): + value = user[key] + if callable(value) or key == 'password': + delattr(user, key) + if self.settings.renew_session_onlogin: + current.session.renew(clear_session=not self.settings.keep_session_onlogin) + current.session.auth = Storage(user=user, + last_visit=current.request.now, + expiration=self.settings.expiration, + hmac_key=web2py_uuid()) + self.user = user + self.update_groups() + + def login(self, log=DEFAULT, **kwargs): + """ + Login a user + + Keyword Args: + username/email/name_of_your_username_field (string) - username + password/name_of_your_passfield (string) - user's password + remember_me (boolean) - extend the duration of the login to settings.long_expiration + """ + settings = self.settings + session = current.session + table_user = self.table_user() + + if 'username' in table_user.fields or \ + not settings.login_email_validate: + userfield_validator = IS_NOT_EMPTY(error_message=self.messages.is_empty) + if not settings.username_case_sensitive: + userfield_validator = [IS_LOWER(), userfield_validator] + else: + userfield_validator = IS_EMAIL(error_message=self.messages.invalid_email) + if not settings.email_case_sensitive: + userfield_validator = [IS_LOWER(), userfield_validator] + + passfield = settings.password_field + + if log is DEFAULT: + log = self.messages['login_log'] + + user = None + + # Setup the default field used for the userfield + if self.settings.login_userfield: + userfield = self.settings.login_userfield + else: + if 'username' in table_user.fields: + userfield = 'username' + else: + userfield = 'email' + + # Get the userfield from kwargs and validate it + userfield_value = kwargs.get(userfield) + if userfield_value is None: + raise KeyError('%s not found in kwargs' % userfield) + + validated, error = self.__validate(userfield_value, userfield_validator) + + if error: + return {'errors': {userfield: error}, 'message': self.messages.invalid_login, 'user': None} + + # Get the user for this userfield and check it + user = table_user(**{userfield: validated}) + + if user is None: + return {'errors': {userfield: self.messages.invalid_user}, 'message': self.messages.invalid_login, 'user': None} + + if (user.registration_key or '').startswith('pending'): + return {'errors': None, 'message': self.messages.registration_pending, 'user': None} + elif user.registration_key in ('disabled', 'blocked'): + return {'errors': None, 'message': self.messages.login_disabled, 'user': None} + elif (user.registration_key is not None and user.registration_key.strip()): + return {'errors': None, 'message': self.messages.registration_verifying, 'user': None} + + # Finally verify the password + passfield = settings.password_field + password = table_user[passfield].validate(kwargs.get(passfield, ''))[0] + + if password == user[passfield]: + self.login_user(user) + session.auth.expiration = \ + kwargs.get('remember_me', False) and \ + settings.long_expiration or \ + settings.expiration + session.auth.remember_me = kwargs.get('remember_me', False) + self.log_event(log, user) + return {'errors': None, 'message': self.messages.logged_in, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + else: + self.log_event(self.messages['login_failed_log'], kwargs) + return {'errors': {passfield: self.messages.invalid_password}, 'message': self.messages.invalid_login, 'user': None} + + def logout(self, log=DEFAULT, onlogout=DEFAULT, **kwargs): + """ + Logs out user + """ + settings = self.settings + session = current.session + + if onlogout is DEFAULT: + onlogout = settings.logout_onlogout + if onlogout: + onlogout(self.user) + if log is DEFAULT: + log = self.messages['logout_log'] + if self.user: + self.log_event(log, self.user) + + session.auth = None + self.user = None + if settings.renew_session_onlogout: + session.renew(clear_session=not settings.keep_session_onlogout) + + return {'errors': None, 'message': self.messages.logged_out, 'user': None} + + def register(self, log=DEFAULT, **kwargs): + """ + Register a user. + """ + + table_user = self.table_user() + settings = self.settings + + if self.is_logged_in(): + raise AssertionError('User trying to register is logged in') + + if log is DEFAULT: + log = self.messages['register_log'] + + if self.settings.login_userfield: + userfield = self.settings.login_userfield + elif 'username' in table_user.fields: + userfield = 'username' + else: + userfield = 'email' + + # Ensure the username field is unique. + unique_validator = IS_NOT_IN_DB(self.db, table_user[userfield]) + userfield_validator = table_user[userfield].requires + if userfield_validator is None: + userfield_validator = unique_validator + elif isinstance(userfield_validator, (list, tuple)): + if not any([isinstance(validator, IS_NOT_IN_DB) for validator in + userfield_validator]): + if isinstance(userfield_validator, list): + userfield_validator.append(unique_validator) + else: + userfield_validator += (unique_validator, ) + elif not isinstance(userfield_validator, IS_NOT_IN_DB): + userfield_validator = [userfield_validator, unique_validator] + table_user[userfield].requires = userfield_validator + + passfield = settings.password_field + + try: # Make sure we have our original minimum length + table_user[passfield].requires[-1].min_length = settings.password_min_length + except: + pass + + key = web2py_uuid() + if settings.registration_requires_approval: + key = 'pending-' + key + + table_user.registration_key.default = key + + result = table_user.validate_and_insert(**kwargs) + if result.errors: + return {'errors': result.errors.as_dict(), 'message': None, 'user': None} + + user = table_user[result.id] + + message = self.messages.registration_successful + + if settings.create_user_groups: + d = user.as_dict() + description = self.messages.group_description % d + group_id = self.add_group(settings.create_user_groups % d, description) + self.add_membership(group_id, result.id) + + if self.settings.everybody_group_id: + self.add_membership(self.settings.everybody_group_id, result) + + if settings.registration_requires_verification: + d = {k: user[k] for k in table_user.fields if table_user[k].readable} + d['key']= key + if settings.login_after_registration and not settings.registration_requires_approval: + self.login_user(user) + return {'errors': None, 'message': None, 'user': d} + + if settings.registration_requires_approval: + user.update_record(registration_key='pending') + message = self.messages.registration_pending + elif settings.login_after_registration: + user.update_record(registration_key='') + self.login_user(user) + message = self.messages.logged_in + + self.log_event(log, user) + + return {'errors': None, 'message': message, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + def profile(self, log=DEFAULT, **kwargs): + """ + Lets the user change his/her profile + """ + + table_user = self.table_user() + settings = self.settings + table_user[settings.password_field].writable = False + + if not self.is_logged_in(): + raise AssertionError('User is not logged in') + + if not kwargs: + user = table_user[self.user.id] + return {'errors': None, 'message': None, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + result = self.db(table_user.id == self.user.id).validate_and_update(**kwargs) + user = table_user[self.user.id] + + if result.errors: + return {'errors': result.errors, 'message': None, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + if log is DEFAULT: + log = self.messages['profile_log'] + + self.log_event(log, user) + self.user.update(**kwargs) + return {'errors': None, 'message': self.messages.profile_updated, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} + + def change_password(self, log=DEFAULT, **kwargs): + """ + Lets the user change password + + Keyword Args: + old_password (string) - User's current password + new_password (string) - User's new password + new_password2 (string) - Verify the new password + """ + settings = self.settings + messages = self.messages + + if not self.is_logged_in(): + raise AssertionError('User is not logged in') + + db = self.db + table_user = self.table_user() + s = db(table_user.id == self.user.id) + + request = current.request + session = current.session + passfield = settings.password_field + + requires = table_user[passfield].requires + if not isinstance(requires, (list, tuple)): + requires = [requires] + requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) + if requires: + requires[0].min_length = 0 + + old_password = kwargs.get('old_password', '') + new_password = kwargs.get('new_password', '') + new_password2 = kwargs.get('new_password2', '') + + validator_old = requires + validator_pass2 = IS_EQUAL_TO(new_password, error_message=messages.mismatched_password) + + old_password, error_old = self.__validate(old_password, validator_old) + new_password2, error_new2 = self.__validate(new_password2, validator_pass2) + + errors = {} + if error_old: + errors['old_password'] = error_old + if error_new2: + errors['new_password2'] = error_new2 + if errors: + return {'errors': errors, 'message': None} + + current_user = s.select(limitby=(0, 1), orderby_on_limitby=False).first() + if not old_password == current_user[passfield]: + return {'errors': {'old_password': messages.invalid_password}, 'message': None} + else: + d = {passfield: new_password} + resp = s.validate_and_update(**d) + if resp.errors: + return {'errors': {'new_password': resp.errors[passfield]}, 'message': None} + if log is DEFAULT: + log = messages['change_password_log'] + self.log_event(log, self.user) + return {'errors': None, 'message': messages.password_changed} + + def verify_key(self, + key=None, + ignore_approval=False, + log=DEFAULT, + ): + """ + Verify a given registration_key actually exists in the user table. + Resets the key to empty string '' or 'pending' if + setttings.registration_requires_approval is true. + + Keyword Args: + key (string) - User's registration key + """ + table_user = self.table_user() + user = table_user(registration_key=key) + if (user is None) or (key is None): + return {'errors': {'key': self.messages.invalid_key}, 'message': self.messages.invalid_key } + + if self.settings.registration_requires_approval: + user.update_record(registration_key='pending') + result = {'errors': None, 'message': self.messages.registration_pending} + else: + user.update_record(registration_key='') + result = {'errors': None, 'message': self.messages.key_verified} + # make sure session has same user.registration_key as db record + if current.session.auth and current.session.auth.user: + current.session.auth.user.registration_key = user.registration_key + if log is DEFAULT: + log = self.messages['verify_log'] + self.log_event(log, user) + return result diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 2f111fe3..a0e3fa56 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -13,6 +13,7 @@ from .test_contribs import * from .test_routes import * from .test_router import * from .test_validators import * +from .test_authapi import * from .test_tools import * from .test_utils import * from .test_serializers import * diff --git a/gluon/tests/test_authapi.py b/gluon/tests/test_authapi.py new file mode 100644 index 00000000..cd00afb6 --- /dev/null +++ b/gluon/tests/test_authapi.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" Unit tests for authapi """ +import os +import unittest +from gluon.globals import Request, Response, Session +from gluon.languages import translator +from gluon.dal import DAL, Field +from gluon.authapi import AuthAPI +from gluon.storage import Storage +from gluon._compat import to_bytes, to_native, add_charset + +DEFAULT_URI = os.getenv('DB', 'sqlite:memory') + + +class TestAuthAPI(unittest.TestCase): + + def setUp(self): + self.request = Request(env={}) + self.request.application = 'a' + self.request.controller = 'c' + self.request.function = 'f' + self.request.folder = 'applications/admin' + self.response = Response() + self.session = Session() + T = translator('', 'en') + self.session.connect(self.request, self.response) + from gluon.globals import current + self.current = current + self.current.request = self.request + self.current.response = self.response + self.current.session = self.session + self.current.T = T + self.db = DAL(DEFAULT_URI, check_reserved=['all']) + self.auth = AuthAPI(self.db) + self.auth.define_tables(username=True, signature=False) + # Create a user + self.auth.table_user().validate_and_insert(first_name='Bart', + last_name='Simpson', + username='bart', + email='bart@simpson.com', + password='bart_password', + registration_key='', + registration_id='' + ) + self.db.commit() + + def test_login(self): + result = self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + self.assertTrue(result['user']['email'] == 'bart@simpson.com') + self.auth.logout() + self.assertFalse(self.auth.is_logged_in()) + self.auth.settings.username_case_sensitive = False + result = self.auth.login(**{'username': 'BarT', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + + def test_logout(self): + self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + result = self.auth.logout() + self.assertTrue(not self.auth.is_logged_in()) + self.assertTrue(result['user'] is None) + + def test_register(self): + self.auth.settings.login_after_registration = True + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + self.assertTrue(result['user']['email'] == 'lisa@simpson.com') + self.assertTrue(self.auth.is_logged_in()) + with self.assertRaises(AssertionError): # Can't register if you're logged in + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + self.auth.logout() + self.auth.settings.login_after_registration = False + result = self.auth.register(**{ + 'username': 'barney', + 'first_name': 'Barney', + 'last_name': 'Gumble', + 'email': 'barney@simpson.com', + 'password': 'barney_password' + }) + self.assertTrue(result['user']['email'] == 'barney@simpson.com') + self.assertFalse(self.auth.is_logged_in()) + self.auth.settings.login_userfield = 'email' + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + self.assertTrue(result['errors']['email'] == self.auth.messages.email_taken) + self.assertTrue(result['user'] is None) + self.auth.settings.registration_requires_verification = True + result = self.auth.register(**{ + 'username': 'homer', + 'first_name': 'Homer', + 'last_name': 'Simpson', + 'email': 'homer@simpson.com', + 'password': 'homer_password' + }) + self.assertTrue('key' in result['user']) + + def test_profile(self): + with self.assertRaises(AssertionError): + # We are not logged in + self.auth.profile() + self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + result = self.auth.profile(email='bartolo@simpson.com') + self.assertTrue(result['user']['email'] == 'bartolo@simpson.com') + self.assertTrue(self.auth.table_user()[result['user']['id']].email == 'bartolo@simpson.com') + + def test_change_password(self): + with self.assertRaises(AssertionError): + # We are not logged in + self.auth.change_password() + self.auth.login(**{'username': 'bart', 'password': 'bart_password'}) + self.assertTrue(self.auth.is_logged_in()) + self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234') + self.auth.logout() + self.assertTrue(not self.auth.is_logged_in()) + self.auth.login(username='bart', password='1234') + self.assertTrue(self.auth.is_logged_in()) + result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='5678') + self.assertTrue('new_password2' in result['errors']) + result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234') + self.assertTrue('old_password' in result['errors']) + + def test_verify_key(self): + self.auth.settings.registration_requires_verification = True + result = self.auth.register(**{ + 'username': 'homer', + 'first_name': 'Homer', + 'last_name': 'Simpson', + 'email': 'homer@simpson.com', + 'password': 'homer_password' + }) + self.assertTrue('key' in result['user']) + homer_id = result['user']['id'] + homers_key = result['user']['key'] + result = self.auth.verify_key(key=None) + self.assertTrue(result['errors'] is not None) + result = self.auth.verify_key(key='12345') + self.assertTrue(result['errors'] is not None) + result = self.auth.verify_key(key=homers_key) + self.assertTrue(result['errors'] is None) + self.assertEqual(self.auth.table_user()[homer_id].registration_key, '') + self.auth.settings.registration_requires_approval = True + result = self.auth.register(**{ + 'username': 'lisa', + 'first_name': 'Lisa', + 'last_name': 'Simpson', + 'email': 'lisa@simpson.com', + 'password': 'lisa_password' + }) + lisa_id = result['user']['id'] + result = self.auth.verify_key(key=result['user']['key']) + self.assertEqual(self.auth.table_user()[lisa_id].registration_key, 'pending') diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index c98b4c26..a7a35e90 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -702,15 +702,19 @@ class TestValidators(unittest.TestCase): def test_IS_LOWER(self): rtn = IS_LOWER()('ABC') + self.assertEqual(rtn, ('abc', None)) + rtn = IS_LOWER()(b'ABC') self.assertEqual(rtn, (b'abc', None)) rtn = IS_LOWER()('Ñ') - self.assertEqual(rtn, (b'\xc3\xb1', None)) + self.assertEqual(rtn, ('ñ', None)) def test_IS_UPPER(self): rtn = IS_UPPER()('abc') + self.assertEqual(rtn, ('ABC', None)) + rtn = IS_UPPER()(b'abc') self.assertEqual(rtn, (b'ABC', None)) rtn = IS_UPPER()('ñ') - self.assertEqual(rtn, (b'\xc3\x91', None)) + self.assertEqual(rtn, ('Ñ', None)) def test_IS_SLUG(self): rtn = IS_SLUG()('abc123') diff --git a/gluon/tools.py b/gluon/tools.py index eb4a61f9..471f0166 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -36,6 +36,7 @@ import json from email import message_from_string +from gluon.authapi import AuthAPI from gluon.contenttype import contenttype from gluon.storage import Storage, StorageList, Settings, Messages from gluon.utils import web2py_uuid, compare @@ -55,7 +56,7 @@ Field = DAL.Field __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] -### mind there are two loggers here (logger and crud.settings.logger)! +# mind there are two loggers here (logger and crud.settings.logger)! logger = logging.getLogger("web2py") DEFAULT = lambda: None @@ -245,7 +246,7 @@ class Mail(object): if filename is None: raise Exception('Missing attachment name') payload = payload.read() - #FIXME PY3 can be used to_native? + # FIXME PY3 can be used to_native? filename = filename.encode(encoding) if content_type is None: content_type = contenttype(filename) @@ -633,13 +634,13 @@ class Mail(object): x509_sign_chainfile = x509_sign_chainfile or self.settings.x509_sign_chainfile x509_sign_certfile = x509_sign_certfile or self.settings.x509_sign_certfile or \ - x509_sign_keyfile or self.settings.x509_sign_certfile + x509_sign_keyfile or self.settings.x509_sign_certfile # crypt certfiles could be a string or a list x509_crypt_certfiles = x509_crypt_certfiles or self.settings.x509_crypt_certfiles x509_nocerts = x509_nocerts or\ - self.settings.x509_nocerts + self.settings.x509_nocerts # need m2crypto try: @@ -697,8 +698,8 @@ class Mail(object): # make an encryption cert's stack for crypt_certfile in x509_crypt_certfiles: certfile = X509.load_cert(crypt_certfile)\ - if os.path.isfile(crypt_certfile)\ - else X509.load_cert_string(crypt_certfile) + if os.path.isfile(crypt_certfile)\ + else X509.load_cert_string(crypt_certfile) sk.push(certfile) s.set_x509_stack(sk) @@ -768,10 +769,10 @@ class Mail(object): xcc['reply_to'] = reply_to from google.appengine.api import mail attachments = attachments and [mail.Attachment( - a.my_filename, - a.my_payload, - content_id='' % k - ) for k, a in enumerate(attachments) if not raw] + a.my_filename, + a.my_payload, + content_id='' % k + ) for k, a in enumerate(attachments) if not raw] if attachments: result = mail.send_mail( sender=sender, to=origTo, @@ -841,7 +842,7 @@ class Recaptcha(DIV): options='', comment='', ajax=False - ): + ): request = request or current.request self.request_vars = request and request.vars or current.request.vars self.remote_addr = request.env.remote_addr @@ -1217,7 +1218,7 @@ class AuthJWT(object): salt=None, additional_payload=None, before_authorization=None, - max_header_length=4*1024, + max_header_length=4 * 1024, ): self.secret_key = secret_key self.auth = auth @@ -1318,9 +1319,9 @@ class AuthJWT(object): while sticking to a somewhat-stable Auth API. """ # TODO: Check the following comment - ## is the following safe or should we use - ## calendar.timegm(datetime.datetime.utcnow().timetuple()) - ## result seem to be the same (seconds since epoch, in UTC) + # is the following safe or should we use + # calendar.timegm(datetime.datetime.utcnow().timetuple()) + # result seem to be the same (seconds since epoch, in UTC) now = time.mktime(datetime.datetime.now().timetuple()) expires = now + self.expiration payload = dict( @@ -1484,172 +1485,119 @@ class AuthJWT(object): return decorator -class Auth(object): - default_settings = dict( - hideerror=False, - password_min_length=4, - cas_maps=None, - reset_password_requires_verification=False, - registration_requires_verification=False, - registration_requires_approval=False, - bulk_register_enabled=False, - login_after_registration=False, - login_after_password_change=True, - alternate_requires_registration=False, - create_user_groups="user_%(id)s", - everybody_group_id=None, - manager_actions={}, - auth_manager_role=None, - two_factor_authentication_group=None, - auth_two_factor_enabled=False, - auth_two_factor_tries_left=3, - login_captcha=None, - register_captcha=None, - pre_registration_div=None, - retrieve_username_captcha=None, - retrieve_password_captcha=None, - captcha=None, - prevent_open_redirect_attacks=True, - prevent_password_reset_attacks=True, - expiration=3600, # one hour - long_expiration=3600 * 30 * 24, # one month - remember_me_form=True, - allow_basic_login=False, - allow_basic_login_only=False, - on_failed_authentication=lambda x: redirect(x), - formstyle=None, - label_separator=None, - logging_enabled=True, - allow_delete_accounts=False, - password_field='password', - table_user_name='auth_user', - table_group_name='auth_group', - table_membership_name='auth_membership', - table_permission_name='auth_permission', - table_event_name='auth_event', - table_cas_name='auth_cas', - table_token_name='auth_token', - table_user=None, - table_group=None, - table_membership=None, - table_permission=None, - table_event=None, - table_cas=None, - showid=False, - use_username=False, - login_email_validate=True, - login_userfield=None, - multi_login=False, - logout_onlogout=None, - register_fields=None, - register_verify_password=True, - profile_fields=None, - email_case_sensitive=True, - username_case_sensitive=True, - update_fields=['email'], - ondelete="CASCADE", - client_side=True, - renew_session_onlogin=True, - renew_session_onlogout=True, - keep_session_onlogin=True, - keep_session_onlogout=False, - wiki=Settings(), - ) - # ## these are messages that can be customized - default_messages = dict( - login_button='Log In', - register_button='Sign Up', - password_reset_button='Request reset password', - password_change_button='Change password', - profile_save_button='Apply changes', - submit_button='Submit', - verify_password='Verify Password', - delete_label='Check to delete', - function_disabled='Function disabled', - access_denied='Insufficient privileges', - registration_verifying='Registration needs verification', - registration_pending='Registration is pending approval', - email_taken='This email already has an account', - invalid_username='Invalid username', - username_taken='Username already taken', - login_disabled='Login disabled by administrator', - logged_in='Logged in', - email_sent='Email sent', - unable_to_send_email='Unable to send email', - email_verified='Email verified', - logged_out='Logged out', - registration_successful='Registration successful', - invalid_email='Invalid email', - invalid_login='Invalid login', - invalid_user='Invalid user', - invalid_password='Invalid password', - invalid_two_factor_code = 'Incorrect code. {0} more attempt(s) remaining.', - is_empty="Cannot be empty", - mismatched_password="Password fields don't match", - verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email', - verify_email_subject='Email verification', - username_sent='Your username was emailed to you', - new_password_sent='A new password was emailed to you', - password_changed='Password changed', - retrieve_username='Your username is: %(username)s', - retrieve_username_subject='Username retrieve', - retrieve_password='Your password is: %(password)s', - retrieve_password_subject='Password retrieve', - reset_password='Click on the link %(link)s to reset your password', - reset_password_subject='Password reset', - bulk_invite_subject='Invitation to join %(site)s', - retrieve_two_factor_code='Your temporary login code is {0}', - retrieve_two_factor_code_subject='Two-step Login Authentication Code', - bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process', - invalid_reset_password='Invalid reset password', - profile_updated='Profile updated', - new_password='New password', - old_password='Old password', - group_description='Group uniquely assigned to user %(id)s', - register_log='User %(id)s Registered', - login_log='User %(id)s Logged-in', - login_failed_log=None, - logout_log='User %(id)s Logged-out', - profile_log='User %(id)s Profile updated', - verify_email_log='User %(id)s Verification email sent', - retrieve_username_log='User %(id)s Username retrieved', - retrieve_password_log='User %(id)s Password retrieved', - reset_password_log='User %(id)s Password reset', - change_password_log='User %(id)s Password changed', - add_group_log='Group %(group_id)s created', - del_group_log='Group %(group_id)s deleted', - add_membership_log=None, - del_membership_log=None, - has_membership_log=None, - add_permission_log=None, - del_permission_log=None, - has_permission_log=None, - impersonate_log='User %(id)s is impersonating %(other_id)s', - label_first_name='First name', - label_last_name='Last name', - label_username='Username', - label_email='E-mail', - label_password='Password', - label_registration_key='Registration key', - label_reset_password_key='Reset Password key', - label_registration_id='Registration identifier', - label_role='Role', - label_description='Description', - label_user_id='User ID', - label_group_id='Group ID', - label_name='Name', - label_table_name='Object or table name', - label_record_id='Record ID', - label_time_stamp='Timestamp', - label_client_ip='Client IP', - label_origin='Origin', - label_remember_me="Remember me (for 30 days)", - label_two_factor='Authentication code', - two_factor_comment = 'This code was emailed to you and is required for login.', - verify_password_comment='please input your password again', - ) +class Auth(AuthAPI): + default_settings = dict(AuthAPI.default_settings, + allow_basic_login=False, + allow_basic_login_only=False, + allow_delete_accounts=False, + alternate_requires_registration=False, + auth_manager_role=None, + auth_two_factor_enabled=False, + auth_two_factor_tries_left=3, + bulk_register_enabled=False, + captcha=None, + cas_maps=None, + client_side=True, + formstyle=None, + hideerror=False, + label_separator=None, + login_after_password_change=True, + login_after_registration=False, + login_captcha=None, + long_expiration=3600 * 30 * 24, # one month + mailer=None, + manager_actions={}, + multi_login=False, + on_failed_authentication=lambda x: redirect(x), + pre_registration_div=None, + prevent_open_redirect_attacks=True, + prevent_password_reset_attacks=True, + profile_fields=None, + register_captcha=None, + register_fields=None, + register_verify_password=True, + remember_me_form=True, + reset_password_requires_verification=False, + retrieve_password_captcha=None, + retrieve_username_captcha=None, + showid=False, + table_cas=None, + table_cas_name='auth_cas', + table_event=None, + table_group=None, + table_membership=None, + table_permission=None, + table_token_name='auth_token', + table_user=None, + two_factor_authentication_group=None, + update_fields=['email'], + wiki=Settings() + ) + # ## these are messages that can be customized + default_messages = dict(AuthAPI.default_messages, + access_denied='Insufficient privileges', + bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process', + bulk_invite_subject='Invitation to join %(site)s', + delete_label='Check to delete', + email_sent='Email sent', + email_verified='Email verified', + function_disabled='Function disabled', + impersonate_log='User %(id)s is impersonating %(other_id)s', + invalid_reset_password='Invalid reset password', + invalid_two_factor_code='Incorrect code. {0} more attempt(s) remaining.', + is_empty="Cannot be empty", + label_client_ip='Client IP', + label_description='Description', + label_email='E-mail', + label_first_name='First name', + label_group_id='Group ID', + label_last_name='Last name', + label_name='Name', + label_origin='Origin', + label_password='Password', + label_record_id='Record ID', + label_registration_id='Registration identifier', + label_registration_key='Registration key', + label_remember_me="Remember me (for 30 days)", + label_reset_password_key='Reset Password key', + label_role='Role', + label_table_name='Object or table name', + label_time_stamp='Timestamp', + label_two_factor='Authentication code', + label_user_id='User ID', + label_username='Username', + login_button='Log In', + login_disabled='Login disabled by administrator', + new_password='New password', + new_password_sent='A new password was emailed to you', + old_password='Old password', + password_change_button='Change password', + password_reset_button='Request reset password', + profile_save_button='Apply changes', + register_button='Sign Up', + reset_password='Click on the link %(link)s to reset your password', + reset_password_log='User %(id)s Password reset', + reset_password_subject='Password reset', + retrieve_password='Your password is: %(password)s', + retrieve_password_log='User %(id)s Password retrieved', + retrieve_password_subject='Password retrieve', + retrieve_two_factor_code='Your temporary login code is {0}', + retrieve_two_factor_code_subject='Two-step Login Authentication Code', + retrieve_username='Your username is: %(username)s', + retrieve_username_log='User %(id)s Username retrieved', + retrieve_username_subject='Username retrieve', + submit_button='Submit', + two_factor_comment='This code was emailed to you and is required for login.', + unable_send_email='Unable to send email', + username_sent='Your username was emailed to you', + verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email', + verify_email_log='User %(id)s Verification email sent', + verify_email_subject='Email verification', + verify_password='Verify Password', + verify_password_comment='please input your password again' + ) """ Class for authentication, authorization, role based access control. @@ -1816,7 +1764,7 @@ class Auth(object): self.user = auth.user # this is a trick to speed up sessions to avoid many writes if (now - auth.last_visit).seconds > (auth.expiration / 10): - auth.last_visit = request.now + auth.last_visit = now else: self.user = None if session.auth: @@ -1893,15 +1841,14 @@ class Auth(object): # ## these are messages that can be customized messages = self.messages = Messages(current.T) messages.update(Auth.default_messages) - messages.update(ajax_failed_authentication= - DIV(H4('NOT AUTHORIZED'), - 'Please ', - A('login', - _href=self.settings.login_url + - ('?_next=' + urllib_quote(current.request.env.http_web2py_component_location)) - if current.request.env.http_web2py_component_location else ''), - ' to view this content.', - _class='not-authorized alert alert-block')) + messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'), + 'Please ', + A('login', + _href=self.settings.login_url + + ('?_next=' + urllib_quote(current.request.env.http_web2py_component_location)) + if current.request.env.http_web2py_component_location else ''), + ' to view this content.', + _class='not-authorized alert alert-block')) messages.lock_keys = True # for "remember me" option @@ -1932,31 +1879,10 @@ class Auth(object): parts = next.split('/') if not ':' in parts[0]: return next - elif len(parts)>2 and parts[0].endswith(':') and parts[1:3]==['', host]: + elif len(parts) > 2 and parts[0].endswith(':') and parts[1:3] == ['', host]: return next return None - def _get_user_id(self): - """accessor for auth.user_id""" - return self.user and self.user.id or None - - user_id = property(_get_user_id, doc="user.id or None") - - def table_user(self): - return self.db[self.settings.table_user_name] - - def table_group(self): - return self.db[self.settings.table_group_name] - - def table_membership(self): - return self.db[self.settings.table_membership_name] - - def table_permission(self): - return self.db[self.settings.table_permission_name] - - def table_event(self): - return self.db[self.settings.table_event_name] - def table_cas(self): return self.db[self.settings.table_cas_name] @@ -2062,7 +1988,7 @@ class Auth(object): if callable(user_identifier): user_identifier = user_identifier(self.user) elif ((isinstance(user_identifier, str) or - type(user_identifier).__name__ == 'lazyT') and + type(user_identifier).__name__ == 'lazyT') and re.search(r'%\(.+\)s', user_identifier)): user_identifier = user_identifier % self.user if not user_identifier: @@ -2090,13 +2016,13 @@ class Auth(object): self.bar[0][3].append((item['name'], False, item['href'])) def bootstrap3(): # Default web2py scaffolding - def rename(icon): return icon+' '+icon.replace('icon', 'glyphicon') - self.bar = UL(LI(Anr(I(_class=rename('icon '+items[0]['icon'])), + def rename(icon): return icon + ' ' + icon.replace('icon', 'glyphicon') + self.bar = UL(LI(Anr(I(_class=rename('icon ' + items[0]['icon'])), ' ' + items[0]['name'], _href=items[0]['href'])), _class='dropdown-menu') del items[0] for item in items: - self.bar.insert(-1, LI(Anr(I(_class=rename('icon '+item['icon'])), + self.bar.insert(-1, LI(Anr(I(_class=rename('icon ' + item['icon'])), ' ' + item['name'], _href=item['href']))) self.bar.insert(-1, LI('', _class='divider')) @@ -2213,15 +2139,6 @@ class Auth(object): return self.bar - def __get_migrate(self, tablename, migrate=True): - - if type(migrate).__name__ == 'str': - return (migrate + tablename + '.table') - elif migrate == False: - return False - else: - return True - def enable_record_versioning(self, tables, archive_db=None, @@ -2260,56 +2177,13 @@ class Auth(object): for table in tables: fieldnames = table.fields() if ('id' in fieldnames and - 'modified_on' in fieldnames and - not current_record in fieldnames): + 'modified_on' in fieldnames and + not current_record in fieldnames): table._enable_record_versioning(archive_db=archive_db, archive_name=archive_names, current_record=current_record, current_record_label=current_record_label) - def define_signature(self): - db = self.db - settings = self.settings - request = current.request - T = current.T - reference_user = 'reference %s' % settings.table_user_name - - def lazy_user(auth=self): - return auth.user_id - - def represent(id, record=None, s=settings): - try: - user = s.table_user(id) - return '%s %s' % (user.get("first_name", user.get("email")), - user.get("last_name", '')) - except: - return id - ondelete = self.settings.ondelete - self.signature = Table( - self.db, 'auth_signature', - Field('is_active', 'boolean', - default=True, - readable=False, writable=False, - label=T('Is Active')), - Field('created_on', 'datetime', - default=request.now, - writable=False, readable=False, - label=T('Created On')), - Field('created_by', - reference_user, - default=lazy_user, represent=represent, - writable=False, readable=False, - label=T('Created By'), ondelete=ondelete), - Field('modified_on', 'datetime', - update=request.now, default=request.now, - writable=False, readable=False, - label=T('Modified On')), - Field('modified_by', - reference_user, represent=represent, - default=lazy_user, update=lazy_user, - writable=False, readable=False, - label=T('Modified By'), ondelete=ondelete)) - def define_tables(self, username=None, signature=None, enable_tokens=False, migrate=None, fake_migrate=None): """ @@ -2333,179 +2207,11 @@ class Auth(object): if fake_migrate is None: fake_migrate = db._fake_migrate settings = self.settings - if username is None: - username = settings.use_username - else: - settings.use_username = username settings.enable_tokens = enable_tokens - if not self.signature: - self.define_signature() - if signature: - signature_list = [self.signature] - elif not signature: - signature_list = [] - elif isinstance(signature, Table): - signature_list = [signature] - else: - signature_list = signature - is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) - is_crypted = CRYPT(key=settings.hmac_key, - min_length=settings.password_min_length) - is_unique_email = [ - IS_EMAIL(error_message=self.messages.invalid_email), - IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name, - error_message=self.messages.email_taken)] - if not settings.email_case_sensitive: - is_unique_email.insert(1, IS_LOWER()) - if settings.table_user_name not in db.tables: - passfield = settings.password_field - extra_fields = settings.extra_fields.get( - settings.table_user_name, []) + signature_list - if username or settings.cas_provider: - is_unique_username = \ - [IS_MATCH('[\w\.\-]+', strict=True, - error_message=self.messages.invalid_username), - IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name, - error_message=self.messages.username_taken)] - if not settings.username_case_sensitive: - is_unique_username.insert(1, IS_LOWER()) - db.define_table( - settings.table_user_name, - Field('first_name', length=128, default='', - label=self.messages.label_first_name, - requires=is_not_empty), - Field('last_name', length=128, default='', - label=self.messages.label_last_name, - requires=is_not_empty), - Field('email', length=512, default='', - label=self.messages.label_email, - requires=is_unique_email), - Field('username', length=128, default='', - label=self.messages.label_username, - requires=is_unique_username), - Field(passfield, 'password', length=512, - readable=False, label=self.messages.label_password, - requires=[is_crypted]), - Field('registration_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_key), - Field('reset_password_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_reset_password_key), - Field('registration_id', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_id), - *extra_fields, - **dict( - migrate=self.__get_migrate(settings.table_user_name, - migrate), - fake_migrate=fake_migrate, - format='%(username)s')) - else: - db.define_table( - settings.table_user_name, - Field('first_name', length=128, default='', - label=self.messages.label_first_name, - requires=is_not_empty), - Field('last_name', length=128, default='', - label=self.messages.label_last_name, - requires=is_not_empty), - Field('email', length=512, default='', - label=self.messages.label_email, - requires=is_unique_email), - Field(passfield, 'password', length=512, - readable=False, label=self.messages.label_password, - requires=[is_crypted]), - Field('registration_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_key), - Field('reset_password_key', length=512, - writable=False, readable=False, default='', - label=self.messages.label_reset_password_key), - Field('registration_id', length=512, - writable=False, readable=False, default='', - label=self.messages.label_registration_id), - *extra_fields, - **dict( - migrate=self.__get_migrate(settings.table_user_name, - migrate), - fake_migrate=fake_migrate, - format='%(first_name)s %(last_name)s (%(id)s)')) - reference_table_user = 'reference %s' % settings.table_user_name - if settings.table_group_name not in db.tables: - extra_fields = settings.extra_fields.get( - settings.table_group_name, []) + signature_list - db.define_table( - settings.table_group_name, - Field('role', length=512, default='', - label=self.messages.label_role, - requires=IS_NOT_IN_DB(db, '%s.role' % settings.table_group_name)), - Field('description', 'text', - label=self.messages.label_description), - *extra_fields, - **dict( - migrate=self.__get_migrate( - settings.table_group_name, migrate), - fake_migrate=fake_migrate, - format='%(role)s (%(id)s)')) - reference_table_group = 'reference %s' % settings.table_group_name - if settings.table_membership_name not in db.tables: - extra_fields = settings.extra_fields.get( - settings.table_membership_name, []) + signature_list - db.define_table( - settings.table_membership_name, - Field('user_id', reference_table_user, - label=self.messages.label_user_id), - Field('group_id', reference_table_group, - label=self.messages.label_group_id), - *extra_fields, - **dict( - migrate=self.__get_migrate( - settings.table_membership_name, migrate), - fake_migrate=fake_migrate)) - if settings.table_permission_name not in db.tables: - extra_fields = settings.extra_fields.get( - settings.table_permission_name, []) + signature_list - db.define_table( - settings.table_permission_name, - Field('group_id', reference_table_group, - label=self.messages.label_group_id), - Field('name', default='default', length=512, - label=self.messages.label_name, - requires=is_not_empty), - Field('table_name', length=512, - label=self.messages.label_table_name), - Field('record_id', 'integer', default=0, - label=self.messages.label_record_id, - requires=IS_INT_IN_RANGE(0, 10 ** 9)), - *extra_fields, - **dict( - migrate=self.__get_migrate( - settings.table_permission_name, migrate), - fake_migrate=fake_migrate)) - if settings.table_event_name not in db.tables: - db.define_table( - settings.table_event_name, - Field('time_stamp', 'datetime', - default=current.request.now, - label=self.messages.label_time_stamp), - Field('client_ip', - default=current.request.client, - label=self.messages.label_client_ip), - Field('user_id', reference_table_user, default=None, - label=self.messages.label_user_id), - Field('origin', default='auth', length=512, - label=self.messages.label_origin, - requires=is_not_empty), - Field('description', 'text', default='', - label=self.messages.label_description, - requires=is_not_empty), - *settings.extra_fields.get(settings.table_event_name, []), - **dict( - migrate=self.__get_migrate( - settings.table_event_name, migrate), - fake_migrate=fake_migrate)) + super(Auth, self).define_tables(username, signature, migrate, fake_migrate) + now = current.request.now + reference_table_user = 'reference %s' % settings.table_user_name if settings.cas_domains: if settings.table_cas_name not in db.tables: db.define_table( @@ -2518,7 +2224,7 @@ class Auth(object): Field('renew', 'boolean', default=False), *settings.extra_fields.get(settings.table_cas_name, []), **dict( - migrate=self.__get_migrate( + migrate=self._get_migrate( settings.table_cas_name, migrate), fake_migrate=fake_migrate)) if settings.enable_tokens: @@ -2532,7 +2238,7 @@ class Auth(object): Field('expires_on', 'datetime', default=datetime.datetime(2999, 12, 31)), Field('token', writable=False, default=web2py_uuid, unique=True), *extra_fields, - **dict(migrate=self.__get_migrate(settings.table_token_name, migrate), + **dict(migrate=self._get_migrate(settings.table_token_name, migrate), fake_migrate=fake_migrate)) if not db._lazy_tables: settings.table_user = db[settings.table_user_name] @@ -2566,26 +2272,6 @@ class Auth(object): maps=maps) return self - def log_event(self, description, vars=None, origin='auth'): - """ - Examples: - Use as:: - - auth.log_event(description='this happened', origin='auth') - - """ - if not self.settings.logging_enabled or not description: - return - elif self.is_logged_in(): - user_id = self.user.id - else: - user_id = None # user unknown - vars = vars or {} - # log messages should not be translated - if type(description).__name__ == 'lazyT': - description = description.m - self.table_event().insert(description=str(description % vars), origin=origin, user_id=user_id) - def get_or_create_user(self, keys, update_fields=['email'], login=True, get=True): """ @@ -2683,29 +2369,6 @@ class Auth(object): raise http_401 return (True, True, is_valid_user) - def login_user(self, user): - """ - Logins the `user = db.auth_user(id)` - """ - from gluon.settings import global_settings - if global_settings.web2py_runtime_gae: - user = Row(self.table_user()._filter_fields(user, id=True)) - delattr(user, 'password') - else: - user = Row(user) - for key in list(user.keys()): - value = user[key] - if callable(value) or key == 'password': - delattr(user, key) - if self.settings.renew_session_onlogin: - current.session.renew(clear_session=not self.settings.keep_session_onlogin) - current.session.auth = Storage(user=user, - last_visit=current.request.now, - expiration=self.settings.expiration, - hmac_key=web2py_uuid()) - self.user = user - self.update_groups() - def _get_login_settings(self): table_user = self.table_user() userfield = self.settings.login_userfield or 'username' \ @@ -2726,7 +2389,7 @@ class Auth(object): settings.passfield].validate(password)[0] if ((user.registration_key is None or not user.registration_key.strip()) and - password == user[settings.passfield]): + password == user[settings.passfield]): self.login_user(user) return user else: @@ -3368,7 +3031,7 @@ class Auth(object): if self.settings.register_fields is None: self.settings.register_fields = [f.name for f in table_user if f.writable] k = self.settings.register_fields.index(passfield) - self.settings.register_fields.insert(k+1, "password_two") + self.settings.register_fields.insert(k + 1, "password_two") extra_fields = [ Field("password_two", "password", requires=IS_EQUAL_TO(request.post_vars.get(passfield, None), @@ -3447,16 +3110,6 @@ class Auth(object): return form - def is_logged_in(self): - """ - Checks if the user is logged in and returns True/False. - If so user is in auth.user as well as in session.auth.user - """ - - if self.user: - return True - return False - def verify_email(self, next=DEFAULT, onaccept=DEFAULT, @@ -3508,7 +3161,7 @@ class Auth(object): response = current.response session = current.session captcha = self.settings.retrieve_username_captcha or \ - (self.settings.retrieve_username_captcha is not False and self.settings.captcha) + (self.settings.retrieve_username_captcha is not False and self.settings.captcha) if not self.settings.mailer: response.flash = self.messages.function_disabled return '' @@ -3627,7 +3280,7 @@ class Auth(object): d = { passfield: str(table_user[passfield].validate(password)[0]), 'registration_key': '' - } + } user.update_record(**d) if self.settings.mailer and \ self.settings.mailer.send(to=form.vars.email, @@ -3684,7 +3337,6 @@ class Auth(object): raise Exception except Exception as e: session.flash = self.messages.invalid_reset_password - redirect(self.url('login', vars=dict(test=e))) redirect(next, client_side=self.settings.client_side) passfield = self.settings.password_field form = SQLFORM.factory( @@ -3768,9 +3420,9 @@ class Auth(object): else: emails_fail.append(email) emails_fail += emails[max_emails:] - form = DIV(H4('Emails sent'), UL(*[A(x, _href='mailto:'+x) for x in emails_sent]), - H4('Emails failed'), UL(*[A(x, _href='mailto:'+x) for x in emails_fail]), - H4('Emails existing'), UL(*[A(x, _href='mailto:'+x) for x in emails_exist])) + form = DIV(H4('Emails sent'), UL(*[A(x, _href='mailto:' + x) for x in emails_sent]), + H4('Emails failed'), UL(*[A(x, _href='mailto:' + x) for x in emails_fail]), + H4('Emails existing'), UL(*[A(x, _href='mailto:' + x) for x in emails_exist])) return form def manage_tokens(self): @@ -3874,7 +3526,7 @@ class Auth(object): response = current.response session = current.session captcha = self.settings.retrieve_password_captcha or \ - (self.settings.retrieve_password_captcha is not False and self.settings.captcha) + (self.settings.retrieve_password_captcha is not False and self.settings.captcha) if next is DEFAULT: next = self.get_vars_next() or self.settings.request_reset_password_next @@ -4077,7 +3729,7 @@ class Auth(object): formstyle=self.settings.formstyle, separator=self.settings.label_separator, deletable=self.settings.allow_delete_accounts, - ) + ) if form.accepts(request, session, formname='profile', onvalidation=onvalidation, @@ -4205,21 +3857,6 @@ class Auth(object): else: return SQLFORM(table_user, user.id, readonly=True) - def update_groups(self): - if not self.user: - return - user_groups = self.user_groups = {} - if current.session.auth: - current.session.auth.user_groups = self.user_groups - table_group = self.table_group() - table_membership = self.table_membership() - memberships = self.db( - table_membership.user_id == self.user.id).select() - for membership in memberships: - group = table_group(membership.group_id) - if group: - user_groups[membership.group_id] = group.role - def groups(self): """ Displays the groups and their roles for the logged in user @@ -4337,6 +3974,7 @@ class Auth(object): If role is provided instead of group_id then the group_id is calculated. """ + def has_membership(self=self, group_id=group_id, role=role): return self.has_membership(group_id=group_id, role=role) return self.requires(has_membership, otherwise=otherwise) @@ -4348,6 +3986,7 @@ class Auth(object): if user logged in is not a member of any group (role) that has 'name' access to 'table_name', 'record_id'. """ + def has_permission(self=self, name=name, table_name=table_name, record_id=record_id): return self.has_permission(name, table_name, record_id) return self.requires(has_permission, otherwise=otherwise) @@ -4363,231 +4002,6 @@ class Auth(object): return URL.verify(current.request, user_signature=True, hash_vars=hash_vars) return self.requires(verify, otherwise) - def add_group(self, role, description=''): - """ - Creates a group associated to a role - """ - group_id = self.table_group().insert(role=role, description=description) - self.log_event(self.messages['add_group_log'], dict(group_id=group_id, role=role)) - return group_id - - def del_group(self, group_id): - """ - Deletes a group - """ - self.db(self.table_group().id == group_id).delete() - self.db(self.table_membership().group_id == group_id).delete() - self.db(self.table_permission().group_id == group_id).delete() - if group_id in self.user_groups: - del self.user_groups[group_id] - self.log_event(self.messages.del_group_log, dict(group_id=group_id)) - - def id_group(self, role): - """ - Returns the group_id of the group specified by the role - """ - rows = self.db(self.table_group().role == role).select() - if not rows: - return None - return rows[0].id - - def user_group(self, user_id=None): - """ - Returns the group_id of the group uniquely associated to this user - i.e. `role=user:[user_id]` - """ - return self.id_group(self.user_group_role(user_id)) - - def user_group_role(self, user_id=None): - if not self.settings.create_user_groups: - return None - if user_id: - user = self.table_user()[user_id] - else: - user = self.user - return self.settings.create_user_groups % user - - def has_membership(self, group_id=None, user_id=None, role=None): - """ - Checks if user is member of group_id or role - """ - group_id = group_id or self.id_group(role) - try: - group_id = int(group_id) - except: - group_id = self.id_group(group_id) # interpret group_id as a role - if not user_id and self.user: - user_id = self.user.id - membership = self.table_membership() - if group_id and user_id and self.db((membership.user_id == user_id) & - (membership.group_id == group_id)).select(): - r = True - else: - r = False - self.log_event(self.messages['has_membership_log'], - dict(user_id=user_id, group_id=group_id, check=r)) - return r - - def add_membership(self, group_id=None, user_id=None, role=None): - """ - Gives user_id membership of group_id or role - if user is None than user_id is that of current logged in user - """ - - group_id = group_id or self.id_group(role) - try: - group_id = int(group_id) - except: - group_id = self.id_group(group_id) # interpret group_id as a role - if not user_id and self.user: - user_id = self.user.id - if not group_id: - raise ValueError('group_id not provided or invalid') - if not user_id: - raise ValueError('user_id not provided or invalid') - membership = self.table_membership() - db = membership._db - record = db((membership.user_id == user_id) & - (membership.group_id == group_id), - ignore_common_filters=True).select().first() - if record: - if hasattr(record, 'is_active') and not record.is_active: - record.update_record(is_active=True) - return record.id - else: - id = membership.insert(group_id=group_id, user_id=user_id) - if role: - self.user_groups[group_id] = role - else: - self.update_groups() - self.log_event(self.messages['add_membership_log'], - dict(user_id=user_id, group_id=group_id)) - return id - - def del_membership(self, group_id=None, user_id=None, role=None): - """ - Revokes membership from group_id to user_id - if user_id is None than user_id is that of current logged in user - """ - - group_id = group_id or self.id_group(role) - try: - group_id = int(group_id) - except: - group_id = self.id_group(group_id) # interpret group_id as a role - if not user_id and self.user: - user_id = self.user.id - membership = self.table_membership() - self.log_event(self.messages['del_membership_log'], - dict(user_id=user_id, group_id=group_id)) - ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete() - if group_id in self.user_groups: - del self.user_groups[group_id] - return ret - - def has_permission(self, - name='any', - table_name='', - record_id=0, - user_id=None, - group_id=None, - ): - """ - Checks if user_id or current logged in user is member of a group - that has 'name' permission on 'table_name' and 'record_id' - if group_id is passed, it checks whether the group has the permission - """ - - if not group_id and self.settings.everybody_group_id and \ - self.has_permission(name, table_name, record_id, user_id=None, - group_id=self.settings.everybody_group_id): - return True - - if not user_id and not group_id and self.user: - user_id = self.user.id - if user_id: - membership = self.table_membership() - rows = self.db(membership.user_id == user_id).select(membership.group_id) - groups = set([row.group_id for row in rows]) - if group_id and group_id not in groups: - return False - else: - groups = set([group_id]) - permission = self.table_permission() - rows = self.db(permission.name == - name)(permission.table_name == - str(table_name))(permission.record_id == - record_id).select(permission.group_id) - groups_required = set([row.group_id for row in rows]) - if record_id: - rows = self.db(permission.name == - name)(permission.table_name == - str(table_name))(permission.record_id == - 0).select(permission.group_id) - groups_required = groups_required.union(set([row.group_id for row in rows])) - if groups.intersection(groups_required): - r = True - else: - r = False - if user_id: - self.log_event(self.messages['has_permission_log'], - dict(user_id=user_id, name=name, - table_name=table_name, record_id=record_id)) - return r - - def add_permission(self, - group_id, - name='any', - table_name='', - record_id=0, - ): - """ - Gives group_id 'name' access to 'table_name' and 'record_id' - """ - - permission = self.table_permission() - if group_id == 0: - group_id = self.user_group() - record = self.db((permission.group_id == group_id) & - (permission.name == name) & - (permission.table_name == str(table_name)) & - (permission.record_id == long(record_id)), - ignore_common_filters=True - ).select(limitby=(0, 1), orderby_on_limitby=False).first() - if record: - if hasattr(record, 'is_active') and not record.is_active: - record.update_record(is_active=True) - id = record.id - else: - id = permission.insert(group_id=group_id, name=name, - table_name=str(table_name), - record_id=long(record_id)) - self.log_event(self.messages['add_permission_log'], - dict(permission_id=id, group_id=group_id, - name=name, table_name=table_name, - record_id=record_id)) - return id - - def del_permission(self, - group_id, - name='any', - table_name='', - record_id=0, - ): - """ - Revokes group_id 'name' access to 'table_name' and 'record_id' - """ - - permission = self.table_permission() - self.log_event(self.messages['del_permission_log'], - dict(group_id=group_id, name=name, - table_name=table_name, record_id=record_id)) - return self.db(permission.group_id == - group_id)(permission.name == - name)(permission.table_name == - str(table_name))(permission.record_id == - long(record_id)).delete() - def accessible_query(self, name, table, user_id=None): """ Returns a query with all accessible records for user_id or @@ -4622,16 +4036,16 @@ class Auth(object): permission = self.table_permission() query = table.id.belongs( db(membership.user_id == user_id) - (membership.group_id == permission.group_id) - (permission.name == name) - (permission.table_name == table) - ._select(permission.record_id)) + (membership.group_id == permission.group_id) + (permission.name == name) + (permission.table_name == table) + ._select(permission.record_id)) if self.settings.everybody_group_id: query |= table.id.belongs( db(permission.group_id == self.settings.everybody_group_id) - (permission.name == name) - (permission.table_name == table) - ._select(permission.record_id)) + (permission.name == name) + (permission.table_name == table) + ._select(permission.record_id)) return query @staticmethod @@ -4769,12 +4183,12 @@ class Auth(object): def wikimenu(self): """To be used in menu.py for app wide wiki menus""" if (hasattr(self, "_wiki") and - self._wiki.settings.controller and - self._wiki.settings.function): + self._wiki.settings.controller and + self._wiki.settings.function): self._wiki.automenu() -class Crud(object): # pragma: no cover +class Crud(object): # pragma: no cover def url(self, f=None, args=None, vars=None): """ @@ -4947,7 +4361,7 @@ class Crud(object): # pragma: no cover formstyle=self.settings.formstyle, separator=self.settings.label_separator, **attributes # contains hidden - ) + ) self.accepted = False self.deleted = False captcha = self.settings.update_captcha or self.settings.captcha @@ -5044,7 +4458,7 @@ class Crud(object): # pragma: no cover showid=self.settings.showid, formstyle=self.settings.formstyle, separator=self.settings.label_separator - ) + ) if current.request.extension not in ('html', 'load'): return table._filter_fields(form.record, id=True) return form @@ -5702,6 +5116,7 @@ class Service(object): self.error() class JsonRpcException(Exception): + def __init__(self, code, info): jrpc_error = Service.jsonrpc_errors.get(code) if jrpc_error: @@ -6218,6 +5633,7 @@ class PluginManager(object): class Expose(object): + def __init__(self, base=None, basename=None, extensions=None, allow_download=True, follow_symlink_out=False): """ @@ -6245,7 +5661,8 @@ class Expose(object): """ # why would this not be callable? but otherwise tests do not pass - if current.session and callable(current.session.forget): current.session.forget() + if current.session and callable(current.session.forget): + current.session.forget() self.follow_symlink_out = follow_symlink_out self.base = self.normalize_path( base or os.path.join(current.request.folder, 'static')) @@ -6305,7 +5722,7 @@ class Expose(object): @staticmethod def __in_base(subdir, basedir, sep=os.path.sep): """True if subdir/ is under basedir/""" - s = lambda f: '%s%s' % (f.rstrip(sep), sep) # f -> f/ + s = lambda f: '%s%s' % (f.rstrip(sep), sep) # f -> f/ # The trailing '/' is for the case of '/foobar' in_base of '/foo': # - becase '/foobar' starts with '/foo' # - but '/foobar/' doesn't start with '/foo/' @@ -6470,46 +5887,46 @@ class Wiki(object): table_definitions = [ ('wiki_page', { - 'args': [ - Field('slug', - requires=[IS_SLUG(), - IS_NOT_IN_DB(db, 'wiki_page.slug')], - writable=False), - Field('title', length=255, unique=True), - Field('body', 'text', notnull=True), - Field('tags', 'list:string'), - Field('can_read', 'list:string', - writable=perms, - readable=perms, - default=[Wiki.everybody]), - Field('can_edit', 'list:string', - writable=perms, readable=perms, - default=[Wiki.everybody]), - Field('changelog'), - Field('html', 'text', - compute=self.get_renderer(), - readable=False, writable=False), - Field('render', default="markmin", - readable=show_engine, - writable=show_engine, - requires=IS_EMPTY_OR( - IS_IN_SET(engines))), - auth.signature], - 'vars': {'format': '%(title)s', 'migrate': migrate}}), + 'args': [ + Field('slug', + requires=[IS_SLUG(), + IS_NOT_IN_DB(db, 'wiki_page.slug')], + writable=False), + Field('title', length=255, unique=True), + Field('body', 'text', notnull=True), + Field('tags', 'list:string'), + Field('can_read', 'list:string', + writable=perms, + readable=perms, + default=[Wiki.everybody]), + Field('can_edit', 'list:string', + writable=perms, readable=perms, + default=[Wiki.everybody]), + Field('changelog'), + Field('html', 'text', + compute=self.get_renderer(), + readable=False, writable=False), + Field('render', default="markmin", + readable=show_engine, + writable=show_engine, + requires=IS_EMPTY_OR( + IS_IN_SET(engines))), + auth.signature], + 'vars': {'format': '%(title)s', 'migrate': migrate}}), ('wiki_tag', { - 'args': [ - Field('name'), - Field('wiki_page', 'reference wiki_page'), - auth.signature], - 'vars':{'format': '%(title)s', 'migrate': migrate}}), + 'args': [ + Field('name'), + Field('wiki_page', 'reference wiki_page'), + auth.signature], + 'vars':{'format': '%(title)s', 'migrate': migrate}}), ('wiki_media', { - 'args': [ - Field('wiki_page', 'reference wiki_page'), - Field('title', required=True), - Field('filename', 'upload', required=True), - auth.signature], - 'vars': {'format': '%(title)s', 'migrate': migrate}}), - ] + 'args': [ + Field('wiki_page', 'reference wiki_page'), + Field('title', required=True), + Field('filename', 'upload', required=True), + auth.signature], + 'vars': {'format': '%(title)s', 'migrate': migrate}}), + ] # define only non-existent tables for key, value in table_definitions: @@ -6567,8 +5984,8 @@ class Wiki(object): elif self.auth.user: groups = self.settings.groups if ('wiki_editor' in groups or - set(groups).intersection(set(page.can_read + page.can_edit)) or - page.created_by == self.auth.user.id): + set(groups).intersection(set(page.can_read + page.can_edit)) or + page.created_by == self.auth.user.id): return True return False @@ -6814,7 +6231,7 @@ class Wiki(object): options.insert(0, OPTION('', _value='')) fields = [Field("slug", default=current.request.args(1) or self.settings.force_prefix, - requires=(IS_SLUG(), IS_NOT_IN_DB(db, db.wiki_page.slug))),] + requires=(IS_SLUG(), IS_NOT_IN_DB(db, db.wiki_page.slug))), ] if self.settings.templates: fields.append( Field("from_template", "reference wiki_page", @@ -6980,14 +6397,14 @@ class Wiki(object): def link(t): return A(t, _href=URL(args='_search', vars=dict(q=t))) items = [DIV(H3(A(p.wiki_page.title, _href=URL( - args=p.wiki_page.slug))), - MARKMIN(self.first_paragraph(p.wiki_page)) - if preview else '', - DIV(_class='w2p_wiki_tags', - *[link(t.strip()) for t in - p.wiki_page.tags or [] if t.strip()]), - _class='w2p_wiki_search_item') - for p in pages] + args=p.wiki_page.slug))), + MARKMIN(self.first_paragraph(p.wiki_page)) + if preview else '', + DIV(_class='w2p_wiki_tags', + *[link(t.strip()) for t in + p.wiki_page.tags or [] if t.strip()]), + _class='w2p_wiki_search_item') + for p in pages] content.append(DIV(_class='w2p_wiki_pages', *items)) else: cloud = False @@ -7032,6 +6449,7 @@ class Wiki(object): class Config(object): + def __init__( self, filename, diff --git a/gluon/validators.py b/gluon/validators.py index 4181c226..779acab4 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2478,9 +2478,14 @@ class IS_LOWER(Validator): ('\\xc3\\xb1', None) """ - def __call__(self, value): - return (to_bytes(to_unicode(value).lower()), None) + cast_back = lambda x: x + if isinstance(value, str): + cast_back = to_native + elif isinstance(value, bytes): + cast_back = to_bytes + value = to_unicode(value).lower() + return (cast_back(value), None) class IS_UPPER(Validator): @@ -2495,7 +2500,13 @@ class IS_UPPER(Validator): """ def __call__(self, value): - return (to_bytes(to_unicode(value).upper()), None) + cast_back = lambda x: x + if isinstance(value, str): + cast_back = to_native + elif isinstance(value, bytes): + cast_back = to_bytes + value = to_unicode(value).upper() + return (cast_back(value), None) def urlify(s, maxlen=80, keep_underscores=False): From d1dfc4a06a30af1d64976e0be35b522d96980edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Nov 2016 16:51:54 +0000 Subject: [PATCH 002/149] use _compat's long --- gluon/authapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gluon/authapi.py b/gluon/authapi.py index 981a314c..dc3860ac 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -4,6 +4,7 @@ | Copyrighted by Massimo Di Pierro | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ +from gluo._compat import long from gluon import current from gluon.storage import Messages, Settings, Storage from gluon.utils import web2py_uuid From 85c68e6876a320bb6f5c8ad4e377aaecfbe0ae5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Nov 2016 16:52:42 +0000 Subject: [PATCH 003/149] typo --- gluon/authapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index dc3860ac..c4507931 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -4,7 +4,7 @@ | Copyrighted by Massimo Di Pierro | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ -from gluo._compat import long +from gluon._compat import long from gluon import current from gluon.storage import Messages, Settings, Storage from gluon.utils import web2py_uuid From bf5ec0d7cfb83a1a39dc254905c46db4eea7b319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 19:38:21 +0000 Subject: [PATCH 004/149] Fixed a long standing bug in login_user which was using 'password' instead of settings.password_field Fixes #636 --- gluon/authapi.py | 17 ++++++++++------- gluon/tools.py | 9 ++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index c4507931..6ff67ab3 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -688,19 +688,22 @@ class AuthAPI(object): return True return False - def login_user(self, user): - """ - Logins the `user = db.auth_user(id)` - """ + def _update_session_user(self, user): if global_settings.web2py_runtime_gae: user = Row(self.table_user()._filter_fields(user, id=True)) - delattr(user, 'password') + delattr(user, self.settings.password_field) else: user = Row(user) for key in list(user.keys()): value = user[key] - if callable(value) or key == 'password': + if callable(value) or key == self.settings.password_field: delattr(user, key) + + def login_user(self, user): + """ + Logins the `user = db.auth_user(id)` + """ + self._update_session_user(user) if self.settings.renew_session_onlogin: current.session.renew(clear_session=not self.settings.keep_session_onlogin) current.session.auth = Storage(user=user, @@ -924,7 +927,7 @@ class AuthAPI(object): log = self.messages['profile_log'] self.log_event(log, user) - self.user.update(**kwargs) + self._update_session_user(user) return {'errors': None, 'message': self.messages.profile_updated, 'user': {k: user[k] for k in table_user.fields if table_user[k].readable}} def change_password(self, log=DEFAULT, **kwargs): diff --git a/gluon/tools.py b/gluon/tools.py index 471f0166..f002bfd4 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3734,7 +3734,14 @@ class Auth(AuthAPI): formname='profile', onvalidation=onvalidation, hideerror=self.settings.hideerror): - self.user.update(table_user._filter_fields(form.vars)) + extra_fields = self.settings.extra_fields.get(self.settings.table_user_name, []) + if any(f.compute for f in extra_fields): + user = table_user[self.user.id] + self._update_session_user(user) + else: + self.user.update(table_user._filter_fields(form.vars)) + + session.flash = self.messages.profile_updated self.log_event(log, self.user) callback(onaccept, form) From 7b66ec0ae3b33aa28c1c0328dc0a62361b780124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 19:51:51 +0000 Subject: [PATCH 005/149] Fixes #1506 --- gluon/authapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index 6ff67ab3..c4934b83 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -135,10 +135,10 @@ class AuthAPI(object): messages = self.messages = Messages(current.T) messages.update(self.default_messages) messages.lock_keys = True - if signature: + if signature is True: self.define_signature() else: - self.signature = None + self.signature = signature or None def __validate(self, value, requires): if not isinstance(requires, (list, tuple)): @@ -251,7 +251,7 @@ class AuthAPI(object): settings.use_username = username if not self.signature: self.define_signature() - if signature: + if signature is True: signature_list = [self.signature] elif not signature: signature_list = [] From 757d46274ee43108d5c67bd9eca03624a0aa01da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 20:00:51 +0000 Subject: [PATCH 006/149] fix bug with new login_user --- gluon/authapi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index c4934b83..c9f425fc 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -698,12 +698,13 @@ class AuthAPI(object): value = user[key] if callable(value) or key == self.settings.password_field: delattr(user, key) + return user def login_user(self, user): """ Logins the `user = db.auth_user(id)` """ - self._update_session_user(user) + user = self._update_session_user(user) if self.settings.renew_session_onlogin: current.session.renew(clear_session=not self.settings.keep_session_onlogin) current.session.auth = Storage(user=user, From 920ab72415998ca9f2a6f406c64dcd0ad815f059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 20 Nov 2016 20:11:45 +0000 Subject: [PATCH 007/149] fixed _update_session_user not really updating the session --- gluon/authapi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index c9f425fc..1937972f 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -698,6 +698,10 @@ class AuthAPI(object): value = user[key] if callable(value) or key == self.settings.password_field: delattr(user, key) + current.session.auth = Storage(user=user, + last_visit=current.request.now, + expiration=self.settings.expiration, + hmac_key=web2py_uuid()) return user def login_user(self, user): @@ -707,10 +711,6 @@ class AuthAPI(object): user = self._update_session_user(user) if self.settings.renew_session_onlogin: current.session.renew(clear_session=not self.settings.keep_session_onlogin) - current.session.auth = Storage(user=user, - last_visit=current.request.now, - expiration=self.settings.expiration, - hmac_key=web2py_uuid()) self.user = user self.update_groups() From 79e256b1d73ea0b9fecc3274cf061fb4283a34a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 21 Nov 2016 15:53:10 +0000 Subject: [PATCH 008/149] require sudo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 472896fd..3bc79dcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -sudo: false +sudo: required cache: pip From a23b264a370ad44fcd8080770bad89a1f830f679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 21 Nov 2016 19:48:41 +0000 Subject: [PATCH 009/149] make sudo false again --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3bc79dcc..472896fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -sudo: required +sudo: false cache: pip From 140023e9203649e3f7945d47bae8da0dfa50987c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Fri, 31 Mar 2017 12:04:55 +0100 Subject: [PATCH 010/149] Make web2pyDialog Python 3 compatible checks "port web2pyDialog" in #1353 --- gluon/widget.py | 91 +++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/gluon/widget.py b/gluon/widget.py index 131bfc3f..0804fa96 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -13,7 +13,7 @@ from __future__ import print_function import datetime import sys -from gluon._compat import StringIO, thread, xrange +from gluon._compat import StringIO, thread, xrange, PY2 import time import threading import os @@ -134,24 +134,29 @@ class web2pyDialog(object): def __init__(self, root, options): """ web2pyDialog constructor """ - import Tkinter - import tkMessageBox + if PY2: + import Tkinter as tkinter + import tkMessageBox as messagebox + else: + import tkinter + from tkinter import messagebox + bg_color = 'white' root.withdraw() - self.root = Tkinter.Toplevel(root, bg=bg_color) + self.root = tkinter.Toplevel(root, bg=bg_color) self.root.resizable(0, 0) self.root.title(ProgramName) self.options = options self.scheduler_processes = {} - self.menu = Tkinter.Menu(self.root) - servermenu = Tkinter.Menu(self.menu, tearoff=0) + self.menu = tkinter.Menu(self.root) + servermenu = tkinter.Menu(self.menu, tearoff=0) httplog = os.path.join(self.options.folder, self.options.log_filename) iconphoto = os.path.join('extras', 'icons', 'web2py.gif') if os.path.exists(iconphoto): - img = Tkinter.PhotoImage(file=iconphoto) + img = tkinter.PhotoImage(file=iconphoto) self.root.tk.call('wm', 'iconphoto', self.root._w, img) # Building the Menu item = lambda: start_browser(httplog) @@ -163,16 +168,16 @@ class web2pyDialog(object): self.menu.add_cascade(label='Server', menu=servermenu) - self.pagesmenu = Tkinter.Menu(self.menu, tearoff=0) + self.pagesmenu = tkinter.Menu(self.menu, tearoff=0) self.menu.add_cascade(label='Pages', menu=self.pagesmenu) #scheduler menu - self.schedmenu = Tkinter.Menu(self.menu, tearoff=0) + self.schedmenu = tkinter.Menu(self.menu, tearoff=0) self.menu.add_cascade(label='Scheduler', menu=self.schedmenu) #start and register schedulers from options self.update_schedulers(start=True) - helpmenu = Tkinter.Menu(self.menu, tearoff=0) + helpmenu = tkinter.Menu(self.menu, tearoff=0) # Home Page item = lambda: start_browser('http://www.web2py.com/') @@ -180,7 +185,7 @@ class web2pyDialog(object): command=item) # About - item = lambda: tkMessageBox.showinfo('About web2py', ProgramInfo) + item = lambda: messagebox.showinfo('About web2py', ProgramInfo) helpmenu.add_command(label='About', command=item) @@ -194,10 +199,10 @@ class web2pyDialog(object): else: self.root.protocol('WM_DELETE_WINDOW', self.quit) - sticky = Tkinter.NW + sticky = tkinter.NW # Prepare the logo area - self.logoarea = Tkinter.Canvas(self.root, + self.logoarea = tkinter.Canvas(self.root, background=bg_color, width=300, height=300) @@ -206,22 +211,22 @@ class web2pyDialog(object): logo = os.path.join('extras', 'icons', 'splashlogo.gif') if os.path.exists(logo): - img = Tkinter.PhotoImage(file=logo) - pnl = Tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0) + img = tkinter.PhotoImage(file=logo) + pnl = tkinter.Label(self.logoarea, image=img, background=bg_color, bd=0) pnl.pack(side='top', fill='both', expand='yes') # Prevent garbage collection of img pnl.image = img # Prepare the banner area - self.bannerarea = Tkinter.Canvas(self.root, + self.bannerarea = tkinter.Canvas(self.root, bg=bg_color, width=300, height=300) self.bannerarea.grid(row=1, column=1, columnspan=2, sticky=sticky) - Tkinter.Label(self.bannerarea, anchor=Tkinter.N, + tkinter.Label(self.bannerarea, anchor=tkinter.N, text=str(ProgramVersion + "\n" + ProgramAuthor), - font=('Helvetica', 11), justify=Tkinter.CENTER, + font=('Helvetica', 11), justify=tkinter.CENTER, foreground='#195866', background=bg_color, height=3).pack(side='top', fill='both', @@ -230,24 +235,24 @@ class web2pyDialog(object): self.bannerarea.after(1000, self.update_canvas) # IP - Tkinter.Label(self.root, + tkinter.Label(self.root, text='Server IP:', bg=bg_color, - justify=Tkinter.RIGHT).grid(row=4, + justify=tkinter.RIGHT).grid(row=4, column=1, sticky=sticky) self.ips = {} - self.selected_ip = Tkinter.StringVar() + self.selected_ip = tkinter.StringVar() row = 4 ips = [('127.0.0.1', 'Local (IPv4)')] + \ ([('::1', 'Local (IPv6)')] if socket.has_ipv6 else []) + \ [(ip, 'Public') for ip in options.ips] + \ [('0.0.0.0', 'Public')] for ip, legend in ips: - self.ips[ip] = Tkinter.Radiobutton( + self.ips[ip] = tkinter.Radiobutton( self.root, bg=bg_color, highlightthickness=0, selectcolor='light grey', width=30, - anchor=Tkinter.W, text='%s (%s)' % (legend, ip), - justify=Tkinter.LEFT, + anchor=tkinter.W, text='%s (%s)' % (legend, ip), + justify=tkinter.LEFT, variable=self.selected_ip, value=ip) self.ips[ip].grid(row=row, column=2, sticky=sticky) if row == 4: @@ -256,30 +261,30 @@ class web2pyDialog(object): shift = row # Port - Tkinter.Label(self.root, + tkinter.Label(self.root, text='Server Port:', bg=bg_color, - justify=Tkinter.RIGHT).grid(row=shift, + justify=tkinter.RIGHT).grid(row=shift, column=1, pady=10, sticky=sticky) - self.port_number = Tkinter.Entry(self.root) - self.port_number.insert(Tkinter.END, self.options.port) + self.port_number = tkinter.Entry(self.root) + self.port_number.insert(tkinter.END, self.options.port) self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10) # Password - Tkinter.Label(self.root, + tkinter.Label(self.root, text='Choose Password:', bg=bg_color, - justify=Tkinter.RIGHT).grid(row=shift + 1, + justify=tkinter.RIGHT).grid(row=shift + 1, column=1, sticky=sticky) - self.password = Tkinter.Entry(self.root, show='*') + self.password = tkinter.Entry(self.root, show='*') self.password.bind('', lambda e: self.start()) self.password.focus_force() self.password.grid(row=shift + 1, column=2, sticky=sticky) # Prepare the canvas - self.canvas = Tkinter.Canvas(self.root, + self.canvas = tkinter.Canvas(self.root, width=400, height=100, bg='black') @@ -288,19 +293,19 @@ class web2pyDialog(object): self.canvas.after(1000, self.update_canvas) # Prepare the frame - frame = Tkinter.Frame(self.root) + frame = tkinter.Frame(self.root) frame.grid(row=shift + 3, column=1, columnspan=2, pady=5, sticky=sticky) # Start button - self.button_start = Tkinter.Button(frame, + self.button_start = tkinter.Button(frame, text='start server', command=self.start) self.button_start.grid(row=0, column=0, sticky=sticky) # Stop button - self.button_stop = Tkinter.Button(frame, + self.button_stop = tkinter.Button(frame, text='stop server', command=self.stop) @@ -454,9 +459,12 @@ class web2pyDialog(object): def error(self, message): """ Shows error message """ - - import tkMessageBox - tkMessageBox.showerror('web2py start server', message) + if PY2: + import tkMessageBox as messagebox + else: + from tkinter import messagebox + + messagebox.showerror('web2py start server', message) def start(self): """ Starts web2py server """ @@ -1191,10 +1199,13 @@ def start(cron=True): if not options.nogui and options.password == '': try: - import Tkinter + if PY2: + import Tkinter as tkinter + else: + import tkinter havetk = True try: - root = Tkinter.Tk() + root = tkinter.Tk() except: pass except (ImportError, OSError): From fe0f506efc406306f807b0c1cfd4148c97668764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 3 Apr 2017 19:54:06 +0100 Subject: [PATCH 011/149] Create a CookieJar on __init__ instead of creating a new one each post Fixes #1505 --- gluon/contrib/webclient.py | 4 ++-- gluon/tests/test_web.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index 7f6e288b..6454e97f 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -43,6 +43,7 @@ class WebClient(object): self.forms = {} self.history = [] self.cookies = {} + self.cookiejar = cookielib.CookieJar() self.default_headers = default_headers self.sessions = {} self.session_regex = session_regex and re.compile(session_regex) @@ -79,9 +80,8 @@ class WebClient(object): cookies = cookies or {} headers = headers or {} - cj = cookielib.CookieJar() args = [ - urllib2.HTTPCookieProcessor(cj), + urllib2.HTTPCookieProcessor(self.cookiejar), urllib2.HTTPHandler(debuglevel=0) ] # if required do basic auth diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index adac0f28..c8f5be29 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -110,7 +110,7 @@ class TestWeb(LiveTest): self.assertTrue('Welcome Homer' in client.text) client = WebClient('http://127.0.0.1:8000/admin/default/') - client.post('index', data=dict(password='hello')) + client.post('index', data=dict(password='testpass')) client.get('site') client.get('design/welcome') From 6da3a9d8fd7ab38015d521bbd76b16d63c1ce5f6 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Wed, 5 Apr 2017 22:16:21 +0200 Subject: [PATCH 012/149] fix is_email with internationalized Domain Names, close #1582 --- gluon/tests/test_validators.py | 5 +++++ gluon/validators.py | 29 ++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 502b193d..49c3c2b1 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -531,6 +531,11 @@ class TestValidators(unittest.TestCase): rtn = IS_EMAIL(error_message='oops')(42) self.assertEqual(rtn, (42, 'oops')) + # test for Internationalized Domain Names, see https://docs.python.org/2/library/codecs.html#module-encodings.idna + rtn = IS_EMAIL()('web2py@Alliancefrançaise.nu') + self.assertEqual(rtn, ('web2py@Alliancefrançaise.nu', None)) + + def test_IS_LIST_OF_EMAILS(self): emails = ['localguy@localhost', '_Yosemite.Sam@example.com'] rtn = IS_LIST_OF_EMAILS()(','.join(emails)) diff --git a/gluon/validators.py b/gluon/validators.py index 0aa1ec37..360cb17f 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -21,7 +21,7 @@ import urllib import struct import decimal import unicodedata -from gluon._compat import StringIO, long, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native +from gluon._compat import StringIO, long, basestring, unicodeT, to_unicode, urllib_unquote, unichr, to_bytes, PY2, to_unicode, to_native from gluon.utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE from pydal.objects import Field, FieldVirtual, FieldMethod from functools import reduce @@ -1156,15 +1156,16 @@ class IS_EMAIL(Validator): """ - regex = re.compile(''' + body_regex = re.compile(''' ^(?!\.) # name may not begin with a dot ( [-a-z0-9!\#$%&'*+/=?^_`{|}~] # all legal characters except dot | (? Date: Thu, 6 Apr 2017 20:48:12 +0200 Subject: [PATCH 013/149] prevent is_empty from stripping whitespaces, close #1570 --- gluon/tests/test_validators.py | 12 ++++++------ gluon/validators.py | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 502b193d..16bc9977 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -434,21 +434,21 @@ class TestValidators(unittest.TestCase): rtn = IS_NOT_EMPTY()('x') self.assertEqual(rtn, ('x', None)) rtn = IS_NOT_EMPTY()(' x ') - self.assertEqual(rtn, ('x', None)) + self.assertEqual(rtn, (' x ', None)) rtn = IS_NOT_EMPTY()(None) self.assertEqual(rtn, (None, 'Enter a value')) rtn = IS_NOT_EMPTY()('') self.assertEqual(rtn, ('', 'Enter a value')) rtn = IS_NOT_EMPTY()(' ') - self.assertEqual(rtn, ('', 'Enter a value')) + self.assertEqual(rtn, (' ', 'Enter a value')) rtn = IS_NOT_EMPTY()(' \n\t') - self.assertEqual(rtn, ('', 'Enter a value')) + self.assertEqual(rtn, (' \n\t', 'Enter a value')) rtn = IS_NOT_EMPTY()([]) self.assertEqual(rtn, ([], 'Enter a value')) rtn = IS_NOT_EMPTY(empty_regex='def')('def') - self.assertEqual(rtn, ('', 'Enter a value')) + self.assertEqual(rtn, ('def', 'Enter a value')) rtn = IS_NOT_EMPTY(empty_regex='de[fg]')('deg') - self.assertEqual(rtn, ('', 'Enter a value')) + self.assertEqual(rtn, ('deg', 'Enter a value')) rtn = IS_NOT_EMPTY(empty_regex='def')('abc') self.assertEqual(rtn, ('abc', None)) @@ -780,7 +780,7 @@ class TestValidators(unittest.TestCase): rtn = IS_EMPTY_OR(IS_EMAIL())('abc') self.assertEqual(rtn, ('abc', 'Enter a valid email address')) rtn = IS_EMPTY_OR(IS_EMAIL())(' abc ') - self.assertEqual(rtn, ('abc', 'Enter a valid email address')) + self.assertEqual(rtn, (' abc ', 'Enter a valid email address')) rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options(zero=False) self.assertEqual(rtn, [('', ''), ('id1', 'first label'), ('id2', 'second label')]) rtn = IS_EMPTY_OR(IS_IN_SET([('id1', 'first label'), ('id2', 'second label')], zero='zero')).options() diff --git a/gluon/validators.py b/gluon/validators.py index 0aa1ec37..e6b6e848 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -989,14 +989,15 @@ class IS_DECIMAL_IN_RANGE(Validator): def is_empty(value, empty_regex=None): + _value = value """test empty field""" if isinstance(value, (str, unicodeT)): value = value.strip() if empty_regex is not None and empty_regex.match(value): value = '' if value is None or value == '' or value == []: - return (value, True) - return (value, False) + return (_value, True) + return (_value, False) class IS_NOT_EMPTY(Validator): From 232598fd8deeb417716c8dab366f66ee4c368b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Mon, 10 Apr 2017 12:23:34 -0400 Subject: [PATCH 014/149] Added on_succes and custom options for web2py ajax function --- applications/admin/static/js/web2py.js | 23 ++++++++++++++++++++--- applications/examples/static/js/web2py.js | 23 ++++++++++++++++++++--- applications/welcome/static/js/web2py.js | 23 ++++++++++++++++++++--- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 1228961c..e98359fe 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -38,7 +38,7 @@ if (value > 0) $('#' + id).hide().fadeIn('slow'); else $('#' + id).show().fadeOut('slow'); }, - ajax: function (u, s, t) { + ajax: function (u, s, t, options) { /*simple ajax function*/ var query = ''; if (typeof s == 'string') { @@ -59,7 +59,9 @@ query = pcs.join('&'); } } - $.ajax({ + + // default options for jquery ajax function + var ajax_options = { type: 'POST', url: u, data: query, @@ -69,8 +71,23 @@ else if (typeof t == 'string') $('#' + t).html(msg); else t(msg); } + // trigger on_success + if (options.on_success){ + options.on_success(); + } } - }); + }; + + // merge default ajax options with user custom options + for (var attrname in options) { + // not merge custom on_success option + if(attrname != "on_success"){ + ajax_options[attrname] = options[attrname]; + } + } + + // call ajax function + $.ajax(ajax_options); }, ajax_fields: function (target) { /* diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 1228961c..e98359fe 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -38,7 +38,7 @@ if (value > 0) $('#' + id).hide().fadeIn('slow'); else $('#' + id).show().fadeOut('slow'); }, - ajax: function (u, s, t) { + ajax: function (u, s, t, options) { /*simple ajax function*/ var query = ''; if (typeof s == 'string') { @@ -59,7 +59,9 @@ query = pcs.join('&'); } } - $.ajax({ + + // default options for jquery ajax function + var ajax_options = { type: 'POST', url: u, data: query, @@ -69,8 +71,23 @@ else if (typeof t == 'string') $('#' + t).html(msg); else t(msg); } + // trigger on_success + if (options.on_success){ + options.on_success(); + } } - }); + }; + + // merge default ajax options with user custom options + for (var attrname in options) { + // not merge custom on_success option + if(attrname != "on_success"){ + ajax_options[attrname] = options[attrname]; + } + } + + // call ajax function + $.ajax(ajax_options); }, ajax_fields: function (target) { /* diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 1228961c..e98359fe 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -38,7 +38,7 @@ if (value > 0) $('#' + id).hide().fadeIn('slow'); else $('#' + id).show().fadeOut('slow'); }, - ajax: function (u, s, t) { + ajax: function (u, s, t, options) { /*simple ajax function*/ var query = ''; if (typeof s == 'string') { @@ -59,7 +59,9 @@ query = pcs.join('&'); } } - $.ajax({ + + // default options for jquery ajax function + var ajax_options = { type: 'POST', url: u, data: query, @@ -69,8 +71,23 @@ else if (typeof t == 'string') $('#' + t).html(msg); else t(msg); } + // trigger on_success + if (options.on_success){ + options.on_success(); + } } - }); + }; + + // merge default ajax options with user custom options + for (var attrname in options) { + // not merge custom on_success option + if(attrname != "on_success"){ + ajax_options[attrname] = options[attrname]; + } + } + + // call ajax function + $.ajax(ajax_options); }, ajax_fields: function (target) { /* From 02d2fefc214b35184fbc2c45cb20f0da2bee3b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Wed, 12 Apr 2017 08:48:52 -0400 Subject: [PATCH 015/149] renamed "on_succes" option to "done" allowing multiple inputs renamed "on_succes" option to "done" allowing multiple inputs,code refactoring --- applications/admin/static/js/web2py.js | 43 +++++++++++++++-------- applications/examples/static/js/web2py.js | 43 +++++++++++++++-------- applications/welcome/static/js/web2py.js | 43 +++++++++++++++-------- 3 files changed, 84 insertions(+), 45 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index e98359fe..313d00bc 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -40,6 +40,10 @@ }, ajax: function (u, s, t, options) { /*simple ajax function*/ + + // set options default value + options = typeof options !== 'undefined' ? options : {}; + var query = ''; if (typeof s == 'string') { var d = $(s).serialize(); @@ -60,30 +64,39 @@ } } - // default options for jquery ajax function + // default success action + var success_function = function (msg) { + if (t) { + if (t == ':eval') eval(msg); + else if (typeof t == 'string') $('#' + t).html(msg); + else t(msg); + } + }; + + // declare success actions as array + var success = [success_function]; + + // add user success actions + if ($.isArray(options.done)){ + success = $.merge(success, options.done); + } else { + success.push(options.done); + } + + // default jquery ajax options var ajax_options = { type: 'POST', url: u, data: query, - success: function (msg) { - if (t) { - if (t == ':eval') eval(msg); - else if (typeof t == 'string') $('#' + t).html(msg); - else t(msg); - } - // trigger on_success - if (options.on_success){ - options.on_success(); - } - } + success: success }; + //remove custom "done" option if exists + delete options.done; + // merge default ajax options with user custom options for (var attrname in options) { - // not merge custom on_success option - if(attrname != "on_success"){ ajax_options[attrname] = options[attrname]; - } } // call ajax function diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index e98359fe..313d00bc 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -40,6 +40,10 @@ }, ajax: function (u, s, t, options) { /*simple ajax function*/ + + // set options default value + options = typeof options !== 'undefined' ? options : {}; + var query = ''; if (typeof s == 'string') { var d = $(s).serialize(); @@ -60,30 +64,39 @@ } } - // default options for jquery ajax function + // default success action + var success_function = function (msg) { + if (t) { + if (t == ':eval') eval(msg); + else if (typeof t == 'string') $('#' + t).html(msg); + else t(msg); + } + }; + + // declare success actions as array + var success = [success_function]; + + // add user success actions + if ($.isArray(options.done)){ + success = $.merge(success, options.done); + } else { + success.push(options.done); + } + + // default jquery ajax options var ajax_options = { type: 'POST', url: u, data: query, - success: function (msg) { - if (t) { - if (t == ':eval') eval(msg); - else if (typeof t == 'string') $('#' + t).html(msg); - else t(msg); - } - // trigger on_success - if (options.on_success){ - options.on_success(); - } - } + success: success }; + //remove custom "done" option if exists + delete options.done; + // merge default ajax options with user custom options for (var attrname in options) { - // not merge custom on_success option - if(attrname != "on_success"){ ajax_options[attrname] = options[attrname]; - } } // call ajax function diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index e98359fe..313d00bc 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -40,6 +40,10 @@ }, ajax: function (u, s, t, options) { /*simple ajax function*/ + + // set options default value + options = typeof options !== 'undefined' ? options : {}; + var query = ''; if (typeof s == 'string') { var d = $(s).serialize(); @@ -60,30 +64,39 @@ } } - // default options for jquery ajax function + // default success action + var success_function = function (msg) { + if (t) { + if (t == ':eval') eval(msg); + else if (typeof t == 'string') $('#' + t).html(msg); + else t(msg); + } + }; + + // declare success actions as array + var success = [success_function]; + + // add user success actions + if ($.isArray(options.done)){ + success = $.merge(success, options.done); + } else { + success.push(options.done); + } + + // default jquery ajax options var ajax_options = { type: 'POST', url: u, data: query, - success: function (msg) { - if (t) { - if (t == ':eval') eval(msg); - else if (typeof t == 'string') $('#' + t).html(msg); - else t(msg); - } - // trigger on_success - if (options.on_success){ - options.on_success(); - } - } + success: success }; + //remove custom "done" option if exists + delete options.done; + // merge default ajax options with user custom options for (var attrname in options) { - // not merge custom on_success option - if(attrname != "on_success"){ ajax_options[attrname] = options[attrname]; - } } // call ajax function From ab537242c4af8411057695933b470dbc5c88d280 Mon Sep 17 00:00:00 2001 From: sugizo Date: Thu, 13 Apr 2017 06:31:40 +0700 Subject: [PATCH 016/149] appadmin can use ccache with cache.ram = cache.disk = cache.memcache --- gluon/contrib/memcache/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gluon/contrib/memcache/__init__.py b/gluon/contrib/memcache/__init__.py index a2a095dd..137c7294 100644 --- a/gluon/contrib/memcache/__init__.py +++ b/gluon/contrib/memcache/__init__.py @@ -21,6 +21,9 @@ def MemcacheClient(*a, **b): class MemcacheClientObj(Client): + def initialize(self): + pass + meta_storage = {} max_time_expire = 24*3600 From 60a6180a77110074ec49fdb4e1748a12ef1ce2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Thu, 27 Apr 2017 10:35:27 -0400 Subject: [PATCH 017/149] fixes in generic layouts for python 3 compatibility --- applications/admin/views/generic.html | 2 +- applications/examples/views/generic.html | 2 +- applications/examples/views/generic.load | 2 +- applications/welcome/views/generic.html | 2 +- applications/welcome/views/generic.load | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/applications/admin/views/generic.html b/applications/admin/views/generic.html index f6ee4962..2ffbd521 100644 --- a/applications/admin/views/generic.html +++ b/applications/admin/views/generic.html @@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers """}}

{{=' '.join(x.capitalize() for x in request.function.split('_'))}}

{{if len(response._vars)==1:}} -{{=BEAUTIFY(response._vars.values()[0])}} +{{=BEAUTIFY(response._vars[next(iter(response._vars))])}} {{elif len(response._vars)>1:}} {{=BEAUTIFY(response._vars)}} {{pass}} diff --git a/applications/examples/views/generic.html b/applications/examples/views/generic.html index f6ee4962..2ffbd521 100644 --- a/applications/examples/views/generic.html +++ b/applications/examples/views/generic.html @@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers """}}

{{=' '.join(x.capitalize() for x in request.function.split('_'))}}

{{if len(response._vars)==1:}} -{{=BEAUTIFY(response._vars.values()[0])}} +{{=BEAUTIFY(response._vars[next(iter(response._vars))])}} {{elif len(response._vars)>1:}} {{=BEAUTIFY(response._vars)}} {{pass}} diff --git a/applications/examples/views/generic.load b/applications/examples/views/generic.load index e5f1adb3..921a3665 100644 --- a/applications/examples/views/generic.load +++ b/applications/examples/views/generic.load @@ -1 +1 @@ -{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}} \ No newline at end of file +{{response.headers['web2py-response-flash']=response.flash}}{{if len(response._vars)==1:}}{{=response._vars[next(iter(response._vars))]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}} \ No newline at end of file diff --git a/applications/welcome/views/generic.html b/applications/welcome/views/generic.html index f6ee4962..2ffbd521 100644 --- a/applications/welcome/views/generic.html +++ b/applications/welcome/views/generic.html @@ -7,7 +7,7 @@ It is used as default when a view is not provided for your controllers """}}

{{=' '.join(x.capitalize() for x in request.function.split('_'))}}

{{if len(response._vars)==1:}} -{{=BEAUTIFY(response._vars.values()[0])}} +{{=BEAUTIFY(response._vars[next(iter(response._vars))])}} {{elif len(response._vars)>1:}} {{=BEAUTIFY(response._vars)}} {{pass}} diff --git a/applications/welcome/views/generic.load b/applications/welcome/views/generic.load index e898cc48..03f0416b 100644 --- a/applications/welcome/views/generic.load +++ b/applications/welcome/views/generic.load @@ -27,4 +27,4 @@ Notice: - no need to return a string even if the function is called via ajax. -'''}}{{if len(response._vars)==1:}}{{=response._vars.values()[0]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}} \ No newline at end of file +'''}}{{if len(response._vars)==1:}}{{=response._vars[next(iter(response._vars))]}}{{else:}}{{=BEAUTIFY(response._vars)}}{{pass}} \ No newline at end of file From 4d117af85f7b764a9dd6bdc0152ae6eea12c2048 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Thu, 27 Apr 2017 18:08:59 +0200 Subject: [PATCH 018/149] fix open file in py3, close #1609 --- scripts/cpdb.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/cpdb.py b/scripts/cpdb.py index a04690b1..2e5e949b 100644 --- a/scripts/cpdb.py +++ b/scripts/cpdb.py @@ -15,6 +15,7 @@ except ImportError: import readline try: from gluon import DAL + from gluon.fileutils import open_file except ImportError as err: print('gluon path not found') @@ -531,7 +532,7 @@ class setCopyDB(): self.db.export_to_csv_file(open('tmp.sql', 'wb')) print 'importing data...' - other_db.import_from_csv_file(open('tmp.sql', 'rb')) + other_db.import_from_csv_file(open_file('tmp.sql', 'rb')) other_db.commit() print 'done!' print 'Attention: do not run this program again or you end up with duplicate records' From 9ab7ed002947770ea312dc84e2e1f9ea49364b62 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 28 Apr 2017 08:01:20 -0500 Subject: [PATCH 019/149] catching errors in python3 in custom_import --- gluon/custom_import.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/custom_import.py b/gluon/custom_import.py index ef6c7ab2..d9eab5b2 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -75,7 +75,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): try: oname = name if not name.startswith('.') else '.'+name return NATIVE_IMPORTER(oname, globals, locals, fromlist, level) - except ImportError: + except (ImportError, KeyError): items = current.request.folder.split(os.path.sep) if not items[-1]: items = items[:-1] @@ -100,7 +100,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1): import_tb = sys.exc_info()[2] try: return NATIVE_IMPORTER(name, globals, locals, fromlist, level) - except ImportError as e3: + except (ImportError, KeyError) as e3: raise ImportError(e1, import_tb) # there an import error in the module except Exception as e2: raise # there is an error in the module From 90e606dcfdd987249fc1b72dfd7253530a9ef94a Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 29 Apr 2017 20:41:12 +0200 Subject: [PATCH 020/149] fix cron with py3, added initial tests, close #1618 --- gluon/newcron.py | 9 ++++++--- gluon/tests/__init__.py | 1 + gluon/tests/test_cron.py | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 gluon/tests/test_cron.py diff --git a/gluon/newcron.py b/gluon/newcron.py index 1a878589..f7482fcb 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -19,13 +19,14 @@ import sched import re import datetime import platform -import gluon.fileutils from functools import reduce try: import cPickle as pickle except: import pickle from gluon.settings import global_settings +from gluon import fileutils +from gluon._compat import to_bytes from pydal.contrib import portalocker logger = logging.getLogger("web2py.cron") @@ -116,7 +117,7 @@ class Token(object): def __init__(self, path): self.path = os.path.join(path, 'cron.master') if not os.path.exists(self.path): - fileutils.write_file(self.path, '', 'wb') + fileutils.write_file(self.path, to_bytes(''), 'wb') self.master = None self.now = time.time() @@ -139,7 +140,7 @@ class Token(object): if portalocker.LOCK_EX is None: logger.warning('WEB2PY CRON: Disabled because no file locking') return None - self.master = open(self.path, 'rb+') + self.master = fileutils.open_file(self.path, 'rb+') try: ret = None portalocker.lock(self.master, portalocker.LOCK_EX) @@ -167,6 +168,7 @@ class Token(object): """ Writes into cron.master the time when cron job was completed """ + ret = self.master.closed if not self.master.closed: portalocker.lock(self.master, portalocker.LOCK_EX) logger.debug('WEB2PY CRON: Releasing cron lock') @@ -177,6 +179,7 @@ class Token(object): pickle.dump((self.now, time.time()), self.master) portalocker.unlock(self.master) self.master.close() + return ret def rangetolist(s, period='min'): diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 2f111fe3..7c200f94 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -22,6 +22,7 @@ from .test_appadmin import * from .test_web import * from .test_sqlhtml import * from .test_scheduler import * +from .test_cron import * if sys.version[:3] == '2.7': from .test_is_url import * diff --git a/gluon/tests/test_cron.py b/gluon/tests/test_cron.py new file mode 100644 index 00000000..47a30af7 --- /dev/null +++ b/gluon/tests/test_cron.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" Unit tests for cron """ +import unittest, os +from gluon.newcron import Token, crondance + +class TestCron(unittest.TestCase): + + + def test_Token(self): + appname_path = os.path.join(os.getcwd(), 'applications', 'welcome') + t = Token(path=appname_path) + self.assertNotEqual(t.acquire(), None) + self.assertFalse(t.release()) + self.assertEqual(t.acquire(), None) + self.assertTrue(t.release()) + return + + def test_crondance(self): + #TODO update crondance to return something + crondance(os.getcwd()) + + From 69e6e79e232db922b0ae4514a6820ee5d2dcf9bb Mon Sep 17 00:00:00 2001 From: geomapdev Date: Mon, 1 May 2017 08:39:31 -0700 Subject: [PATCH 021/149] Update dal.py updated to handle references without format --- gluon/dal.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index 95476a9f..27195f1a 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -51,11 +51,13 @@ def _default_validators(db, field): if hasattr(referenced, '_format') and referenced._format: requires = validators.IS_IN_DB(db, referenced._id, referenced._format) - if field.unique: - requires._and = validators.IS_NOT_IN_DB(db, field) - if field.tablename == field_type[10:]: - return validators.IS_EMPTY_OR(requires) - return requires + else: + requires = validators.IS_IN_DB(db, referenced._id) + if field.unique: + requires._and = validators.IS_NOT_IN_DB(db, field) + if not field.notnull: + return validators.IS_EMPTY_OR(requires) + return requires elif db and field_type.startswith('list:reference') and \ field_type.find('.') < 0 and \ field_type[15:] in db.tables: From 57b554d61876d5432e6797a61185975feb9c4b10 Mon Sep 17 00:00:00 2001 From: Andrew Willimott Date: Thu, 4 May 2017 07:03:07 +1200 Subject: [PATCH 022/149] Initial d3 graph commit. Add d3 to graph layout --- applications/admin/controllers/appadmin.py | 55 +++++- applications/admin/views/appadmin.html | 13 ++ applications/admin/views/default/design.html | 4 + applications/examples/controllers/appadmin.py | 55 +++++- applications/examples/views/appadmin.html | 13 ++ applications/welcome/controllers/appadmin.py | 55 +++++- applications/welcome/static/css/d3_graph.css | 33 ++++ applications/welcome/static/js/d3.min.js | 8 + applications/welcome/static/js/d3_graph.js | 181 ++++++++++++++++++ applications/welcome/views/appadmin.html | 13 ++ 10 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 applications/welcome/static/css/d3_graph.css create mode 100644 applications/welcome/static/js/d3.min.js create mode 100644 applications/welcome/static/js/d3_graph.js diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index a602a95c..5bca14de 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -12,6 +12,7 @@ import gluon.contenttype import gluon.fileutils from gluon._compat import iteritems +# d3_graph_model added but leaving pygraphviz code as is for initial tests. try: import pygraphviz as pgv except ImportError: @@ -566,6 +567,9 @@ def table_template(table): ).xml() +# d3_graph_model added but leaving pygraphviz code as is for initial tests. +# The Graph Model button in admin app views/default/design.html has been redirected +# to the d3_graph_model function. def bg_graph_model(): graph = pgv.AGraph(layout='dot', directed=True, strict=False, rankdir='LR') @@ -699,4 +703,53 @@ def hooks(): ul_t.append(LI (op['name'])) ul_t.append(UL([LI(A(f['funcname'], _class="editor_filelink", _href=f['url']if 'url' in f else None, **{'_data-lineno':f['lineno']-1})) for f in op['functions']])) ul_main.append(ul_t) - return ul_main + return + + +# ########################################################## +# d3 based model visualizations +# ########################################################### + +def d3_graph_model(): + """ See https://www.facebook.com/web2py/posts/145613995589010 from Bruno Rocha + and also the app_admin bg_graph_model function + + Create a list of table dicts, called "nodes" + """ + + data = {} + nodes = [] + links = [] + + subgraphs = dict() + + for tablename in db.tables: + fields = [] + for field in db[tablename]: + f_type = field.type + if not isinstance(f_type,str): + disp = ' ' + elif f_type == 'string': + disp = field.length + elif f_type == 'id': + disp = "PK" + elif f_type.startswith('reference') or \ + f_type.startswith('list:reference'): + disp = "FK" + else: + disp = ' ' + fields.append(dict(name= field.name, type=field.type, disp = disp)) + + if isinstance(f_type,str) and ( + f_type.startswith('reference') or + f_type.startswith('list:reference')): + referenced_table = f_type.split()[1].split('.')[0] + + links.append(dict(source=tablename, target = referenced_table)) + + nodes.append(dict(name=tablename, type="table", fields = fields)) + + # d3 v4 allows individual modules to be specified. The complete d3 library is included below. + response.files.append(URL('static','js/d3.min.js')) + response.files.append(URL('static','js/d3_graph.js')) + return dict(nodes=nodes, links=links) \ No newline at end of file diff --git a/applications/admin/views/appadmin.html b/applications/admin/views/appadmin.html index d054ff5a..c92e4058 100644 --- a/applications/admin/views/appadmin.html +++ b/applications/admin/views/appadmin.html @@ -258,6 +258,19 @@ {{pass}} {{pass}} +{{if request.function=='d3_graph_model':}} +

{{=T("Graph Model")}}

+
+ + +{{pass}} + {{if request.function == 'manage':}}

{{=heading}}