From 3292f760ca7255c878ed84470898c5893d53b23f Mon Sep 17 00:00:00 2001 From: geomapdev Date: Thu, 20 Apr 2017 10:21:16 -0700 Subject: [PATCH 001/230] handle reference fields for keyed tables --- gluon/dal.py | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index 95476a9f..e60cf82d 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -51,11 +51,29 @@ 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 field.tablename == field_type[10:]: + return validators.IS_EMPTY_OR(requires) + return requires + elif db and field_type.startswith('reference') and \ + field_type.find('.') > 0 and \ + field_type[10:].split('.')[0] in db.tables: + table_name=field_type[10:].split('.')[0] + field_name=field_type[10:].split('.')[1] + referenced = db[table_name] + if hasattr(referenced, '_format') and referenced._format: + requires = validators.IS_IN_DB(db, referenced[field_name], + referenced._format) + else: + requires = validators.IS_IN_DB(db, referenced[field_name]) + if field.unique: + requires._and = validators.IS_NOT_IN_DB(db, field) + if field.tablename == table_name: + 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: @@ -71,6 +89,23 @@ def _default_validators(db, field): if not field.notnull: requires = validators.IS_EMPTY_OR(requires) return requires + elif db and field_type.startswith('list:reference') and \ + field_type.find('.') > 0 and \ + field_type[15:].split('.')[0] in db.tables: + table_name=field_type[15:].split('.')[0] + field_name=field_type[15:].split('.')[1] + referenced = db[table_name] + if hasattr(referenced, '_format') and referenced._format: + requires = validators.IS_IN_DB(db, referenced[field_name], + referenced._format, multiple=True) + else: + requires = validators.IS_IN_DB(db, referenced[field_name], + multiple=True) + if field.unique: + requires._and = validators.IS_NOT_IN_DB(db, field) + if not field.notnull: + requires = validators.IS_EMPTY_OR(requires) + return requires # does not get here for reference and list:reference if field.unique: requires.insert(0, validators.IS_NOT_IN_DB(db, field)) From 69e6e79e232db922b0ae4514a6820ee5d2dcf9bb Mon Sep 17 00:00:00 2001 From: geomapdev Date: Mon, 1 May 2017 08:39:31 -0700 Subject: [PATCH 002/230] 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 4a47bb8e83acaf451eac36f440e7a368eecc7369 Mon Sep 17 00:00:00 2001 From: Andrew Willimott Date: Wed, 2 Aug 2017 21:47:26 +1200 Subject: [PATCH 003/230] =?UTF-8?q?d3=5Fgraph=5Fmodel=20fixed,=20no=20long?= =?UTF-8?q?er=20hardcoded=20for=20a=20=E2=80=9Cdb=E2=80=9D=20database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- applications/admin/controllers/appadmin.py | 49 +++++++++---------- applications/examples/controllers/appadmin.py | 49 +++++++++---------- applications/welcome/controllers/appadmin.py | 49 +++++++++---------- 3 files changed, 72 insertions(+), 75 deletions(-) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 139ad1aa..74240e59 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -657,37 +657,36 @@ def d3_graph_model(): Create a list of table dicts, called "nodes" """ - data = {} nodes = [] links = [] - subgraphs = dict() + for database in databases: + db = eval_in_global_env(database) + 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)) - 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] - 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)) - links.append(dict(source=tablename, target = referenced_table)) - - nodes.append(dict(name=tablename, type="table", fields = fields)) + 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('admin','static','js/d3.min.js')) diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 139ad1aa..74240e59 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -657,37 +657,36 @@ def d3_graph_model(): Create a list of table dicts, called "nodes" """ - data = {} nodes = [] links = [] - subgraphs = dict() + for database in databases: + db = eval_in_global_env(database) + 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)) - 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] - 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)) - links.append(dict(source=tablename, target = referenced_table)) - - nodes.append(dict(name=tablename, type="table", fields = fields)) + 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('admin','static','js/d3.min.js')) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 139ad1aa..74240e59 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -657,37 +657,36 @@ def d3_graph_model(): Create a list of table dicts, called "nodes" """ - data = {} nodes = [] links = [] - subgraphs = dict() + for database in databases: + db = eval_in_global_env(database) + 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)) - 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] - 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)) - links.append(dict(source=tablename, target = referenced_table)) - - nodes.append(dict(name=tablename, type="table", fields = fields)) + 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('admin','static','js/d3.min.js')) From b7270df9c3f3b87895e9420afdc11b68465ebc12 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 4 Aug 2017 09:58:05 -0500 Subject: [PATCH 004/230] fixed issue topic/web2py/UnN6AyOh2Lg thanks Paul --- gluon/sqlhtml.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index cb317e82..0653c4c7 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1939,11 +1939,13 @@ class SQLFORM(FORM): if settings.global_settings.web2py_runtime_gae: return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields]) else: + if not (sfields and key and key.split()): + return fields[0].table return reduce(lambda a,b:a&b,[ reduce(lambda a,b: a|b, [ field.contains(k) for field in sfields] ) for k in key.split()]) - + # from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4 # needs testing... #words = key.split(' ') if key else [] From ebc614bf91877d82ce34bb57c8d5ce9261f3620c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 4 Aug 2017 10:21:43 -0500 Subject: [PATCH 005/230] fixed has_ssl, thanks leonel --- gluon/main.py | 2 +- gluon/packages/dal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/main.py b/gluon/main.py index 32103ea6..e5d73e21 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -745,7 +745,7 @@ class HttpServer(object): sock_list = [ip, port] if not ssl_certificate or not ssl_private_key: logger.info('SSL is off') - elif not rocket.ssl: + elif not rocket.has_ssl: logger.warning('Python "ssl" module unavailable. SSL is OFF') elif not exists(ssl_certificate): logger.warning('unable to open SSL certificate. SSL is OFF') diff --git a/gluon/packages/dal b/gluon/packages/dal index f66ad176..c707d558 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit f66ad176943f55d80a4e3f9f8cab659a3852c343 +Subproject commit c707d558995d0318022f3a26935f1100020975fc From a744835f2159dfe1de1f2d882ab81a076ad664d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sat, 5 Aug 2017 13:27:44 +0100 Subject: [PATCH 006/230] Fix response.download with nonasccii filenames Fixes #1718 --- gluon/globals.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gluon/globals.py b/gluon/globals.py index 267ea661..7ec9a0a1 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -14,7 +14,7 @@ Contains the classes for the global used variables: """ from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \ - unicodeT, long, hashlib_md5 + unicodeT, long, hashlib_md5, urllib_quote from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -641,6 +641,11 @@ class Response(Storage): if download_filename is None: download_filename = filename if attachment: + # Browsers still don't have a simple uniform way to have non ascii + # characters in the filename so for now we are percent encoding it + if isinstance(download_filename, unicodeT): + download_filename = download_filename.encode('utf-8') + download_filename = urllib_quote(download_filename) headers['Content-Disposition'] = \ 'attachment; filename="%s"' % download_filename.replace('"', '\"') return self.stream(stream, chunk_size=chunk_size, request=request) From 90e20a4f393fe5bdced41ad30c02f8dbfd8bc34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 6 Aug 2017 17:01:27 +0100 Subject: [PATCH 007/230] Fix BEAUTIFY trying to display uploaded file contents Fixes #1717 --- gluon/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/html.py b/gluon/html.py index f5383109..73eae9b8 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -2431,7 +2431,7 @@ class BEAUTIFY(DIV): if level == 0: return for c in self.components: - if hasattr(c, 'value') and not callable(c.value): + if hasattr(c, 'value') and not callable(c.value) and not isinstance(c, cgi.FieldStorage): if c.value: components.append(c.value) if hasattr(c, 'xml') and callable(c.xml): From 0b41ed36f92a1973dda66e7479e062cd9658678a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Sun, 6 Aug 2017 19:20:01 +0100 Subject: [PATCH 008/230] mobilize is back Fixes #1721 --- gluon/contrib/user_agent_parser.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gluon/contrib/user_agent_parser.py b/gluon/contrib/user_agent_parser.py index bba1e436..61fee028 100644 --- a/gluon/contrib/user_agent_parser.py +++ b/gluon/contrib/user_agent_parser.py @@ -673,3 +673,20 @@ def simple_detect(agent): if os_version: os = " ".join((os, os_version)) return os, browser + + +class mobilize(object): + """ + Decorator for controller functions so they use different views for mobile devices. + """ + def __init__(self, func): + self.func = func + + def __call__(self): + from gluon import current + user_agent = current.request.user_agent() + if user_agent.is_mobile: + items = current.response.view.split('.') + items.insert(-1, 'mobile') + current.response.view = '.'.join(items) + return self.func() From 10b6b93cb2ac6118bad2207f87903722110a7745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 7 Aug 2017 00:17:43 +0100 Subject: [PATCH 009/230] minor py3 compatibility change --- gluon/restricted.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gluon/restricted.py b/gluon/restricted.py index 3fe5bfb0..5f3ffc10 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -10,7 +10,7 @@ Restricted environment to execute application's code """ import sys -from gluon._compat import pickle, ClassType +from gluon._compat import pickle, ClassType, unicodeT, to_bytes import traceback import types import os @@ -192,10 +192,10 @@ class RestrictedError(Exception): # safely show an useful message to the user try: output = self.output - if isinstance(output, unicode): - output = output.encode("utf8") - elif not isinstance(output, str): + if not isinstance(output, str, bytes, bytearray): output = str(output) + if isinstance(output, unicodeT): + output = to_bytes(output) except: output = "" return output From 3eea1f68f477f1d391edda93e71eedae82ad07bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 7 Aug 2017 00:20:29 +0100 Subject: [PATCH 010/230] Make sure you return bytes when you str(body) --- gluon/http.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gluon/http.py b/gluon/http.py index 539a00fd..179dc4fe 100644 --- a/gluon/http.py +++ b/gluon/http.py @@ -11,7 +11,7 @@ HTTP statuses helpers """ import re -from gluon._compat import iteritems, unicodeT +from gluon._compat import iteritems, unicodeT, to_bytes __all__ = ['HTTP', 'redirect'] @@ -111,6 +111,8 @@ class HTTP(Exception): if not body: body = status if isinstance(body, (str, bytes, bytearray)): + if isinstance(body, unicodeT): + body = to_bytes(body) # This must be done before len headers['Content-Length'] = len(body) rheaders = [] for k, v in iteritems(headers): @@ -123,12 +125,15 @@ class HTTP(Exception): return [''] elif isinstance(body, (str, bytes, bytearray)): if isinstance(body, unicodeT): - body = body.encode('utf-8') + body = to_bytes(body) return [body] elif hasattr(body, '__iter__'): return body else: - return [str(body)] + body = str(body) + if isinstance(body, unicodeT): + body = to_bytes(body) + return [body] @property def message(self): From dc29f333656c0a1b7dcb17e792dc33fc0ae28629 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 7 Aug 2017 07:18:42 -0500 Subject: [PATCH 011/230] addressed __ssl.pyd issue #1716, thanks Jhleite --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 1f6e9099..6d72e470 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,8 @@ win: cp -r applications/welcome ../web2py_win/web2py/applications cp -r applications/examples ../web2py_win/web2py/applications cp applications/__init__.py ../web2py_win/web2py/applications + # per https://github.com/web2py/web2py/issues/1716 + mv ../web2py_win/_ssl.pyd ../web2py_win/_ssl.pyd.legacy cd ../web2py_win; zip -r web2py_win.zip web2py mv ../web2py_win/web2py_win.zip . run: From a29947f298e508a23425c9c151281a81a67904f5 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 7 Aug 2017 07:27:28 -0500 Subject: [PATCH 012/230] websocket_messaging in Python 3 #1696, thanks hirolau --- gluon/contrib/websocket_messaging.py | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/gluon/contrib/websocket_messaging.py b/gluon/contrib/websocket_messaging.py index ae8fbc4f..962e42a2 100644 --- a/gluon/contrib/websocket_messaging.py +++ b/gluon/contrib/websocket_messaging.py @@ -94,32 +94,10 @@ import optparse import time import sys import gluon.utils - -if (sys.version_info[0] == 2): - from urllib import urlencode, urlopen - def to_bytes(obj, charset='utf-8', errors='strict'): - if obj is None: - return None - if isinstance(obj, (bytes, bytearray, buffer)): - return bytes(obj) - if isinstance(obj, unicode): - return obj.encode(charset, errors) - raise TypeError('Expected bytes') -else: - from urllib.request import urlopen - from urllib.parse import urlencode - def to_bytes(obj, charset='utf-8', errors='strict'): - if obj is None: - return None - if isinstance(obj, (bytes, bytearray, memoryview)): - return bytes(obj) - if isinstance(obj, str): - return obj.encode(charset, errors) - raise TypeError('Expected bytes') +from gluon._compat import to_native, to_bytes, urlencode, urlopen listeners, names, tokens = {}, {}, {} - def websocket_send(url, message, hmac_key=None, group='default'): sig = hmac_key and hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() or '' params = urlencode( @@ -138,8 +116,8 @@ class PostHandler(tornado.web.RequestHandler): if hmac_key and not 'signature' in self.request.arguments: self.send_error(401) if 'message' in self.request.arguments: - message = self.request.arguments['message'][0] - group = self.request.arguments.get('group', ['default'])[0] + message = self.request.arguments['message'][0].decode(encoding='UTF-8') + group = self.request.arguments.get('group', ['default'])[0].decode(encoding='UTF-8') print('%s:MESSAGE to %s:%s' % (time.time(), group, message)) if hmac_key: signature = self.request.arguments['signature'][0] From 86ea728f4f51c1312216f552899b7e6bf75ed31d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 7 Aug 2017 07:32:58 -0500 Subject: [PATCH 013/230] R-2.15.3 --- CHANGELOG | 2 +- Makefile | 2 +- VERSION | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3aa5d351..1df92c35 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -## 2.15.0b1 +## 2.15.1-3 - dropped support for python 2.6 - dropped web shell - experimental python 3 support diff --git a/Makefile b/Makefile index 6d72e470..543882fb 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.15.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.15.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION ### rm -f all junk files make clean ### clean up baisc apps diff --git a/VERSION b/VERSION index 09af535e..6b247f67 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.15.2-stable+timestamp.2017.07.19.01.21.31 +Version 2.15.3-stable+timestamp.2017.08.07.07.32.04 From 7e96ecafd720a2179da8ec3dbb3d14fb2b3f8da0 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 7 Aug 2017 07:41:11 -0500 Subject: [PATCH 014/230] R-2.15.3 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 543882fb..3016e604 100644 --- a/Makefile +++ b/Makefile @@ -98,7 +98,7 @@ win: cp -r applications/examples ../web2py_win/web2py/applications cp applications/__init__.py ../web2py_win/web2py/applications # per https://github.com/web2py/web2py/issues/1716 - mv ../web2py_win/_ssl.pyd ../web2py_win/_ssl.pyd.legacy + mv ../web2py_win/web2py/_ssl.pyd ../web2py_win/web2py/_ssl.pyd.legacy cd ../web2py_win; zip -r web2py_win.zip web2py mv ../web2py_win/web2py_win.zip . run: From 8533fa0d00106f621c865f9db04a6044c84bf484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Tue, 8 Aug 2017 00:50:55 +0100 Subject: [PATCH 015/230] put is_mobile and is_tablet in the result of user_agent() --- gluon/contrib/user_agent_parser.py | 3 +++ gluon/globals.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gluon/contrib/user_agent_parser.py b/gluon/contrib/user_agent_parser.py index 61fee028..f966b616 100644 --- a/gluon/contrib/user_agent_parser.py +++ b/gluon/contrib/user_agent_parser.py @@ -678,6 +678,9 @@ def simple_detect(agent): class mobilize(object): """ Decorator for controller functions so they use different views for mobile devices. + + WARNING: If you update httpagentparser make sure to leave mobilize for + backwards compatibility. """ def __init__(self, func): self.func = func diff --git a/gluon/globals.py b/gluon/globals.py index 267ea661..e2ec33f1 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -331,11 +331,16 @@ class Request(Storage): user_agent = session._user_agent if user_agent: return user_agent - user_agent = user_agent_parser.detect(self.env.http_user_agent) + http_user_agent = self.env.http_user_agent + user_agent = user_agent_parser.detect(http_user_agent) for key, value in user_agent.items(): if isinstance(value, dict): user_agent[key] = Storage(value) - user_agent = session._user_agent = Storage(user_agent) + user_agent = Storage(user_agent) + user_agent.is_mobile = 'Mobile' in http_user_agent + user_agent.is_tablet = 'Tablet' in http_user_agent + session._user_agent = user_agent + return user_agent def requires_https(self): From 485f868cd10bec18e067110ce947dc0ad623d0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Mon, 7 Aug 2017 15:07:02 -0400 Subject: [PATCH 016/230] auth.has_membership(cached=True) check membership in auth.user_groups --- gluon/authapi.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index d0922384..faef8c1e 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -561,23 +561,39 @@ class AuthAPI(object): del self.user_groups[group_id] return ret - def has_membership(self, group_id=None, user_id=None, role=None): + def has_membership(self, group_id=None, user_id=None, role=None, cached=False): """ Checks if user is member of group_id or role + + NOTE: To avoid database query at each page load that use auth.has_membership, someone can use cached=True. + If cached is set to True has_membership() check group_id or role only against auth.user_groups variable + which is populated properly only at login time. This means that if an user membership change during a + given session the user has to log off and log in again in order to auth.user_groups to be properly + recreated and reflecting the user membership modification. There is one exception to this log off and + log in process which is in case that the user change his own membership, in this case auth.user_groups + can be properly update for the actual connected user because web2py has access to the proper session + user_groups variable. To make use of this exception someone has to place an "auth.update_groups()" + instruction in his app code to force auth.user_groups to be updated. As mention this will only work if it + the user itself that change it membership not if another user, let say an administrator, change someone + else's membership. """ - 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 + if cached: + id_role = group_id or role + r = (user_id and id_role in self.user_groups.values()) or (user_id and id_role in self.user_groups) else: - r = False + 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 + 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 From 6e5c8b62cc2cf3dab774a721863db1006fce3238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 9 Aug 2017 01:05:37 +0100 Subject: [PATCH 017/230] Make test_include_files demand the same order --- gluon/tests/test_globals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index 390079ce..da403dbe 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -158,10 +158,10 @@ class testResponse(unittest.TestCase): response.files.append(URL('a', 'static', 'css/file.ts')) content = return_includes(response) self.assertEqual(content, + '' + '' + '' + - '' + - '' + '' ) response = Response() From 892fba9e54209228e06e7b4570ece1345b741d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 9 Aug 2017 01:08:28 +0100 Subject: [PATCH 018/230] fix unordered include_files result Now only concats adjacent internal to the application stuff, this also Fixes #1673 --- gluon/globals.py | 91 +++++++++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index c47c596c..f9f5013b 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -473,45 +473,67 @@ class Response(Storage): response.cache_includes = (cache_method, time_expire). Example: (cache.disk, 60) # caches to disk for 1 minute. """ + app = current.request.application + + # We start by building a files list in which adjacent files internal to + # the application are placed in a list inside the files list. + # + # We will only minify and concat adjacent internal files as there's + # no way to know if changing the order with which the files are apppended + # will break things since the order matters in both CSS and JS and + # internal files may be interleaved with external ones. files = [] - ext_files = [] - has_js = has_css = False + # For the adjacent list we're going to use storage List to both distinguish + # from the regular list and so we can add attributes + internal = List() + internal.has_js = False + internal.has_css = False + done = set() # to remove duplicates for item in self.files: - if isinstance(item, (list, tuple)): - ext_files.append(item) + if not isinstance(item, list): + if item in done: + continue + done.add(item) + if isinstance(item, (list, tuple)) or not item.startswith('/' + app): # also consider items in other web2py applications to be external + if internal: + files.append(internal) + internal = List() + internal.has_js = False + internal.has_css = False + files.append(item) continue if extensions and not item.rpartition('.')[2] in extensions: continue - if item in files: - continue + internal.append(item) if item.endswith('.js'): - has_js = True + internal.has_js = True if item.endswith('.css'): - has_css = True - files.append(item) + internal.has_css = True + if internal: + files.append(internal) - if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)): - # cache for 5 minutes by default - key = hashlib_md5(repr(files)).hexdigest() - cache = self.cache_includes or (current.cache.ram, 60 * 5) - - def call_minify(files=files): - return minify.minify(files, - URL('static', 'temp'), - current.request.folder, - self.optimize_css, - self.optimize_js) - if cache: - cache_model, time_expire = cache - files = cache_model('response.files.minified/' + key, - call_minify, - time_expire) - else: - files = call_minify() - - files.extend(ext_files) - s = [] - for item in files: + # We're done we can now minify + if have_minify: + for i, f in enumerate(files): + if isinstance(f, List) and ((self.optimize_css and f.has_css) or (self.optimize_js and f.has_js)): + # cache for 5 minutes by default + key = hashlib_md5(repr(f)).hexdigest() + cache = self.cache_includes or (current.cache.ram, 60 * 5) + def call_minify(files=f): + return List(minify.minify(files, + URL('static', 'temp'), + current.request.folder, + self.optimize_css, + self.optimize_js)) + if cache: + cache_model, time_expire = cache + files[i] = cache_model('response.files.minified/' + key, + call_minify, + time_expire) + else: + files[i] = call_minify() + + def static_map(s, item): if isinstance(item, str): f = item.lower().split('?')[0] ext = f.rpartition('.')[2] @@ -531,6 +553,13 @@ class Response(Storage): if tmpl: s.append(tmpl % item[1]) + s = [] + for item in files: + if isinstance(item, List): + for f in item: + static_map(s, f) + else: + static_map(s, item) self.write(''.join(s), escape=False) def stream(self, From 135f41041dfe69b35f3fbaadf152918397a5b6ed Mon Sep 17 00:00:00 2001 From: LAdm Date: Thu, 10 Aug 2017 06:49:36 +0200 Subject: [PATCH 019/230] fixes #1724 call func instead of module in url_is_acceptable --- gluon/sanitizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index 5cd2ea4a..b98b7477 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -145,7 +145,7 @@ class XssCleaner(HTMLParser): if url.startswith('#'): return True else: - parsed = urlparse(url) + parsed = urlparse.urlparse(url) return ((parsed[0] in self.allowed_schemes and '.' in parsed[1]) or (parsed[0] in self.allowed_schemes and '@' in parsed[2]) or (parsed[0] == '' and parsed[2].startswith('/'))) From bd7ee209ea38b5e5a2b3826c8810238a4d8d7d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Tue, 15 Aug 2017 15:34:35 +0100 Subject: [PATCH 020/230] Fixes #1737 getproxies is in urllib.request for python 3 --- gluon/widget.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gluon/widget.py b/gluon/widget.py index d56be20a..20b602bb 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -1076,7 +1076,10 @@ def start_schedulers(options): return # Work around OS X problem: http://bugs.python.org/issue9405 - import urllib + if PY2: + import urllib + else: + import urllib.request as urllib urllib.getproxies() for app in apps: From f22e3a762493e07dbd9ccb7557e1791cdef85731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 16 Aug 2017 16:35:46 +0100 Subject: [PATCH 021/230] execfile shim for python 3 compatibility Fixes #1739 --- gluon/shell.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gluon/shell.py b/gluon/shell.py index b6c8f27b..38d12ec4 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -31,10 +31,16 @@ from gluon.globals import Request, Response, Session from gluon.storage import Storage, List from gluon.admin import w2p_unpack from pydal.base import BaseAdapter -from gluon._compat import iteritems, ClassType +from gluon._compat import iteritems, ClassType, PY2 logger = logging.getLogger("web2py") +if not PY2: + def execfile(filename, global_vars=None, local_vars=None): + with open(filename) as f: + code = compile(f.read(), filename, 'exec') + exec(code, global_vars, local_vars) + def enable_autocomplete_and_history(adir, env): try: From 9694c66703f4c08eb16029dc93704c8a6d713f8b Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sat, 19 Aug 2017 08:55:53 +0200 Subject: [PATCH 022/230] Fix view file name in ticket traceback, fix #1740, fix #1676 --- gluon/compileapp.py | 9 ++++++--- gluon/restricted.py | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 116a23f5..ac9cc491 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -676,6 +676,7 @@ def run_view_in(environment): badv = 'invalid view (%s)' % view patterns = response.get('generic_patterns') layer = None + scode = None if patterns: regex = re_compile('|'.join(map(fnmatch.translate, patterns))) short_action = '%(controller)s/%(function)s.%(extension)s' % request @@ -718,12 +719,14 @@ def run_view_in(environment): # if the view is not compiled if not layer: - # Compile the template - ccode = parse_template(view, + # Parse template + scode = parse_template(view, pjoin(folder, 'views'), context=environment) + # Compile template + ccode = compile2(scode, filename) layer = filename - restricted(ccode, environment, layer=layer) + restricted(ccode, environment, layer=layer, scode=scode) # parse_template saves everything in response body return environment['response'].body.getvalue() diff --git a/gluon/restricted.py b/gluon/restricted.py index 5f3ffc10..115470e3 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -205,7 +205,7 @@ def compile2(code, layer): return compile(code, layer, 'exec') -def restricted(ccode, environment=None, layer='Unknown'): +def restricted(ccode, environment=None, layer='Unknown', scode=None): """ Runs code in environment and returns the output. If an exception occurs in code it raises a RestrictedError containing the traceback. Layer is @@ -230,7 +230,9 @@ def restricted(ccode, environment=None, layer='Unknown'): sys.excepthook(etype, evalue, tb) del tb output = "%s %s" % (etype, evalue) - raise RestrictedError(layer, ccode, output, environment) + # Save source code in ticket when available + scode = scode if scode else ccode + raise RestrictedError(layer, scode, output, environment) def snapshot(info=None, context=5, code=None, environment=None): From e880da0d0e85783eb373f2a6d703311a681c5609 Mon Sep 17 00:00:00 2001 From: abastardi Date: Wed, 23 Aug 2017 11:43:40 -0400 Subject: [PATCH 023/230] Fix submit button disable bug When a form includes more than one submit button, after being disabled all buttons are re-enabled with the value of the first button rather than having their original values restored. This change iterates over each button separately to disable and enable. --- applications/welcome/static/js/web2py.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index 1b52245e..f59263f1 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -265,13 +265,17 @@ } }); /* help preventing double form submission for normal form (not LOADed) */ - $(doc).on('submit', 'form', function () { - var submit_button = $(this).find(web2py.formInputClickSelector); - web2py.disableElement(submit_button); + $(doc).on('submit', 'form', function (e) { + var submit_buttons = $(this).find(web2py.formInputClickSelector); + submit_buttons.each(function() { + web2py.disableElement($(this)); + }) /* safeguard in case the form doesn't trigger a refresh, see https://github.com/web2py/web2py/issues/1100 */ setTimeout(function () { - web2py.enableElement(submit_button); + submit_buttons.each(function() { + web2py.enableElement($(this)); + }); }, 5000); }); doc.ajaxSuccess(function (e, xhr) { From c9a71a7055a123bec0591dcf8a8c4bf30f3dadaa Mon Sep 17 00:00:00 2001 From: Yi Liu Date: Tue, 29 Aug 2017 11:26:22 -0700 Subject: [PATCH 024/230] MInor error in EOF instructions start -> stop --- scripts/setup-web2py-nginx-uwsgi-ubuntu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh index 9d7fcee8..e34959d3 100644 --- a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh +++ b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh @@ -222,7 +222,7 @@ echo < Date: Thu, 31 Aug 2017 17:31:43 -0500 Subject: [PATCH 025/230] following pydal 17.08 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index c707d558..f9f0fdfc 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit c707d558995d0318022f3a26935f1100020975fc +Subproject commit f9f0fdfc9a9bedb40a191e5b85b44cb08c672f17 From d06a1f9dc6cff17f92ba4abd4a58f1f02e9a6faa Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 1 Sep 2017 22:40:20 -0500 Subject: [PATCH 026/230] R-2.15.4 --- CHANGELOG | 3 ++- Makefile | 2 +- VERSION | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1df92c35..ada9a5c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ -## 2.15.1-3 +## 2.15.1-4 +- pydal 17.08 - dropped support for python 2.6 - dropped web shell - experimental python 3 support diff --git a/Makefile b/Makefile index 3016e604..852dd016 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.15.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.15.4-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION ### rm -f all junk files make clean ### clean up baisc apps diff --git a/VERSION b/VERSION index 6b247f67..fb2019a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.15.3-stable+timestamp.2017.08.07.07.32.04 +Version 2.15.4-stable+timestamp.2017.09.01.22.38.25 From 087280ec1741cbdbb659458333554d94d54f8c58 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 1 Sep 2017 22:47:54 -0500 Subject: [PATCH 027/230] fixed Makefile _ssd --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 852dd016..78f911a8 100644 --- a/Makefile +++ b/Makefile @@ -98,7 +98,7 @@ win: cp -r applications/examples ../web2py_win/web2py/applications cp applications/__init__.py ../web2py_win/web2py/applications # per https://github.com/web2py/web2py/issues/1716 - mv ../web2py_win/web2py/_ssl.pyd ../web2py_win/web2py/_ssl.pyd.legacy + mv ../web2py_win/web2py/_ssl.pyd ../web2py_win/web2py/_ssl.pyd.legacy | echo 'done' cd ../web2py_win; zip -r web2py_win.zip web2py mv ../web2py_win/web2py_win.zip . run: From 2861dc4215ae97f65b6f7bc78177aba5f5c35cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Tue, 5 Sep 2017 15:46:31 +0100 Subject: [PATCH 028/230] Fixes #1753 --- gluon/compileapp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index ac9cc491..dc0dae79 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -205,7 +205,7 @@ def LOAD(c=None, f='index', args=None, vars=None, other_response = Response() other_request.env.path_info = '/' + \ '/'.join([request.application, c, f] + - map(str, other_request.args)) + [str(a) for a in other_request.args]) other_request.env.query_string = \ vars and URL(vars=vars).split('?')[1] or '' other_request.env.http_web2py_component_location = \ @@ -288,7 +288,7 @@ class LoadFactory(object): other_response = globals.Response() other_request.env.path_info = '/' + \ '/'.join([request.application, c, f] + - map(str, other_request.args)) + [str(a) for a in other_request.args]) other_request.env.query_string = \ vars and html.URL(vars=vars).split('?')[1] or '' other_request.env.http_web2py_component_location = \ @@ -678,7 +678,7 @@ def run_view_in(environment): layer = None scode = None if patterns: - regex = re_compile('|'.join(map(fnmatch.translate, patterns))) + regex = re_compile('|'.join(fnmatch.translate(p) for p in patterns)) short_action = '%(controller)s/%(function)s.%(extension)s' % request allow_generic = regex.search(short_action) else: From 4aefb93ab44e0555d134603da88137cd73e534b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 6 Sep 2017 15:28:39 +0100 Subject: [PATCH 029/230] Fixes #1752 --- gluon/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/_compat.py b/gluon/_compat.py index f7562b12..1a68163d 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -151,7 +151,7 @@ def with_metaclass(meta, *bases): def to_unicode(obj, charset='utf-8', errors='strict'): if obj is None: return None - if not isinstance(obj, bytes): + if not hasattr(obj, 'decode') and not isinstance(obj, bytes): return text_type(obj) return obj.decode(charset, errors) From a6044068cd043a89db6b643a221dca7b1344d840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 6 Sep 2017 15:31:30 +0100 Subject: [PATCH 030/230] Fixes #1751 --- gluon/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/_compat.py b/gluon/_compat.py index 1a68163d..5b7ec948 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -63,7 +63,7 @@ if PY2: return None if isinstance(obj, (bytes, bytearray, buffer)): return bytes(obj) - if isinstance(obj, unicode): + if hasattr(obj, 'encode'): return obj.encode(charset, errors) raise TypeError('Expected bytes') From 83f90165284a25a2852db0c50e7fc81380be0087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 6 Sep 2017 15:33:47 +0100 Subject: [PATCH 031/230] fix in py3 too --- gluon/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/_compat.py b/gluon/_compat.py index 5b7ec948..e5fcbdd8 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -122,7 +122,7 @@ else: return None if isinstance(obj, (bytes, bytearray, memoryview)): return bytes(obj) - if isinstance(obj, str): + if hasattr(obj, 'encode'): return obj.encode(charset, errors) raise TypeError('Expected bytes') From 4b38186b518244ebbd91bf53073d273f32d70ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 6 Sep 2017 15:37:51 +0100 Subject: [PATCH 032/230] simplify condition if you ain't got decode you ain't bytes --- gluon/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/_compat.py b/gluon/_compat.py index e5fcbdd8..a61ddc51 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -151,7 +151,7 @@ def with_metaclass(meta, *bases): def to_unicode(obj, charset='utf-8', errors='strict'): if obj is None: return None - if not hasattr(obj, 'decode') and not isinstance(obj, bytes): + if not hasattr(obj, 'decode'): return text_type(obj) return obj.decode(charset, errors) From 912c22d593375d491e83dcb65ee12a2bec03f02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 6 Sep 2017 16:22:50 +0100 Subject: [PATCH 033/230] Don't use gluon.utf8.Utf8 with py3 there's no need for it and it breaks stuff --- gluon/languages.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gluon/languages.py b/gluon/languages.py index b626457c..aff3db14 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -18,10 +18,11 @@ import pkgutil import logging from cgi import escape from threading import RLock -from gluon.utf8 import Utf8 + from gluon.utils import local_html_escape from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin + from pydal.contrib.portalocker import read_locked, LockedFile from gluon.fileutils import listdir @@ -49,8 +50,10 @@ DEFAULT_CONSTRUCT_PLURAL_FORM = lambda word, plural_id: word if PY2: NUMBERS = (int, long, float) + from gluon.utf8 import Utf8 else: NUMBERS = (int, float) + Utf8 = str # pattern to find T(blah blah blah) expressions PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P'\ From 3ecdd1c11b05b805bc1a5d82bb3e28344a9b8f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 7 Sep 2017 10:18:41 +0100 Subject: [PATCH 034/230] Fix #1757 --- gluon/globals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/globals.py b/gluon/globals.py index f9f5013b..cb52d331 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -331,7 +331,7 @@ class Request(Storage): user_agent = session._user_agent if user_agent: return user_agent - http_user_agent = self.env.http_user_agent + http_user_agent = self.env.http_user_agent or '' user_agent = user_agent_parser.detect(http_user_agent) for key, value in user_agent.items(): if isinstance(value, dict): From 27c9250efb1bfa846870053d3256008e2060f510 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 7 Sep 2017 12:46:36 -0500 Subject: [PATCH 035/230] fixed book link --- applications/examples/views/default/download.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/examples/views/default/download.html b/applications/examples/views/default/download.html index 587a1e8b..362d64bf 100644 --- a/applications/examples/views/default/download.html +++ b/applications/examples/views/default/download.html @@ -48,7 +48,7 @@ - Manual + Manual Change Log From e8cf50326d5be1ce0773aabcff7bc9322676c564 Mon Sep 17 00:00:00 2001 From: Jose de Soto Date: Wed, 13 Sep 2017 11:21:03 +0200 Subject: [PATCH 036/230] When profile is updated self._update_session_user(user) set session.user_groups to None. self.update_groups() needs to be done. --- gluon/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 0cf9b205..8eeab12a 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3777,9 +3777,9 @@ class Auth(AuthAPI): if any(f.compute for f in extra_fields): user = table_user[self.user.id] self._update_session_user(user) + self.update_groups() 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 8eda21ca8685ab5dcde4efcec85009806f3be327 Mon Sep 17 00:00:00 2001 From: abastardi Date: Wed, 13 Sep 2017 11:48:25 -0400 Subject: [PATCH 037/230] Fix bug with compiled views in compiled-only apps Compiled views were not being executed in apps containing only compiled files (i.e., generated via the admin "Pack compiled" functionality), resulting in "Invalid view" errors for all pages. --- gluon/compileapp.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index ac9cc491..1ec547d5 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -709,23 +709,22 @@ def run_view_in(environment): ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled)) layer = compiled break - if not os.path.exists(filename) and allow_generic: - view = 'generic.' + request.extension - filename = pjoin(folder, 'views', view) - if not os.path.exists(filename): - raise HTTP(404, - rewrite.THREAD_LOCAL.routes.error_message % badv, - web2py_error=badv) - # if the view is not compiled if not layer: + if not os.path.exists(filename) and allow_generic: + view = 'generic.' + request.extension + filename = pjoin(folder, 'views', view) + if not os.path.exists(filename): + raise HTTP(404, + rewrite.THREAD_LOCAL.routes.error_message % badv, + web2py_error=badv) # Parse template scode = parse_template(view, pjoin(folder, 'views'), context=environment) # Compile template ccode = compile2(scode, filename) - layer = filename + layer = filename restricted(ccode, environment, layer=layer, scode=scode) # parse_template saves everything in response body return environment['response'].body.getvalue() From a605e43fa6d2e0d15626429411cf074c0609c982 Mon Sep 17 00:00:00 2001 From: abastardi Date: Thu, 14 Sep 2017 18:49:31 -0400 Subject: [PATCH 038/230] Fix grid TSV export bug --- gluon/sqlhtml.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 0653c4c7..555123e9 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -3570,7 +3570,9 @@ class ExportClass(object): if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col): row.append(record._extra[col]) else: - (t, f) = col.split('.') + # The grid code modifies rows.colnames, adding double quotes + # around the table and field names -- so they must be removed here. + (t, f) = [name.strip('"') for name in col.split('.')] field = self.rows.db[t][f] if isinstance(record.get(t, None), (Row, dict)): value = record[t][f] From 5f4c47729b3d2b9be74d1b2c5ff5291c838739b0 Mon Sep 17 00:00:00 2001 From: Jose de Soto <1744837+josedesoto@users.noreply.github.com> Date: Thu, 21 Sep 2017 10:17:17 +0200 Subject: [PATCH 039/230] Removed a tab and replaced by spaces --- gluon/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 8eeab12a..a0373745 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3777,7 +3777,7 @@ class Auth(AuthAPI): if any(f.compute for f in extra_fields): user = table_user[self.user.id] self._update_session_user(user) - self.update_groups() + self.update_groups() else: self.user.update(table_user._filter_fields(form.vars)) session.flash = self.messages.profile_updated From 4a347a4f783d9cd0b5d3550b161208ccecc12f0a Mon Sep 17 00:00:00 2001 From: "tiago.bar" Date: Fri, 22 Sep 2017 13:04:47 -0300 Subject: [PATCH 040/230] Enhances/fixes validators/is_empty() for Python 3 compatibility, fix #1764 --- gluon/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/validators.py b/gluon/validators.py index 1d9dddcf..21de1ce2 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -996,7 +996,7 @@ def is_empty(value, empty_regex=None): value = value.strip() if empty_regex is not None and empty_regex.match(value): value = '' - if value is None or value == '' or value == []: + if value is None or value == '' or value == b'' or value == []: return (_value, True) return (_value, False) From 511245a68c4abcce3c721bbd3d0d48d614f55f3b Mon Sep 17 00:00:00 2001 From: "tiago.bar" Date: Sat, 23 Sep 2017 20:21:14 -0300 Subject: [PATCH 041/230] Unit test for #1764 fix --- gluon/tests/test_validators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index e534fbd8..36bf454e 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -444,6 +444,8 @@ class TestValidators(unittest.TestCase): self.assertEqual(rtn, (None, 'Enter a value')) rtn = IS_NOT_EMPTY()('') self.assertEqual(rtn, ('', 'Enter a value')) + rtn = IS_NOT_EMPTY()(b'') + self.assertEqual(rtn, (b'', 'Enter a value')) rtn = IS_NOT_EMPTY()(' ') self.assertEqual(rtn, (' ', 'Enter a value')) rtn = IS_NOT_EMPTY()(' \n\t') From 8590aae2e8f5d49f7661afe62b8ad151e054b3c5 Mon Sep 17 00:00:00 2001 From: geomapdev Date: Wed, 4 Oct 2017 10:34:48 -0700 Subject: [PATCH 042/230] dal.py code cleanup --- gluon/dal.py | 92 ++++++++++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 54 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index e60cf82d..e4abd6fa 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -44,66 +44,50 @@ def _default_validators(db, field): requires.append(validators.IS_TIME()) elif field_type == 'datetime': requires.append(validators.IS_DATETIME()) - elif db and field_type.startswith('reference') and \ - field_type.find('.') < 0 and \ - field_type[10:] in db.tables: - referenced = db[field_type[10:]] - if hasattr(referenced, '_format') and referenced._format: - requires = validators.IS_IN_DB(db, referenced._id, - referenced._format) - else: - requires = validators.IS_IN_DB(db, referenced._id) - 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 - elif db and field_type.startswith('reference') and \ - field_type.find('.') > 0 and \ - field_type[10:].split('.')[0] in db.tables: - table_name=field_type[10:].split('.')[0] - field_name=field_type[10:].split('.')[1] - referenced = db[table_name] - if hasattr(referenced, '_format') and referenced._format: - requires = validators.IS_IN_DB(db, referenced[field_name], - referenced._format) - else: - requires = validators.IS_IN_DB(db, referenced[field_name]) - if field.unique: - requires._and = validators.IS_NOT_IN_DB(db, field) - if field.tablename == table_name: - 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: - referenced = db[field_type[15:]] - if hasattr(referenced, '_format') and referenced._format: - requires = validators.IS_IN_DB(db, referenced._id, - referenced._format, multiple=True) - else: - requires = validators.IS_IN_DB(db, referenced._id, - multiple=True) + elif db and field_type.startswith('reference'): + if field_type.find('.') < 0 and field_type[10:] in db.tables: + referenced = db[field_type[10:]] + if hasattr(referenced, '_format') and referenced._format: + requires = validators.IS_IN_DB(db, referenced._id,referenced._format) + else: + requires = validators.IS_IN_DB(db, referenced._id) + elif field_type.find('.') > 0 and field_type[10:].split('.')[0] in db.tables: + table_field = field_type[10:].split('.') + table_name=table_field[0] + field_name=table_field[1] + referenced = db[table_name] + if hasattr(referenced, '_format') and referenced._format: + requires = validators.IS_IN_DB(db, referenced[field_name],referenced._format) + else: + requires = validators.IS_IN_DB(db, referenced[field_name]) if field.unique: requires._and = validators.IS_NOT_IN_DB(db, field) if not field.notnull: - requires = validators.IS_EMPTY_OR(requires) + 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:].split('.')[0] in db.tables: - table_name=field_type[15:].split('.')[0] - field_name=field_type[15:].split('.')[1] - referenced = db[table_name] - if hasattr(referenced, '_format') and referenced._format: - requires = validators.IS_IN_DB(db, referenced[field_name], - referenced._format, multiple=True) - else: - requires = validators.IS_IN_DB(db, referenced[field_name], - multiple=True) + elif db and field_type.startswith('list:reference'): + if field_type.find('.') < 0 and field_type[15:] in db.tables: + referenced = db[field_type[15:]] + if hasattr(referenced, '_format') and referenced._format: + requires = validators.IS_IN_DB(db, referenced._id, + referenced._format, multiple=True) + else: + requires = validators.IS_IN_DB(db, referenced._id, + multiple=True) + elif field_type.find('.') > 0 and field_type[15:].split('.')[0] in db.tables: + table_field = field_type[15:].split('.') + table_name=table_field[0] + field_name=table_field[1] + referenced = db[table_name] + if hasattr(referenced, '_format') and referenced._format: + requires = validators.IS_IN_DB(db, referenced[field_name], + referenced._format, multiple=True) + else: + requires = validators.IS_IN_DB(db, referenced[field_name], + multiple=True) if field.unique: requires._and = validators.IS_NOT_IN_DB(db, field) - if not field.notnull: + if field.notnull: requires = validators.IS_EMPTY_OR(requires) return requires # does not get here for reference and list:reference From 603cc7092a2a490c7d9ae521e92e1f15a2cccc68 Mon Sep 17 00:00:00 2001 From: geomapdev Date: Wed, 4 Oct 2017 10:39:42 -0700 Subject: [PATCH 043/230] dal.py more code cleanup --- gluon/dal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index e4abd6fa..b0e095c5 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -63,7 +63,7 @@ def _default_validators(db, field): if field.unique: requires._and = validators.IS_NOT_IN_DB(db, field) if not field.notnull: - return validators.IS_EMPTY_OR(requires) + requires = validators.IS_EMPTY_OR(requires) return requires elif db and field_type.startswith('list:reference'): if field_type.find('.') < 0 and field_type[15:] in db.tables: @@ -87,7 +87,7 @@ def _default_validators(db, field): multiple=True) if field.unique: requires._and = validators.IS_NOT_IN_DB(db, field) - if field.notnull: + if not field.notnull: requires = validators.IS_EMPTY_OR(requires) return requires # does not get here for reference and list:reference From 02c32ebace87e550a3239bf3f4ac76afa7f1c8ef Mon Sep 17 00:00:00 2001 From: geomapdev Date: Mon, 9 Oct 2017 11:22:48 -0700 Subject: [PATCH 044/230] update gluon unittest for dal reference fields --- gluon/tests/test_validators.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index 502b193d..e6cffc54 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -273,8 +273,41 @@ class TestValidators(unittest.TestCase): self.assertEqual(rtn, ('jerry', 'oops')) rtn = IS_IN_DB(db, 'person.id', '%(name)s', auto_add=True)('jerry') self.assertEqual(rtn, (3, None)) + # Test it works with reference table + db.define_table('ref_table', + Field('name'), + Field('person_id', 'reference person') + ) + ret = db.ref_table.validate_and_insert(name='test reference table') + self.assertFalse(list(ret.errors)) + ret = db.ref_table.validate_and_insert(name='test reference table', person_id=george_id) + self.assertFalse(list(ret.errors)) + rtn = IS_IN_DB(db, 'ref_table.person_id', '%(name)s')(george_id) + self.assertEqual(rtn, (george_id, None)) + # Test it works with reference table.field and keyed table + db.define_table('ref_person', + Field('name'), + primarykey=['name']) + ret=db.ref_person.insert(name='george') + rtn = IS_IN_DB(db, 'ref_person.name')('george') + self.assertEqual(rtn, ('george', None)) + db.define_table('ref_table_field', + Field('name'), + Field('person_name', 'reference ref_person.name') + ) + ret = db.ref_table_field.validate_and_insert(name='test reference table.field') + self.assertFalse(list(ret.errors)) + ret = db.ref_table_field.validate_and_insert(name='test reference table.field', person_name='george') + self.assertFalse(list(ret.errors)) + vldtr = IS_IN_DB(db, 'ref_table_field.person_name', '%(name)s') + vldtr.options() + rtn = vldtr('george') + self.assertEqual(rtn, ('george', None)) db.person.drop() db.category.drop() + db.ref_person.drop() + db.ref_table.drop() + db.ref_table_field.drop() def test_IS_NOT_IN_DB(self): from gluon.dal import DAL, Field From 6b103f7e3abb0ffb9c37f49688d2982d2cbf7ae2 Mon Sep 17 00:00:00 2001 From: geomapdev Date: Tue, 10 Oct 2017 08:42:17 -0700 Subject: [PATCH 045/230] updated test_validators IS_IN_DB --- gluon/tests/test_validators.py | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index e6cffc54..d5b93207 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -285,15 +285,16 @@ class TestValidators(unittest.TestCase): rtn = IS_IN_DB(db, 'ref_table.person_id', '%(name)s')(george_id) self.assertEqual(rtn, (george_id, None)) # Test it works with reference table.field and keyed table - db.define_table('ref_person', + db.define_table('person_keyed', Field('name'), primarykey=['name']) - ret=db.ref_person.insert(name='george') - rtn = IS_IN_DB(db, 'ref_person.name')('george') + db.person_keyed.insert(name='george') + db.person_keyed.insert(name='costanza') + rtn = IS_IN_DB(db, 'person_keyed.name')('george') self.assertEqual(rtn, ('george', None)) db.define_table('ref_table_field', Field('name'), - Field('person_name', 'reference ref_person.name') + Field('person_name', 'reference person_keyed.name') ) ret = db.ref_table_field.validate_and_insert(name='test reference table.field') self.assertFalse(list(ret.errors)) @@ -303,11 +304,37 @@ class TestValidators(unittest.TestCase): vldtr.options() rtn = vldtr('george') self.assertEqual(rtn, ('george', None)) + # Test it works with list:reference table + db.define_table('list_ref_table', + Field('name'), + Field('person_list', 'list:reference person')) + ret = db.list_ref_table.validate_and_insert(name='test list:reference table') + self.assertFalse(list(ret.errors)) + ret = db.list_ref_table.validate_and_insert(name='test list:reference table', person_list=[george_id,costanza_id]) + self.assertFalse(list(ret.errors)) + vldtr = IS_IN_DB(db, 'list_ref_table.person_list') + vldtr.options() + rtn = vldtr([george_id,costanza_id]) + self.assertEqual(rtn, ([george_id,costanza_id], None)) + # Test it works with list:reference table.field and keyed table + #db.define_table('list_ref_table_field', + # Field('name'), + # Field('person_list', 'list:reference person_keyed.name')) + #ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field') + #self.assertFalse(list(ret.errors)) + #ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field', person_list=['george','costanza']) + #self.assertFalse(list(ret.errors)) + #vldtr = IS_IN_DB(db, 'list_ref_table_field.person_list') + #vldtr.options() + #rtn = vldtr(['george','costanza']) + #self.assertEqual(rtn, (['george','costanza'], None)) db.person.drop() db.category.drop() - db.ref_person.drop() + db.person_keyed.drop() db.ref_table.drop() db.ref_table_field.drop() + db.list_ref_table.drop() + #db.list_ref_table_field.drop() def test_IS_NOT_IN_DB(self): from gluon.dal import DAL, Field From 2e6f529dfdfcf60e6fde1d31bb411f2d391697fe Mon Sep 17 00:00:00 2001 From: Tim Nyborg Date: Thu, 12 Oct 2017 17:18:06 +0100 Subject: [PATCH 046/230] Fix setting 'ac' in request.vars --- gluon/sqlhtml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 555123e9..c4095e44 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -17,6 +17,7 @@ Holds: import datetime import urllib import re +import copy import os from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native, to_unicode @@ -682,7 +683,7 @@ class AutocompleteWidget(object): else: self.is_reference = False if hasattr(request, 'application'): - urlvars = request.vars + urlvars = copy.copy(request.vars) urlvars[default_var] = 1 self.url = URL(args=request.args, vars=urlvars, user_signature=user_signature, hash_vars=hash_vars) From afa5fac074edf5695f6da9a13cb013433242e8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B8=D0=BC=20=D0=A9?= Date: Sat, 28 Oct 2017 19:26:34 +0300 Subject: [PATCH 047/230] Add reload --- gluon/_compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gluon/_compat.py b/gluon/_compat.py index a61ddc51..d8b7a495 100644 --- a/gluon/_compat.py +++ b/gluon/_compat.py @@ -35,6 +35,7 @@ if PY2: from gluon.contrib import ipaddress BytesIO = StringIO reduce = reduce + reload = reload hashlib_md5 = hashlib.md5 iterkeys = lambda d: d.iterkeys() itervalues = lambda d: d.itervalues() @@ -77,6 +78,7 @@ else: import pickle from io import StringIO, BytesIO import copyreg + from importlib import reload from functools import reduce from html.parser import HTMLParser from http import cookies as Cookie From dd14d1c8c9d10fb86d204462f0dc1b279c225101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B8=D0=BC=20=D0=A9?= Date: Sat, 28 Oct 2017 19:28:51 +0300 Subject: [PATCH 048/230] Add reload --- gluon/compileapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index a59e96d1..1e0d3ef0 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -18,7 +18,7 @@ import fnmatch import os import copy import random -from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long +from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long, reload from gluon.storage import Storage, List from gluon.template import parse_template from gluon.restricted import restricted, compile2 From 2e572aee9a80160c35184fdf1dfe72bb3f61ad44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B8=D0=BC=20=D0=A9?= Date: Sat, 28 Oct 2017 19:29:55 +0300 Subject: [PATCH 049/230] Update custom_import.py --- gluon/custom_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/custom_import.py b/gluon/custom_import.py index d9eab5b2..e12dea42 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -8,7 +8,7 @@ Support for smart import syntax for web2py applications ------------------------------------------------------- """ -from gluon._compat import builtin, unicodeT, PY2, to_native +from gluon._compat import builtin, unicodeT, PY2, to_native, reload import os import sys import threading From c7d415fefdb97b8d056b542cf9684afc4c5e290d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B8=D0=BC=20=D0=A9?= Date: Sat, 28 Oct 2017 19:32:48 +0300 Subject: [PATCH 050/230] compatible unicode --- gluon/languages.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gluon/languages.py b/gluon/languages.py index aff3db14..0d8bd4a2 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -110,15 +110,17 @@ def markmin(s): def upper_fun(s): - return unicode(s, 'utf-8').upper().encode('utf-8') + return to_unicode(s).upper() def title_fun(s): - return unicode(s, 'utf-8').title().encode('utf-8') + return to_unicode(s).title() def cap_fun(s): - return unicode(s, 'utf-8').capitalize().encode('utf-8') + return to_unicode(s).capitalize() + + ttab_in = maketrans("\\%{}", '\x1c\x1d\x1e\x1f') ttab_out = maketrans('\x1c\x1d\x1e\x1f', "\\%{}") From d5eb8d8f17c68ebd3c66c3451cc9a998a2d35595 Mon Sep 17 00:00:00 2001 From: Vitaly Karpenko Date: Thu, 2 Nov 2017 13:52:25 +0300 Subject: [PATCH 051/230] gluon.http expects status code as integer --- gluon/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/main.py b/gluon/main.py index e5d73e21..43b2189f 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -231,7 +231,7 @@ class LazyWSGI(object): to call third party WSGI applications """ - self.response.status = str(status).split(' ', 1)[0] + self.response.status = int(str(status).split(' ', 1)[0]) self.response.headers = dict(headers) return lambda *args, **kargs: \ self.response.write(escape=False, *args, **kargs) From 2aa8fd22a2c6c6e68c652238654103748827eb5e Mon Sep 17 00:00:00 2001 From: Eric Waldman Date: Fri, 3 Nov 2017 15:17:18 -0400 Subject: [PATCH 052/230] Set entry animation to 'fadeIn' and provide an easy switch for 'slideDown' --- applications/welcome/static/js/web2py.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index f59263f1..7131727d 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -13,6 +13,8 @@ } var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; + var animateIn = 'fadeIn'; + // var animateIn = 'slideDown'; String.prototype.reverse = function () { return this.split('').reverse().join(''); @@ -180,7 +182,7 @@ }, /* manage errors in forms */ manage_errors: function (target) { - $('div.error', target).hide().slideDown('slow'); + $('div.error', target).hide()[animateIn]('slow'); }, after_ajax: function (xhr) { /* called whenever an ajax request completes */ @@ -616,7 +618,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ')[animateIn](); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -630,7 +632,7 @@ for (var k = 0; k < triggers[id].length; k++) { var dep = $('#' + triggers[id][k], target); var tr = $('#' + triggers[id][k] + '__row', target); - if (t.is(dep.attr('data-show-if'))) tr.slideDown(); + if (t.is(dep.attr('data-show-if'))) tr[animateIn](); else tr.hide(); } }; @@ -823,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ From 5db3b214371c1a871f9cce361c61a266f0b4b260 Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Mon, 6 Nov 2017 01:40:03 -0800 Subject: [PATCH 053/230] Add update_languages script --- scripts/update_languages.py | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 scripts/update_languages.py diff --git a/scripts/update_languages.py b/scripts/update_languages.py new file mode 100644 index 00000000..3e94cb36 --- /dev/null +++ b/scripts/update_languages.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Written by Vinyl Darkscratch, www.queengoob.org + +# TODO: add comments + +import os +import ast +import sys +import inspect + +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parentdir = os.path.dirname(currentdir) +sys.path.insert(0, parentdir) + +from gluon.cfs import getcfs +from gluon.utf8 import Utf8 +from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin +from gluon.languages import findT + +# This script can be run with no arguments (which sets the language folder to the current working directory, and default language to English), one argument (which sets the default language), or two arguments (language folder path and default language). +# When run, it will update the default language, as well as strip all of the strings found in the non-default languages but not in the default language, and add the strings found in the default language to the non-default languages it is not, making sure translators don't do additional work that will never be used. + +def read_dict_aux(filename): + lang_text = open(filename, 'r').read().replace(b'\r\n', b'\n') + try: + return safe_eval(to_native(lang_text)) or {} + except Exception: + e = sys.exc_info()[1] + status = 'Syntax error in %s (%s)' % (filename, e) + return {'__corrupted__': status} + +def read_dict(filename): + return getcfs('lang:' + filename, filename, lambda: read_dict_aux(filename)) + +def safe_eval(text): + if text.strip(): + try: + return ast.literal_eval(text) + except ImportError: + return eval(text, {}, {}) + return None + +def sort_function(x, y): + return cmp(unicode(x, 'utf-8').lower(), unicode(y, 'utf-8').lower()) + +def write_file(file, contents): + file.write('# -*- coding: utf-8 -*-\n{\n') + for key in sorted(contents, sort_function): + file.write('%s: %s,\n' % (repr(Utf8(key)), + repr(Utf8(contents[key])))) + file.write('}\n') + file.close() + +def update_languages(cwd, default_lang): + defaultfp = os.path.join(cwd, '%s.py' %default_lang) + findT(cwd, default_lang) + default = read_dict(defaultfp) + + for x in os.listdir(cwd): + if x == default_lang or x.startswith("plural-"): continue + + i18n = read_dict(os.path.join(cwd, x)) + if i18n: + nd = default + for k_d1 in i18n: + if k_d1 in default: + nd[k_d1] = i18n[k_d1] + write_file(open(x, 'w'), nd) + print x + +if __name__ == "__main__": + cwd = os.getcwd() + default_lang = 'en' + + if len(sys.argv) > 2: + cwd = sys.argv[1] + default_lang = sys.argv[2] + elif len(sys.argv) > 1: + default_lang = sys.argv[1] + + update_languages(cwd, default_lang) From 3ab91b9fe9b8d760b4e355beccbaa58095d58dbf Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Mon, 6 Nov 2017 01:42:25 -0800 Subject: [PATCH 054/230] Add check_lang_progress script --- scripts/check_lang_progress.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts/check_lang_progress.py diff --git a/scripts/check_lang_progress.py b/scripts/check_lang_progress.py new file mode 100644 index 00000000..6c53bbd7 --- /dev/null +++ b/scripts/check_lang_progress.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Written by Vinyl Darkscratch, www.queengoob.org + +import sys +import os + +# TODO: add comments + +# This script can be run with no arguments (which sets the language folder to the current working directory, and default language to English), one argument (which sets the default language), or two arguments (language folder path and default language). +# When run, this script will compare the original language's strings to their assigned values and determine if they match, just like web2py's interface. While some phrases may be the exact same between both languages, this should provide a good idea of how far along translations are. + +def check_lang_progress(cwd, default_lang): + for x in os.listdir(cwd): + if x == default_lang or x.startswith("plural-"): continue + + data = eval(open(os.path.join(cwd, x)).read()) + + total = 0 + translated = 0 + + for key in data: + total += 1 + if key.replace('@markmin\x01', '') != data[key]: translated += 1 + + print "Translations for %s (%s): %d/%d Translated (%d Untranslated)" %(data['!langname!'], data['!langcode!'], translated, total, total-translated) + +if __name__ == "__main__": + cwd = os.getcwd() + default_lang = 'en' + + if len(sys.argv) > 2: + cwd = sys.argv[1] + default_lang = sys.argv[2] + elif len(sys.argv) > 1: + default_lang = sys.argv[1] + + check_lang_progress(cwd, default_lang) \ No newline at end of file From 228d3c41b6ea713192b883009089e78aec3cb592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Tue, 7 Nov 2017 23:34:35 +0000 Subject: [PATCH 055/230] Fixes #1800 --- gluon/authapi.py | 2 +- gluon/tests/test_authapi.py | 3 +++ gluon/tools.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index faef8c1e..ddae855a 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -988,7 +988,7 @@ class AuthAPI(object): requires = [requires] requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) if requires: - requires[0].min_length = 0 + requires[0] = CRYPT(key=settings.hmac_key, min_length=0) old_password = kwargs.get('old_password', '') new_password = kwargs.get('new_password', '') diff --git a/gluon/tests/test_authapi.py b/gluon/tests/test_authapi.py index cd00afb6..cc92778e 100644 --- a/gluon/tests/test_authapi.py +++ b/gluon/tests/test_authapi.py @@ -138,6 +138,9 @@ class TestAuthAPI(unittest.TestCase): 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']) + # Test the default 4 min_length is enforced on change password + result = self.auth.change_password(old_password='1234', new_password='123', new_password2='123') + self.assertTrue('new_password' in result['errors']) def test_verify_key(self): self.auth.settings.registration_requires_verification = True diff --git a/gluon/tools.py b/gluon/tools.py index 0cf9b205..80b905ac 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3693,7 +3693,7 @@ class Auth(AuthAPI): requires = [requires] requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) if requires: - requires[0].min_length = 0 + requires[0] = CRYPT(key=self.settings.hmac_key, min_length=0) form = SQLFORM.factory( Field('old_password', 'password', requires=requires, label=self.messages.old_password), From 2f5eb409b632df5fe722739c59fc69fc5f5f54f7 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 8 Nov 2017 00:40:47 -0600 Subject: [PATCH 056/230] show computed fields in readonly mode in appadmin --- gluon/contrib/populate.py | 2 ++ gluon/packages/dal | 2 +- gluon/sqlhtml.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/gluon/contrib/populate.py b/gluon/contrib/populate.py index 6977e8d5..26ef8aed 100644 --- a/gluon/contrib/populate.py +++ b/gluon/contrib/populate.py @@ -137,6 +137,8 @@ def populate_generator(table, default=True, compute=False, contents={}): continue elif field.type == 'upload': continue + elif field.compute is not None: + continue elif default and not field.default in (None, ''): record[fieldname] = field.default elif compute and field.compute: diff --git a/gluon/packages/dal b/gluon/packages/dal index f9f0fdfc..35dd4fc6 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit f9f0fdfc9a9bedb40a191e5b85b44cb08c672f17 +Subproject commit 35dd4fc6f8fb8187e7c08217eebf3074e0d27fbc diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index c4095e44..afb2279e 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1244,8 +1244,8 @@ class SQLFORM(FORM): # will only use writable or readable fields, unless forced to ignore if fields is None: fields = [f.name for f in table if - (ignore_rw or f.writable or f.readable) and - (readonly or not f.compute)] + (ignore_rw or f.writable or f.readable) and + not (f.compute and not record)] self.fields = fields # make sure we have an id @@ -1328,7 +1328,7 @@ class SQLFORM(FORM): label = LABEL(label, label and sep, _for=field_id, _id=field_id + SQLFORM.ID_LABEL_SUFFIX) - cond = readonly or \ + cond = readonly or field.compute or \ (not ignore_rw and not field.writable and field.readable) if cond: From 925f92884399183f512cf8d5a9f46b202ec86a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 8 Nov 2017 11:53:29 +0000 Subject: [PATCH 057/230] Copy all CRYPT attributes thanks @abastardi --- gluon/authapi.py | 3 ++- gluon/tools.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index ddae855a..db2ae13f 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -988,7 +988,8 @@ class AuthAPI(object): requires = [requires] requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) if requires: - requires[0] = CRYPT(key=settings.hmac_key, min_length=0) + requires[0] = CRYPT(**requires[0].__dict__) # Copy the existing CRYPT attributes + requires[0].min_length = 0 # But do not enforce minimum length for the old password old_password = kwargs.get('old_password', '') new_password = kwargs.get('new_password', '') diff --git a/gluon/tools.py b/gluon/tools.py index 80b905ac..b063b6ea 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3693,7 +3693,8 @@ class Auth(AuthAPI): requires = [requires] requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) if requires: - requires[0] = CRYPT(key=self.settings.hmac_key, min_length=0) + requires[0] = CRYPT(**requires[0].__dict__) # Copy the existing CRYPT attributes + requires[0].min_length = 0 # But do not enforce minimum length for the old password form = SQLFORM.factory( Field('old_password', 'password', requires=requires, label=self.messages.old_password), From 4bfa9c1686a83983239bbf8ecdd13c5b50f877ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 8 Nov 2017 22:21:51 +0000 Subject: [PATCH 058/230] Fix a problem with Expressions in SQLTABLE possibly fixes #1735 and fixes web2py/pydal#487 --- gluon/sqlhtml.py | 2 +- gluon/tests/test_sqlhtml.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index afb2279e..594339dd 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -3328,7 +3328,7 @@ class SQLTABLE(TABLE): return REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD fieldmap = dict(zip(sqlrows.colnames, sqlrows.fields)) - tablemap = dict(((f.tablename, f.table) for f in fieldmap.values())) + tablemap = dict(((f.tablename, f.table) if isinstance(f, Field) else (f._table._tablename, f._table) for f in fieldmap.values())) for table in tablemap.values(): pref = table._tablename + '.' fieldmap.update(((pref+f.name, f) for f in table._virtual_fields)) diff --git a/gluon/tests/test_sqlhtml.py b/gluon/tests/test_sqlhtml.py index d0b519a0..1ba9b051 100644 --- a/gluon/tests/test_sqlhtml.py +++ b/gluon/tests/test_sqlhtml.py @@ -355,10 +355,10 @@ class TestSQLTABLE(unittest.TestCase): self.db.commit() -# def test_SQLTABLE(self): -# rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL) -# sqltable = SQLTABLE(rows) -# self.assertEqual(sqltable.xml(), '
auth_user.idauth_user.first_nameauth_user.last_nameauth_user.emailauth_user.usernameauth_user.passwordauth_user.registration_keyauth_user.reset_password_keyauth_user.registration_id
1BartSimpsonuser1@test.comuser1password_123NoneNone
') + def test_SQLTABLE(self): + rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL) + sqltable = SQLTABLE(rows) + self.assertEqual(sqltable.xml(), b'
auth_user.idauth_user.first_nameauth_user.last_nameauth_user.emailauth_user.usernameauth_user.passwordauth_user.registration_keyauth_user.reset_password_keyauth_user.registration_id
1BartSimpsonuser1@test.comuser1password_123NoneNone
') # class TestExportClass(unittest.TestCase): From 14e58276cf4cba3abfc5066b536b869b1c5914a7 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 10 Nov 2017 18:41:41 -0600 Subject: [PATCH 059/230] form handling of f.options, f.regex, f.listable, f.rearchable --- applications/admin/static/js/web2py.js | 58 ++++++++++++++++------- applications/examples/static/js/web2py.js | 58 ++++++++++++++++------- gluon/dal.py | 23 +++++---- gluon/packages/dal | 2 +- gluon/sqlhtml.py | 8 ++-- 5 files changed, 103 insertions(+), 46 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 313d00bc..7131727d 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -12,6 +12,10 @@ $.error('web2py.js has already been loaded!'); } + var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; + var animateIn = 'fadeIn'; + // var animateIn = 'slideDown'; + String.prototype.reverse = function () { return this.split('').reverse().join(''); }; @@ -178,7 +182,7 @@ }, /* manage errors in forms */ manage_errors: function (target) { - $('div.error', target).hide().slideDown('slow'); + $('div.error', target).hide()[animateIn]('slow'); }, after_ajax: function (xhr) { /* called whenever an ajax request completes */ @@ -263,13 +267,17 @@ } }); /* help preventing double form submission for normal form (not LOADed) */ - $(doc).on('submit', 'form', function () { - var submit_button = $(this).find(web2py.formInputClickSelector); - web2py.disableElement(submit_button); + $(doc).on('submit', 'form', function (e) { + var submit_buttons = $(this).find(web2py.formInputClickSelector); + submit_buttons.each(function() { + web2py.disableElement($(this)); + }) /* safeguard in case the form doesn't trigger a refresh, see https://github.com/web2py/web2py/issues/1100 */ setTimeout(function () { - web2py.enableElement(submit_button); + submit_buttons.each(function() { + web2py.enableElement($(this)); + }); }, 5000); }); doc.ajaxSuccess(function (e, xhr) { @@ -320,7 +328,15 @@ form.submit(function (e) { web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.hide_flash(); - web2py.ajax_page('post', url, form.serialize(), target, form); + + var formData; + if (FORMDATA_IS_SUPPORTED) { + formData = new FormData(form[0]); // Allows file uploads. + } else { + formData = form.serialize(); // Fallback for older browsers. + } + web2py.ajax_page('post', url, formData, target, form); + e.preventDefault(); }); form.on('click', web2py.formInputClickSelector, function (e) { @@ -339,11 +355,18 @@ if (web2py.isUndefined(element)) element = $(document); /* if target is not there, fill it with something that there isn't in the page*/ if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; + + /* processData and contentType must be set to false when passing a FormData + object to jQuery.ajax. */ + var isFormData = Object.prototype.toString.call(data) === '[object FormData]'; + var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8'; if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ $.ajax({ 'type': method, 'url': action, 'data': data, + 'processData': !isFormData, + 'contentType': contentType, 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); @@ -595,7 +618,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ')[animateIn](); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -609,7 +632,7 @@ for (var k = 0; k < triggers[id].length; k++) { var dep = $('#' + triggers[id][k], target); var tr = $('#' + triggers[id][k] + '__row', target); - if (t.is(dep.attr('data-show-if'))) tr.slideDown(); + if (t.is(dep.attr('data-show-if'))) tr[animateIn](); else tr.hide(); } }; @@ -699,8 +722,9 @@ }); }, /* Disables form elements: + - Does not disable elements with 'data-w2p_disable' attribute - Caches element value in 'w2p_enable_with' data store - - Replaces element text with value of 'data-disable-with' attribute + - Replaces element text with value of 'data-w2p_disable_with' attribute - Sets disabled property to true */ disableFormElements: function (form) { @@ -712,13 +736,15 @@ if (!web2py.isUndefined(disable)) { return false; } - if (web2py.isUndefined(disable_with)) { - element.data('w2p_disable_with', element[method]()); + if (!element.is(':file')) { // Altering file input values is not allowed. + if (web2py.isUndefined(disable_with)) { + element.data('w2p_disable_with', element[method]()); + } + if (web2py.isUndefined(element.data('w2p_enable_with'))) { + element.data('w2p_enable_with', element[method]()); + } + element[method](element.data('w2p_disable_with')); } - if (web2py.isUndefined(element.data('w2p_enable_with'))) { - element.data('w2p_enable_with', element[method]()); - } - element[method](element.data('w2p_disable_with')); element.prop('disabled', true); }); }, @@ -799,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index 313d00bc..7131727d 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -12,6 +12,10 @@ $.error('web2py.js has already been loaded!'); } + var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; + var animateIn = 'fadeIn'; + // var animateIn = 'slideDown'; + String.prototype.reverse = function () { return this.split('').reverse().join(''); }; @@ -178,7 +182,7 @@ }, /* manage errors in forms */ manage_errors: function (target) { - $('div.error', target).hide().slideDown('slow'); + $('div.error', target).hide()[animateIn]('slow'); }, after_ajax: function (xhr) { /* called whenever an ajax request completes */ @@ -263,13 +267,17 @@ } }); /* help preventing double form submission for normal form (not LOADed) */ - $(doc).on('submit', 'form', function () { - var submit_button = $(this).find(web2py.formInputClickSelector); - web2py.disableElement(submit_button); + $(doc).on('submit', 'form', function (e) { + var submit_buttons = $(this).find(web2py.formInputClickSelector); + submit_buttons.each(function() { + web2py.disableElement($(this)); + }) /* safeguard in case the form doesn't trigger a refresh, see https://github.com/web2py/web2py/issues/1100 */ setTimeout(function () { - web2py.enableElement(submit_button); + submit_buttons.each(function() { + web2py.enableElement($(this)); + }); }, 5000); }); doc.ajaxSuccess(function (e, xhr) { @@ -320,7 +328,15 @@ form.submit(function (e) { web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.hide_flash(); - web2py.ajax_page('post', url, form.serialize(), target, form); + + var formData; + if (FORMDATA_IS_SUPPORTED) { + formData = new FormData(form[0]); // Allows file uploads. + } else { + formData = form.serialize(); // Fallback for older browsers. + } + web2py.ajax_page('post', url, formData, target, form); + e.preventDefault(); }); form.on('click', web2py.formInputClickSelector, function (e) { @@ -339,11 +355,18 @@ if (web2py.isUndefined(element)) element = $(document); /* if target is not there, fill it with something that there isn't in the page*/ if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; + + /* processData and contentType must be set to false when passing a FormData + object to jQuery.ajax. */ + var isFormData = Object.prototype.toString.call(data) === '[object FormData]'; + var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8'; if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ $.ajax({ 'type': method, 'url': action, 'data': data, + 'processData': !isFormData, + 'contentType': contentType, 'beforeSend': function (xhr, settings) { xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-element', target); @@ -595,7 +618,7 @@ var flash = $('.w2p_flash'); web2py.hide_flash(); flash.html(message).addClass(status); - if (flash.html()) flash.append(' × ').slideDown(); + if (flash.html()) flash.append(' × ')[animateIn](); }, hide_flash: function () { $('.w2p_flash').fadeOut(0).html(''); @@ -609,7 +632,7 @@ for (var k = 0; k < triggers[id].length; k++) { var dep = $('#' + triggers[id][k], target); var tr = $('#' + triggers[id][k] + '__row', target); - if (t.is(dep.attr('data-show-if'))) tr.slideDown(); + if (t.is(dep.attr('data-show-if'))) tr[animateIn](); else tr.hide(); } }; @@ -699,8 +722,9 @@ }); }, /* Disables form elements: + - Does not disable elements with 'data-w2p_disable' attribute - Caches element value in 'w2p_enable_with' data store - - Replaces element text with value of 'data-disable-with' attribute + - Replaces element text with value of 'data-w2p_disable_with' attribute - Sets disabled property to true */ disableFormElements: function (form) { @@ -712,13 +736,15 @@ if (!web2py.isUndefined(disable)) { return false; } - if (web2py.isUndefined(disable_with)) { - element.data('w2p_disable_with', element[method]()); + if (!element.is(':file')) { // Altering file input values is not allowed. + if (web2py.isUndefined(disable_with)) { + element.data('w2p_disable_with', element[method]()); + } + if (web2py.isUndefined(element.data('w2p_enable_with'))) { + element.data('w2p_enable_with', element[method]()); + } + element[method](element.data('w2p_disable_with')); } - if (web2py.isUndefined(element.data('w2p_enable_with'))) { - element.data('w2p_enable_with', element[method]()); - } - element[method](element.data('w2p_disable_with')); element.prop('disabled', true); }); }, @@ -799,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ diff --git a/gluon/dal.py b/gluon/dal.py index 215fd4e1..0ebfd6a5 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -50,6 +50,10 @@ def _default_validators(db, field): requires.append(validators.IS_TIME()) elif field_type == 'datetime': requires.append(validators.IS_DATETIME()) + elif field.options is not None: + requires = IS_IN_SET(field.options, multiple=field_type.startswith('list:')) + elif field.regex and not requires: + requires = IS_REGEX(regex) elif db and field_type.startswith('reference') and \ field_type.find('.') < 0 and \ field_type[10:] in db.tables: @@ -76,16 +80,17 @@ def _default_validators(db, field): requires._and = validators.IS_NOT_IN_DB(db, field) if not field.notnull: requires = validators.IS_EMPTY_OR(requires) - return requires + return requires # does not get here for reference and list:reference - if field.unique: - requires.insert(0, validators.IS_NOT_IN_DB(db, field)) - excluded_fields = ['string', 'upload', 'text', 'password', 'boolean'] - if (field.notnull or field.unique) and field_type not in excluded_fields: - requires.insert(0, validators.IS_NOT_EMPTY()) - elif not field.notnull and not field.unique and requires: - requires[0] = \ - validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) + if isinstance(requires, list): + if field.unique: + requires.insert(0, validators.IS_NOT_IN_DB(db, field)) + excluded_fields = ['string', 'upload', 'text', 'password', 'boolean'] + if (field.notnull or field.unique) and field_type not in excluded_fields: + requires.insert(0, validators.IS_NOT_EMPTY()) + elif not field.notnull and not field.unique and requires: + null = null='' if field.type in ('string', 'text', 'password') else None + requires[0] = validators.IS_EMPTY_OR(requires[0], null=null) return requires DAL.serializers = {'json': custom_json, 'xml': xml} diff --git a/gluon/packages/dal b/gluon/packages/dal index 35dd4fc6..d8631f68 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 35dd4fc6f8fb8187e7c08217eebf3074e0d27fbc +Subproject commit d8631f683cd374b274f7fe5caa0abf79b7fdc54c diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 594339dd..93e431fb 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2348,12 +2348,12 @@ class SQLFORM(FORM): for k, f in iteritems(table): if isinstance(f, Field.Virtual): f.tablename = table._tablename - columns = [f for f in fields if f.tablename in tablenames] + columns = [f for f in fields if f.tablename in tablenames and f.listable] else: fields = [] columns = [] filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs) - filter2 = lambda f: isinstance(f, Field) and f.readable + filter2 = lambda f: isinstance(f, Field) and f.readable and f.listable for table in tables: fields += filter(filter1, table) columns += filter(filter2, table) @@ -2571,8 +2571,8 @@ class SQLFORM(FORM): try: # the query should be constructed using searchable # fields but not virtual fields - sfields = reduce(lambda a, b: a + b, - [[f for f in t if f.readable and not isinstance(f, Field.Virtual)] for t in tables]) + is_searchable = lambda f: f.readable and not isinstance(f, Field.Virtual) and f.searchable + sfields = reduce(lambda a, b: a + b, [filter(is_searchable, t) for t in tables]) # use custom_query using searchable if callable(searchable): dbset = dbset(searchable(sfields, keywords)) From 166e268308d62a4a982c4048968e13700c4b70bc Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 10 Nov 2017 18:50:52 -0600 Subject: [PATCH 060/230] improved handling of regex and options field attributes --- gluon/dal.py | 10 +++++----- gluon/packages/dal | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index 0ebfd6a5..c794f612 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -32,7 +32,11 @@ def _default_validators(db, field): field_type, field_length = field.type, field.length requires = [] - if field_type in (('string', 'text', 'password')): + if field.options is not None and field.requires: + requires = validators.IS_IN_SET(field.options, multiple=field_type.startswith('list:')) + elif field.regex and not field.requires: + requires.append(validators.IS_REGEX(regex)) + elif field_type in (('string', 'text', 'password')): requires.append(validators.IS_LENGTH(field_length)) elif field_type == 'json': requires.append(validators.IS_EMPTY_OR(validators.IS_JSON())) @@ -50,10 +54,6 @@ def _default_validators(db, field): requires.append(validators.IS_TIME()) elif field_type == 'datetime': requires.append(validators.IS_DATETIME()) - elif field.options is not None: - requires = IS_IN_SET(field.options, multiple=field_type.startswith('list:')) - elif field.regex and not requires: - requires = IS_REGEX(regex) elif db and field_type.startswith('reference') and \ field_type.find('.') < 0 and \ field_type[10:] in db.tables: diff --git a/gluon/packages/dal b/gluon/packages/dal index d8631f68..c4ec44ca 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit d8631f683cd374b274f7fe5caa0abf79b7fdc54c +Subproject commit c4ec44cac84c148121af340b27799b1c2f4e76e7 From 30b3d84f240e94ed5da15f19b47a3a230246db85 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 10 Nov 2017 22:31:35 -0600 Subject: [PATCH 061/230] fixed options when type is list --- gluon/dal.py | 2 +- gluon/packages/dal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index c794f612..2506e1c5 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -32,7 +32,7 @@ def _default_validators(db, field): field_type, field_length = field.type, field.length requires = [] - if field.options is not None and field.requires: + if isinstance(field.options, list) and field.requires: requires = validators.IS_IN_SET(field.options, multiple=field_type.startswith('list:')) elif field.regex and not field.requires: requires.append(validators.IS_REGEX(regex)) diff --git a/gluon/packages/dal b/gluon/packages/dal index c4ec44ca..b34b8be0 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit c4ec44cac84c148121af340b27799b1c2f4e76e7 +Subproject commit b34b8be0b54332ff5e87e09ed80ed20b1820b04c From 12253ab757538312b1adcc37998fbc2910090394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 13 Nov 2017 15:31:05 +0000 Subject: [PATCH 062/230] Fix #1715 --- gluon/globals.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gluon/globals.py b/gluon/globals.py index cb52d331..b4344f0f 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -220,7 +220,12 @@ class Request(Storage): if is_json: try: - json_vars = json_parser.load(body) + # In Python 3 versions prior to 3.6 load doesn't accept bytes and + # bytearray, so we read the body convert to native and use loads + # instead of load. + # This line can be simplified to json_vars = json_parser.load(body) + # if and when we drop support for python versions under 3.6 + json_vars = json_parser.loads(to_native(body.read())) except: # incoherent request bodies can still be parsed "ad-hoc" json_vars = {} From e44a12eaf8147cefbbea2a37c6257303ed4673bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 13 Nov 2017 22:40:37 +0000 Subject: [PATCH 063/230] Update supported python versions --- .../private/content/en/default/what/whyweb2py.markmin | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/examples/private/content/en/default/what/whyweb2py.markmin b/applications/examples/private/content/en/default/what/whyweb2py.markmin index e4337a59..358a81c0 100644 --- a/applications/examples/private/content/en/default/what/whyweb2py.markmin +++ b/applications/examples/private/content/en/default/what/whyweb2py.markmin @@ -3,9 +3,9 @@ - **Created by a community of professionals** and University professors in Computer Science and Software Engineering. - **Always backward compatible.** We have not broken backward compatibility since version 1.0 in 2007, and we pledge not to break it in the future. - **Easy to run.** It requires no installation and no configuration. -- **Runs on** Windows, Mac, Unix/Linux, Google App Engine, Amazon EC2, and almost any web hosting via Python 2.5/2.6/2.7/pypy, or Java with Jython. -- **Runs with** Apache, Lighttpd, Cherokee and almost any other web server via CGI, FastCGI, WSGI, mod_proxy, and/or mod_python. It can embed third party WSGI apps and middleware. -- **Talks to** SQLite, PostgreSQL, MySQL, MSSQL, FireBird, Oracle, IBM DB2, Informix, Ingres, and Google App Engine. +- **Runs on** Windows, Mac, Unix/Linux, Google App Engine, Amazon EC2, and almost any web hosting via Python 2.7/3.5/3.6/pypy. +- **Runs with** Apache, Nginx, Lighttpd, Cherokee and almost any other web server via CGI, FastCGI, WSGI, mod_proxy, and/or mod_python. It can embed third party WSGI apps and middleware. +- **Talks to** SQLite, PostgreSQL, MySQL, MSSQL, FireBird, Sybase, Oracle, IBM DB2, Informix, Ingres, MongoDB, and Google App Engine. - **Secure** [[It prevents the most common types of vulnerabilities http://web2py.com/examples/default/security]] including Cross Site Scripting, Injection Flaws, and Malicious File Execution. - **Enforces good Software Engineering practices** (Model-View-Controller design, Server-side form validation, postbacks) that make the code more readable, scalable, and maintainable. - **Speaks multiple protocols** HTML/XML, RSS/ATOM, RTF, PDF, JSON, AJAX, XML-RPC, CSV, REST, WIKI, Flash/AMF, and Linked Data (RDF). From 7c1fb6643e5bf85af93345f82d9472ab159eb104 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 13 Nov 2017 21:12:06 -0600 Subject: [PATCH 064/230] fllowing pydal v17.11 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index b34b8be0..d4327595 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit b34b8be0b54332ff5e87e09ed80ed20b1820b04c +Subproject commit d43275953d2cafa51813546c1cce163f154cdfc1 From 65b3e6dda9c89512e18d37be082c9c74af83122d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 13 Nov 2017 23:29:34 -0600 Subject: [PATCH 065/230] support for bootstrap4 --- CHANGELOG | 4 + Makefile | 14 ++- VERSION | 2 +- applications/admin/static/js/jquery.js | 8 +- applications/welcome/controllers/default.py | 12 -- applications/welcome/models/db.py | 72 +++++++----- applications/welcome/models/menu.py | 70 +++--------- applications/welcome/private/appconfig.ini | 12 +- .../welcome/static/css/bootstrap.min.css | 17 +-- ...y-bootstrap3.css => web2py-bootstrap4.css} | 4 +- .../welcome/static/images/background.jpg | Bin 740881 -> 0 bytes .../welcome/static/js/bootstrap.min.js | 10 +- .../welcome/static/js/respond-1.4.2.min.js | 6 - ...2py-bootstrap3.js => web2py-bootstrap4.js} | 0 applications/welcome/views/default/index.html | 40 +++---- applications/welcome/views/layout.html | 104 +++++++++--------- gluon/sqlhtml.py | 102 +++++++++++++++++ 17 files changed, 265 insertions(+), 212 deletions(-) rename applications/welcome/static/css/{web2py-bootstrap3.css => web2py-bootstrap4.css} (98%) delete mode 100644 applications/welcome/static/images/background.jpg delete mode 100644 applications/welcome/static/js/respond-1.4.2.min.js rename applications/welcome/static/js/{web2py-bootstrap3.js => web2py-bootstrap4.js} (100%) diff --git a/CHANGELOG b/CHANGELOG index ada9a5c6..f9371be2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +## 2.15.5 +- pydal 17.11 +- many bug fixes + ## 2.15.1-4 - pydal 17.08 - dropped support for python 2.6 diff --git a/Makefile b/Makefile index 78f911a8..888f563e 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,7 @@ update: wget -O gluon/contrib/feedparser.py http://feedparser.googlecode.com/svn/trunk/feedparser/feedparser.py wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py echo "remember that pymysql was tweaked" -src: - ### Use semantic versioning - echo 'Version 2.15.4-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION - ### rm -f all junk files - make clean +rmfiles: ### clean up baisc apps rm -f routes.py rm -rf applications/*/sessions/* @@ -46,6 +42,12 @@ src: rm -rf applications/admin/uploads/* rm -rf applications/welcome/uploads/* rm -rf applications/examples/uploads/* +src: + ### Use semantic versioning + echo 'Version 2.15.5-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + ### rm -f all junk files + #make clean + # make rmfiles ### make welcome layout and appadmin the default cp applications/welcome/views/appadmin.html applications/admin/views cp applications/welcome/views/appadmin.html applications/examples/views @@ -54,7 +56,7 @@ src: ### build web2py_src.zip echo '' > NEWINSTALL mv web2py_src.zip web2py_src_old.zip | echo 'no old' - cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/fabfile.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py + cd ..; zip -r --exclude=**.git** web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/fabfile.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py mdp: make src diff --git a/VERSION b/VERSION index fb2019a7..1cc562ba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.15.4-stable+timestamp.2017.09.01.22.38.25 +Version 2.15.5-stable+timestamp.2017.11.13.21.46.11 diff --git a/applications/admin/static/js/jquery.js b/applications/admin/static/js/jquery.js index 4024b662..644d35e2 100644 --- a/applications/admin/static/js/jquery.js +++ b/applications/admin/static/js/jquery.js @@ -1,4 +1,4 @@ -/*! jQuery v2.2.4 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; -}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("
') -# -# # class TestRecaptcha2(unittest.TestCase): # def test_Recaptcha2(self): # from html import FORM @@ -248,7 +240,6 @@ class TestAuthJWT(unittest.TestCase): self.user_data['password'])[0])) self.jwtauth = AuthJWT(self.auth, secret_key='secret', verify_expiration=True) - def test_jwt_token_manager(self): import gluon.serializers self.request.vars.update(self.user_data) @@ -260,7 +251,6 @@ class TestAuthJWT(unittest.TestCase): self.token = self.jwtauth.jwt_token_manager() self.assertIsNotNone(self.token) - def test_allows_jwt(self): import gluon.serializers self.request.vars.update(self.user_data) @@ -270,11 +260,13 @@ class TestAuthJWT(unittest.TestCase): del self.request.vars['password'] self.token = self.jwtauth.jwt_token_manager() self.request.vars._token = gluon.serializers.json_parser.loads(self.token)['token'] + @self.jwtauth.allows_jwt() def optional_auth(): self.assertEqual(self.user_data['username'], self.auth.user.username) optional_auth() + @unittest.skipIf(IS_IMAP, "TODO: Imap raises 'Connection refused'") # class TestAuth(unittest.TestCase): # @@ -495,8 +487,6 @@ class TestAuthJWT(unittest.TestCase): # # impersonate_form = auth.impersonate(user_id=omer_id) # # self.assertTrue(auth.is_impersonating()) # # self.assertEqual(impersonate_form, 'test') - - class TestAuth(unittest.TestCase): def myassertRaisesRegex(self, *args, **kwargs): @@ -904,7 +894,7 @@ class TestAuth(unittest.TestCase): self.assertEqual(count_log_event_test_after, count_log_event_test_before) def test_add_membership(self): - user = self.db(self.db.auth_user.username == 'bart').select().first() # bypass login_bare() + user = self.db(self.db.auth_user.username == 'bart').select().first() # bypass login_bare() user_id = user.id role_name = 'test_add_membership_group' group_id = self.auth.add_group(role_name) @@ -1174,6 +1164,7 @@ class TestToolsFunctions(unittest.TestCase): pjoin = os.path.join + def have_symlinks(): return os.name == 'posix' @@ -1181,18 +1172,20 @@ def have_symlinks(): class Test_Expose__in_base(unittest.TestCase): def test_in_base(self): - are_under = [ # (sub, base) + are_under = [ + # (sub, base) ('/foo/bar', '/foo'), ('/foo', '/foo'), ('/foo', '/'), ('/', '/'), ] for sub, base in are_under: - self.assertTrue( Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), - '%s is not under %s' % (sub, base) ) + self.assertTrue(Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), + '%s is not under %s' % (sub, base)) def test_not_in_base(self): - are_not_under = [ # (sub, base) + are_not_under = [ + # (sub, base) ('/foobar', '/foo'), ('/foo', '/foo/bar'), ('/bar', '/foo'), @@ -1200,8 +1193,8 @@ class Test_Expose__in_base(unittest.TestCase): ('/', '/x'), ] for sub, base in are_not_under: - self.assertFalse( Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), - '%s should not be under %s' % (sub, base) ) + self.assertFalse(Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), + '%s should not be under %s' % (sub, base)) class TestExpose(unittest.TestCase): @@ -1237,7 +1230,7 @@ class TestExpose(unittest.TestCase): shutil.rmtree(self.base_dir) def make_dirs(self): - """setup direcotry strucutre""" + """setup directory structure""" for d in (['inside'], ['inside', 'dir1'], ['inside', 'dir2'], @@ -1257,7 +1250,7 @@ class TestExpose(unittest.TestCase): f.write('README content') def make_symlinks(self): - """setup extenstion for posix systems""" + """setup extension for posix systems""" # inside links os.symlink( pjoin(self.base_dir, 'inside', 'dir1'), diff --git a/gluon/tools.py b/gluon/tools.py index 753408aa..d82e9fd4 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -52,7 +52,7 @@ import gluon.serializers as serializers Table = DAL.Table Field = DAL.Field -__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', +__all__ = ['Mail', 'Auth', 'Recaptcha2', 'Crud', 'Service', 'Wiki', 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] # mind there are two loggers here (logger and crud.settings.logger)! @@ -826,149 +826,6 @@ class Mail(object): return True -class Recaptcha(DIV): - - """ - Examples: - Use as:: - - form = FORM(Recaptcha(public_key='...', private_key='...')) - - or:: - - form = SQLFORM(...) - form.append(Recaptcha(public_key='...', private_key='...')) - - """ - - API_SSL_SERVER = 'https://www.google.com/recaptcha/api' - API_SERVER = 'http://www.google.com/recaptcha/api' - VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify' - - def __init__(self, - request=None, - public_key='', - private_key='', - use_ssl=False, - error=None, - error_message='invalid', - label='Verify:', - 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 - self.public_key = public_key - self.private_key = private_key - self.use_ssl = use_ssl - self.error = error - self.errors = Storage() - self.error_message = error_message - self.components = [] - self.attributes = {} - self.label = label - self.options = options - self.comment = comment - self.ajax = ajax - - def _validate(self): - - # for local testing: - - recaptcha_challenge_field = \ - self.request_vars.recaptcha_challenge_field - recaptcha_response_field = \ - self.request_vars.recaptcha_response_field - private_key = self.private_key - remoteip = self.remote_addr - if not (recaptcha_response_field and recaptcha_challenge_field - and len(recaptcha_response_field) - and len(recaptcha_challenge_field)): - self.errors['captcha'] = self.error_message - return False - params = urlencode({ - 'privatekey': private_key, - 'remoteip': remoteip, - 'challenge': recaptcha_challenge_field, - 'response': recaptcha_response_field, - }) - request = urllib2.Request( - url=self.VERIFY_SERVER, - data=params, - headers={'Content-type': 'application/x-www-form-urlencoded', - 'User-agent': 'reCAPTCHA Python'}) - httpresp = urllib2.urlopen(request) - return_values = httpresp.read().splitlines() - httpresp.close() - return_code = return_values[0] - if return_code == 'true': - del self.request_vars.recaptcha_challenge_field - del self.request_vars.recaptcha_response_field - self.request_vars.captcha = '' - return True - else: - # In case we get an error code, store it so we can get an error message - # from the /api/challenge URL as described in the reCAPTCHA api docs. - self.error = return_values[1] - self.errors['captcha'] = self.error_message - return False - - def xml(self): - public_key = self.public_key - use_ssl = self.use_ssl - error_param = '' - if self.error: - error_param = '&error=%s' % self.error - if use_ssl: - server = self.API_SSL_SERVER - else: - server = self.API_SERVER - if not self.ajax: - captcha = DIV( - SCRIPT("var RecaptchaOptions = {%s};" % self.options), - SCRIPT(_type="text/javascript", - _src="%s/challenge?k=%s%s" % (server, public_key, error_param)), - TAG.noscript( - IFRAME( - _src="%s/noscript?k=%s%s" % ( - server, public_key, error_param), - _height="300", _width="500", _frameborder="0"), BR(), - INPUT( - _type='hidden', _name='recaptcha_response_field', - _value='manual_challenge')), _id='recaptcha') - - else: # use Google's ajax interface, needed for LOADed components - - url_recaptcha_js = "%s/js/recaptcha_ajax.js" % server - RecaptchaOptions = "var RecaptchaOptions = {%s}" % self.options - script = """%(options)s; - jQuery.getScript('%(url)s',function() { - Recaptcha.create('%(public_key)s', - 'recaptcha',jQuery.extend(RecaptchaOptions,{'callback':Recaptcha.focus_response_field})) - }) """ % ({'options': RecaptchaOptions, 'url': url_recaptcha_js, 'public_key': public_key}) - captcha = DIV( - SCRIPT( - script, - _type="text/javascript", - ), - TAG.noscript( - IFRAME( - _src="%s/noscript?k=%s%s" % ( - server, public_key, error_param), - _height="300", _width="500", _frameborder="0"), BR(), - INPUT( - _type='hidden', _name='recaptcha_response_field', - _value='manual_challenge')), _id='recaptcha') - - if not self.errors.captcha: - return XML(captcha).xml() - else: - captcha.append(DIV(self.errors['captcha'], _class='error')) - return XML(captcha).xml() - - class Recaptcha2(DIV): """ Experimental: From cec14c741aa2c2f04bfe488b11c2300c76906f7f Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 2 Apr 2018 23:53:05 -0500 Subject: [PATCH 137/230] fixed no-email edit in profile --- fabfile.py | 2 +- gluon/authapi.py | 4 +++- gluon/packages/dal | 2 +- gluon/tools.py | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/fabfile.py b/fabfile.py index e4dbbe60..2a1f6018 100644 --- a/fabfile.py +++ b/fabfile.py @@ -122,7 +122,7 @@ def deploy(appname=None, all=False): if all=='all' or not backup: local('zip -r _update.zip * -x *~ -x .* -x \#* -x *.bak -x *.bak2') - else: + else: local('zip -r _update.zip */*.py */*/*.py views/*.html views/*/*.html static/*') put('_update.zip','/tmp/_update.zip') diff --git a/gluon/authapi.py b/gluon/authapi.py index db2ae13f..50e30db9 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -443,7 +443,9 @@ class AuthAPI(object): # 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) + if self.table_user()[user_id]: + self.table_event().insert( + description=str(description % vars), origin=origin, user_id=user_id) def id_group(self, role): """ diff --git a/gluon/packages/dal b/gluon/packages/dal index d4d7e48c..6e9b8794 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit d4d7e48c1f82a0b2a27b7475f18ee9f92e3cb0fe +Subproject commit 6e9b87943bb5162b51d085904845558a1b690812 diff --git a/gluon/tools.py b/gluon/tools.py index 753408aa..656917e3 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3762,6 +3762,7 @@ class Auth(AuthAPI): client_side=self.settings.client_side) passfield = self.settings.password_field table_user[passfield].writable = False + table_user['email'].writable = False request = current.request session = current.session if next is DEFAULT: @@ -3772,6 +3773,7 @@ class Auth(AuthAPI): onaccept = self.settings.profile_onaccept if log is DEFAULT: log = self.messages['profile_log'] + form = SQLFORM( table_user, self.user.id, From 8d4dc1b373f5c8750ee0de6f542dec22eb222e0e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 3 Apr 2018 00:22:19 -0500 Subject: [PATCH 138/230] fixed logging issue when invalid user_id --- gluon/authapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/authapi.py b/gluon/authapi.py index 50e30db9..887ae724 100644 --- a/gluon/authapi.py +++ b/gluon/authapi.py @@ -443,7 +443,7 @@ class AuthAPI(object): # log messages should not be translated if type(description).__name__ == 'lazyT': description = description.m - if self.table_user()[user_id]: + if not user_id or self.table_user()[user_id]: self.table_event().insert( description=str(description % vars), origin=origin, user_id=user_id) From 5de8cb4da57b389a94f5243f15f66ae5e22477ce Mon Sep 17 00:00:00 2001 From: mdipierro Date: Tue, 3 Apr 2018 11:24:32 -0500 Subject: [PATCH 139/230] fixed syntax for attachemnts, thanks Paolo Pastori --- gluon/globals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 767a9b0b..d80d53be 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -605,7 +605,7 @@ class Response(Storage): else: attname = filename headers["Content-Disposition"] = \ - 'attachment;filename="%s"' % attname + 'attachment; filename="%s"' % attname if not request: request = current.request @@ -686,7 +686,7 @@ class Response(Storage): download_filename = download_filename.encode('utf-8') download_filename = urllib_quote(download_filename) headers['Content-Disposition'] = \ - 'attachment; filename="%s"' % download_filename.replace('"', '\"') + 'attachment; filename="%s"' % download_filename.replace('"', '\\"') return self.stream(stream, chunk_size=chunk_size, request=request) def json(self, data, default=None, indent=None): From 9b4b41223934f6eadd3ebfc891e78a2f1eb47c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Thu, 12 Apr 2018 12:25:30 -0400 Subject: [PATCH 140/230] Fixed Python 3.x LDAP bug on user fistname and lastname part --- gluon/contrib/login_methods/ldap_auth.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index 4c07b757..d12ed83d 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -398,17 +398,25 @@ def ldap_auth(server='ldap', if manage_user: logger.info('[%s] Manage user data' % str(username)) try: + user_firstname = result[user_firstname_attrib][0] if user_firstname_part is not None: - store_user_firstname = result[user_firstname_attrib][0].split(' ', 1)[user_firstname_part] + store_user_firstname = user_firstname.split( + b' ' if isinstance(user_firstname, bytes) else ' ', + 1 + )[user_firstname_part] else: - store_user_firstname = result[user_firstname_attrib][0] + store_user_firstname = user_firstname except KeyError as e: store_user_firstname = None try: + user_lastname = result[user_lastname_attrib][0] if user_lastname_part is not None: - store_user_lastname = result[user_lastname_attrib][0].split(' ', 1)[user_lastname_part] + store_user_lastname = user_lastname.split( + b' ' if isinstance(user_lastname, bytes) else ' ', + 1 + )[user_lastname_part] else: - store_user_lastname = result[user_lastname_attrib][0] + store_user_lastname = user_lastname except KeyError as e: store_user_lastname = None try: From de0e63469c69c64a63e42dcc769fcda44a7e67da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Fri, 13 Apr 2018 14:35:52 -0400 Subject: [PATCH 141/230] Add support for Python logger logging levels in LDAP auth --- gluon/contrib/login_methods/ldap_auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index 4c07b757..2bec1c6f 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -170,7 +170,9 @@ def ldap_auth(server='ldap', is "error" and can be set to error, warning, info, debug. """ logger = logging.getLogger('web2py.auth.ldap_auth') - if logging_level == 'error': + if isinstance(logging_level, int): + logger.setLevel(logging_level) + elif logging_level == 'error': logger.setLevel(logging.ERROR) elif logging_level == 'warning': logger.setLevel(logging.WARNING) From 90288a013446ce15434fee196c932709213b4c14 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Sat, 14 Apr 2018 11:42:31 +0300 Subject: [PATCH 142/230] fix sessions in cookies for python3 --- gluon/globals.py | 2 +- gluon/utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/globals.py b/gluon/globals.py index 767a9b0b..95e310c6 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -1167,7 +1167,7 @@ class Session(Storage): compression_level=compression_level) rcookies = response.cookies rcookies.pop(name, None) - rcookies[name] = value + rcookies[name] = value.decode('utf8') rcookies[name]['path'] = '/' expires = response.session_cookie_expires if isinstance(expires, datetime.datetime): diff --git a/gluon/utils.py b/gluon/utils.py index 3b26a51b..3b93ea18 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -207,6 +207,8 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): def secure_loads(data, encryption_key, hash_key=None, compression_level=None): + if not isinstance(data, bytes): + data = bytes(data, 'utf8') components = data.count(b':') if components == 1: return secure_loads_deprecated(data, encryption_key, hash_key, compression_level) From 086bfb58512162437d11e6693a4060c181bc04ff Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Sat, 14 Apr 2018 14:33:37 +0300 Subject: [PATCH 143/230] fix redis sessions and redis for python3 --- gluon/contrib/redis_cache.py | 4 ++-- gluon/contrib/redis_session.py | 12 +++++++----- gluon/contrib/redis_utils.py | 4 ++-- gluon/globals.py | 6 ++++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/gluon/contrib/redis_cache.py b/gluon/contrib/redis_cache.py index 05aa53fc..110360eb 100644 --- a/gluon/contrib/redis_cache.py +++ b/gluon/contrib/redis_cache.py @@ -10,7 +10,7 @@ except: import time import re import logging -import thread +from threading import Lock import random from gluon import current from gluon.cache import CacheAbstract @@ -19,7 +19,7 @@ from gluon.contrib.redis_utils import register_release_lock, RConnectionError logger = logging.getLogger("web2py.cache.redis") -locker = thread.allocate_lock() +locker = Lock() def RedisCache(redis_conn=None, debug=False, with_lock=False, fail_gracefully=False, db=None): diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py index 1273e6de..b26b6b21 100644 --- a/gluon/contrib/redis_session.py +++ b/gluon/contrib/redis_session.py @@ -8,7 +8,7 @@ Redis-backed sessions """ import logging -import thread +from threading import Lock from gluon import current from gluon.storage import Storage from gluon.contrib.redis_utils import acquire_lock, release_lock @@ -16,7 +16,7 @@ from gluon.contrib.redis_utils import register_release_lock logger = logging.getLogger("web2py.session.redis") -locker = thread.allocate_lock() +locker = Lock() def RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None): @@ -43,7 +43,7 @@ def RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None): try: instance_name = 'redis_instance_' + current.request.application if not hasattr(RedisSession, instance_name): - setattr(RedisSession, instance_name, + setattr(RedisSession, instance_name, RedisClient(redis_conn, session_expiry=session_expiry, with_lock=with_lock)) return getattr(RedisSession, instance_name) finally: @@ -185,8 +185,10 @@ class MockQuery(object): if rtn: if self.unique_key: # make sure the id and unique_key are correct - if rtn['unique_key'] == self.unique_key: - rtn['update_record'] = self.update # update record support + if not isinstance(self.unique_key, bytes): + self.unique_key = bytes(self.unique_key, 'utf8') + if rtn[b'unique_key'] == self.unique_key: + rtn[b'update_record'] = self.update # update record support else: rtn = None return [Storage(rtn)] if rtn else [] diff --git a/gluon/contrib/redis_utils.py b/gluon/contrib/redis_utils.py index 217ca952..8dc7b2d3 100644 --- a/gluon/contrib/redis_utils.py +++ b/gluon/contrib/redis_utils.py @@ -11,7 +11,7 @@ to ensure compatibility with another - similar - library """ import logging -import thread +from threading import Lock import time from gluon import current @@ -26,7 +26,7 @@ except ImportError: raise RuntimeError('Needs redis library to work') -locker = thread.allocate_lock() +locker = Lock() def RConn(*args, **vars): diff --git a/gluon/globals.py b/gluon/globals.py index 95e310c6..edf0e504 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -967,7 +967,7 @@ class Session(Storage): if row: # rows[0].update_record(locked=True) # Unpickle the data - session_data = pickle.loads(row.session_data) + session_data = pickle.loads(row[b'session_data']) self.update(session_data) response.session_new = False else: @@ -1049,7 +1049,9 @@ class Session(Storage): if record_id.isdigit() and long(record_id) > 0: new_unique_key = web2py_uuid() row = table(record_id) - if row and row.unique_key == unique_key: + if not isinstance(unique_key, bytes): + unique_key = bytes(unique_key, 'utf8') + if row and row[b'unique_key'] == unique_key: table._db(table.id == record_id).update(unique_key=new_unique_key) else: record_id = None From 0900a3ddb94c10cfb509726277798c2a71a25dbb Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Sat, 14 Apr 2018 14:58:53 +0300 Subject: [PATCH 144/230] discovered and use _compat to_bytes and to_native functions --- gluon/contrib/redis_session.py | 5 ++--- gluon/globals.py | 22 ++++++++++------------ gluon/utils.py | 3 +-- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py index b26b6b21..e5771719 100644 --- a/gluon/contrib/redis_session.py +++ b/gluon/contrib/redis_session.py @@ -13,6 +13,7 @@ from gluon import current from gluon.storage import Storage from gluon.contrib.redis_utils import acquire_lock, release_lock from gluon.contrib.redis_utils import register_release_lock +from gluon._compat import to_bytes logger = logging.getLogger("web2py.session.redis") @@ -185,9 +186,7 @@ class MockQuery(object): if rtn: if self.unique_key: # make sure the id and unique_key are correct - if not isinstance(self.unique_key, bytes): - self.unique_key = bytes(self.unique_key, 'utf8') - if rtn[b'unique_key'] == self.unique_key: + if rtn[b'unique_key'] == to_bytes(self.unique_key): rtn[b'update_record'] = self.update # update record support else: rtn = None diff --git a/gluon/globals.py b/gluon/globals.py index edf0e504..c54d4635 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -14,7 +14,7 @@ Contains the classes for the global used variables: """ from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \ - unicodeT, long, hashlib_md5, urllib_quote + to_bytes, unicodeT, long, hashlib_md5, urllib_quote from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -225,7 +225,7 @@ class Request(Storage): # instead of load. # This line can be simplified to json_vars = json_parser.load(body) # if and when we drop support for python versions under 3.6 - json_vars = json_parser.loads(to_native(body.read())) + json_vars = json_parser.loads(to_native(body.read())) except: # incoherent request bodies can still be parsed "ad-hoc" json_vars = {} @@ -344,8 +344,8 @@ class Request(Storage): user_agent = Storage(user_agent) user_agent.is_mobile = 'Mobile' in http_user_agent user_agent.is_tablet = 'Tablet' in http_user_agent - session._user_agent = user_agent - + session._user_agent = user_agent + return user_agent def requires_https(self): @@ -485,12 +485,12 @@ class Response(Storage): # # We will only minify and concat adjacent internal files as there's # no way to know if changing the order with which the files are apppended - # will break things since the order matters in both CSS and JS and + # will break things since the order matters in both CSS and JS and # internal files may be interleaved with external ones. files = [] # For the adjacent list we're going to use storage List to both distinguish # from the regular list and so we can add attributes - internal = List() + internal = List() internal.has_js = False internal.has_css = False done = set() # to remove duplicates @@ -513,7 +513,7 @@ class Response(Storage): if item.endswith('.js'): internal.has_js = True if item.endswith('.css'): - internal.has_css = True + internal.has_css = True if internal: files.append(internal) @@ -537,7 +537,7 @@ class Response(Storage): time_expire) else: files[i] = call_minify() - + def static_map(s, item): if isinstance(item, str): f = item.lower().split('?')[0] @@ -1049,9 +1049,7 @@ class Session(Storage): if record_id.isdigit() and long(record_id) > 0: new_unique_key = web2py_uuid() row = table(record_id) - if not isinstance(unique_key, bytes): - unique_key = bytes(unique_key, 'utf8') - if row and row[b'unique_key'] == unique_key: + if row and row[b'unique_key'] == to_bytes(unique_key): table._db(table.id == record_id).update(unique_key=new_unique_key) else: record_id = None @@ -1169,7 +1167,7 @@ class Session(Storage): compression_level=compression_level) rcookies = response.cookies rcookies.pop(name, None) - rcookies[name] = value.decode('utf8') + rcookies[name] = to_native(value) rcookies[name]['path'] = '/' expires = response.session_cookie_expires if isinstance(expires, datetime.datetime): diff --git a/gluon/utils.py b/gluon/utils.py index 3b93ea18..ea738047 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -207,8 +207,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): def secure_loads(data, encryption_key, hash_key=None, compression_level=None): - if not isinstance(data, bytes): - data = bytes(data, 'utf8') + data = to_bytes(data) components = data.count(b':') if components == 1: return secure_loads_deprecated(data, encryption_key, hash_key, compression_level) From b26e28fda13c508ed0b98b943b0e53ef04bf150b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Bo=C3=9F?= Date: Tue, 17 Apr 2018 20:31:38 +0200 Subject: [PATCH 145/230] fix editing mistakes --- applications/examples/views/default/download.html | 2 +- applications/examples/views/default/examples.html | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/examples/views/default/download.html b/applications/examples/views/default/download.html index 362d64bf..c1ecdec5 100644 --- a/applications/examples/views/default/download.html +++ b/applications/examples/views/default/download.html @@ -81,7 +81,7 @@

License

Web2py code is released under LGPLv3 License. This license does not extend to third party libraries distributed with web2py (which can be MIT, BSD or Apache type licenses) nor does it extend to applications built with web2py (under the terms of the LGPL.

-

Applications built with web2py can be released under any license the author wishes as long they do not contain web2py code. They can link unmodified web2py libraries and they can be distributed with official web2py binaries. In particular web2py applications can be distributed in closed source. The admin interface provides a button to byte-code compile.

+

Applications built with web2py can be released under any license the author wishes as long as they do not contain web2py code. They can link unmodified web2py libraries and they can be distributed with official web2py binaries. In particular web2py applications can be distributed in closed source. The admin interface provides a button to byte-code compile.

It is fine to distribute web2py (source or compiled) with your applications as long as you make it clear in the license where your application ends and web2py starts.

web2py is copyrighted by Massimo Di Pierro. The web2py trademark is owned by Massimo Di Pierro.

read more diff --git a/applications/examples/views/default/examples.html b/applications/examples/views/default/examples.html index f048b557..905d22e9 100644 --- a/applications/examples/views/default/examples.html +++ b/applications/examples/views/default/examples.html @@ -50,10 +50,10 @@ def hello3(): and view: simple_examples/hello3.html {{=CODE(open(os.path.join(request.folder,'views/simple_examples/hello3.html'),'r').read(),language='html',link=URL('global','vars'),_class='boxCode')}} -

If you return a dictionary, the variables defined in the dictionery are visible to the view (template). +

If you return a dictionary, the variables defined in the dictionary are visible to the view (template).
Try it here: hello3

-

Actions can also be be rendered in other formsts like JSON, hello3.json, and XML, hello3.xml

+

Actions can also be be rendered in other forms like JSON, hello3.json, and XML, hello3.xml

Example {{=c}}{{c+=1}}

In controller: simple_examples.py {{=CODE(""" @@ -69,7 +69,7 @@ def hello4(): def hello5(): return HTML(BODY(H1(T('Hello World'),_style="color: red;"))).xml() # .xml to serialize """.strip(),language='web2py',link=URL('global','vars'),_class='boxCode')}} -

You can also generate HTML using helper objects HTML, BODY, H1, etc. Each of these tags is a class and the views know how to render the corresponding objects. The method .xml() serializes them and produce html/xml code for the page. +

You can also generate HTML using helper objects HTML, BODY, H1, etc. Each of these tags is a class and the views know how to render the corresponding objects. The method .xml() serializes them and produces html/xml code for the page. Each tag, DIV for example, takes three types of arguments: