From a921751e8ef88447bce7ca9522faa307a434a217 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 5 Apr 2015 18:17:27 -0500 Subject: [PATCH 001/115] stripe form bs3 compatible, disabled cache.ram max utilization --- applications/examples/views/appadmin.html | 12 ++-- gluon/cache.py | 2 +- gluon/contrib/stripe.py | 70 +++++++++++------------ gluon/packages/dal | 2 +- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/applications/examples/views/appadmin.html b/applications/examples/views/appadmin.html index d8341969..6e12639d 100644 --- a/applications/examples/views/appadmin.html +++ b/applications/examples/views/appadmin.html @@ -18,7 +18,7 @@
- +
{{for db in sorted(databases):}} {{for table in databases[db].tables:}} {{qry='%s.%s.id>0'%(db,table)}} @@ -40,7 +40,7 @@ {{=A("%s.%s" % (db,table),_href=URL('select',args=[db],vars=dict(query=qry)))}} {{pass}} @@ -61,7 +61,7 @@ {{pass}} {{if table:}} - {{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}

+ {{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn btn-default")}}

{{=T("Rows in Table")}}


{{else:}}

{{=T("Rows selected")}}


@@ -72,8 +72,8 @@ {{=T('"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN')}}



{{=T("%s selected", nrows)}}

- {{if start>0:}}{{=A(T('previous %s rows') % step,_href=URL('select',args=request.args[0],vars=dict(start=start-step)),_class="btn")}}{{pass}} - {{if stop0:}}{{=A(T('previous %s rows') % step,_href=URL('select',args=request.args[0],vars=dict(start=start-step)),_class="btn btn-default")}}{{pass}} + {{if stop {{linkto = lambda f, t, r: URL('update', args=[request.args[0], r, f]) if f else "#"}} @@ -82,7 +82,7 @@ {{pass}}

{{=T("Import/Export")}}


- {{=T("export as csv file")}} + {{=T("export as csv file")}} {{=formcsv or ''}} {{elif request.function=='insert':}} diff --git a/gluon/cache.py b/gluon/cache.py index a7d8aeba..ad32cb33 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -99,7 +99,7 @@ class CacheAbstract(object): """ cache_stats_name = 'web2py_cache_statistics' - max_ram_utilization = 90 # percent + max_ram_utilization = None # percent def __init__(self, request=None): """Initializes the object diff --git a/gluon/contrib/stripe.py b/gluon/contrib/stripe.py index a0f6ce6d..ec0bfa05 100644 --- a/gluon/contrib/stripe.py +++ b/gluon/contrib/stripe.py @@ -4,7 +4,24 @@ from hashlib import sha1 class Stripe: """ - Usage: + Use in WEB2PY (guaranteed PCI compliant) + +def pay(): + from gluon.contrib.stripe import StripeForm + form = StripeForm( + pk=STRIPE_PUBLISHABLE_KEY, + sk=STRIPE_SECRET_KEY, + amount=150, # $1.5 (amount is in cents) + description="Nothing").process() + if form.accepted: + payment_id = form.response['id'] + redirect(URL('thank_you')) + elif form.errors: + redirect(URL('pay_error')) + return dict(form=form) + +Low level API: + key='' d = Stripe(key).charge( amount=100, # 1 dollar!!!! @@ -22,22 +39,6 @@ class Stripe: {u'fee': 0, u'description': u'test charge', u'created': 1321242072, u'refunded': False, u'livemode': False, u'object': u'charge', u'currency': u'usd', u'amount': 100, u'paid': True, u'id': u'ch_sdjasgfga83asf', u'card': {u'exp_month': 5, u'country': u'US', u'object': u'card', u'last4': u'4242', u'exp_year': 2012, u'type': u'Visa'}} if paid is True than transaction was processed - Use in WEB2PY (guaranteed PCI compliant) - -def pay(): - from gluon.contrib.stripe import StripeForm - form = StripeForm( - pk=STRIPE_PUBLISHABLE_KEY, - sk=STRIPE_SECRET_KEY, - amount=150, # $1.5 (amount is in cents) - description="Nothing").process() - if form.accepted: - payment_id = form.response['id'] - redirect(URL('thank_you')) - elif form.errors: - redirect(URL('pay_error')) - return dict(form=form) - """ URL_CHARGE = 'https://%s:@api.stripe.com/v1/charges' @@ -188,37 +189,36 @@ jQuery(function(){

Payment Amount: {{=currency_symbol}} {{="%.2f" % (0.01*amount)}}

-
- -
+
+ +
+ placeholder="4242424242424242" class="form-control"/>
-
- -
+
+ +
+ placeholder="XXX" class="form-control"/> What is this?
-
- -
- +
+ +
+ / - +
- -
-
+
+
diff --git a/gluon/packages/dal b/gluon/packages/dal index b08cb1f7..5eef2e79 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit b08cb1f779d1ef7973bf460344772778a344a077 +Subproject commit 5eef2e7943543820b9fa9a375d60c51ac1cfdaed From 15ff8669cbedb8c80143f26965a212c2c86577a8 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 6 Apr 2015 16:24:49 -0500 Subject: [PATCH 002/115] fixed dal tracking, thanks Niphlod --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 5eef2e79..b08cb1f7 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 5eef2e7943543820b9fa9a375d60c51ac1cfdaed +Subproject commit b08cb1f779d1ef7973bf460344772778a344a077 From cefa30841b12bece802b22a5013311b7bd4264ba Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 8 Apr 2015 21:47:04 +0200 Subject: [PATCH 003/115] file is already open at this point... fixes #895 --- gluon/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/cache.py b/gluon/cache.py index ad32cb33..f94937d8 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -353,7 +353,7 @@ class CacheOnDisk(CacheAbstract): raise KeyError self.wait_portalock(val_file) - value = pickle.load(recfile.open(key, 'rb', path=self.folder)) + value = pickle.load(val_file) val_file.close() return value From 9c92bd10505dba508ee732b9247c4f4e35ded14c Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 12 Apr 2015 21:04:05 +0200 Subject: [PATCH 004/115] extend sqlcustomform to support widget/represent (possible fix to web2py/pydal#127) --- gluon/sqlhtml.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index f4e270b2..cd57bbd3 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1205,6 +1205,9 @@ class SQLFORM(FORM): elif field.type == 'boolean': inp = self.widgets.boolean.widget( field, default, _disabled=True) + elif isinstance(field.type, SQLCustomType) and callable(field.type.represent): + # SQLCustomType has a represent, use it + inp = field.type.represent(default, record) else: inp = field.formatter(default) if getattr(field, 'show_if', None): @@ -1246,6 +1249,9 @@ class SQLFORM(FORM): dspval = '' elif field.type == 'blob': continue + elif isinstance(field.type, SQLCustomType) and callable(field.type.widget): + # SQLCustomType has a widget, use it + inp = field.type.widget(field, default) else: field_type = widget_class.match(str(field.type)).group() field_type = field_type in self.widgets and field_type or 'string' @@ -2708,6 +2714,9 @@ class SQLFORM(FORM): _href='%s/%s' % (upload, value)) else: value = '' + elif isinstance(field.type, SQLCustomType) and callable(field.type.represent): + # SQLCustomType has a represent, use it + value = field.type.represent(value, row) if isinstance(value, str): value = truncate_string(value, maxlength) elif not isinstance(value, XmlComponent): From 19c83d4ad697abdd0665ab7886f7fa4793b37736 Mon Sep 17 00:00:00 2001 From: Hardirc Date: Mon, 13 Apr 2015 18:43:48 -0400 Subject: [PATCH 005/115] Update gluon/contrib/pypyodbc.py 1.3.0 -> 1.3.3 --- gluon/contrib/pypyodbc.py | 860 +++++++++++++++++++------------------- 1 file changed, 439 insertions(+), 421 deletions(-) diff --git a/gluon/contrib/pypyodbc.py b/gluon/contrib/pypyodbc.py index bb2a9ceb..b58a6557 100644 --- a/gluon/contrib/pypyodbc.py +++ b/gluon/contrib/pypyodbc.py @@ -7,25 +7,25 @@ # Copyright (c) 2014 Henry Zhou and PyPyODBC contributors # Copyright (c) 2004 Michele Petrazzo -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated # documentation files (the "Software"), to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in all copies or substantial portions +# The above copyright notice and this permission notice shall be included in all copies or substantial portions # of the Software. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. pooling = True apilevel = '2.0' paramstyle = 'qmark' threadsafety = 1 -version = '1.3.0' +version = '1.3.3' lowercase=True DEBUG = 0 @@ -52,7 +52,7 @@ else: use_unicode = False if py_ver < '2.6': bytearray = str - + if not hasattr(ctypes, 'c_ssize_t'): if ctypes.sizeof(ctypes.c_uint) == ctypes.sizeof(ctypes.c_void_p): @@ -69,7 +69,7 @@ SQLWCHAR_SIZE = ctypes.sizeof(ctypes.c_wchar) #determin the size of Py_UNICODE #sys.maxunicode > 65536 and 'UCS4' or 'UCS2' -UNICODE_SIZE = sys.maxunicode > 65536 and 4 or 2 +UNICODE_SIZE = sys.maxunicode > 65536 and 4 or 2 # Define ODBC constants. They are widly used in ODBC documents and programs @@ -89,7 +89,7 @@ SQL_ATTR_AUTOCOMMIT = SQL_AUTOCOMMIT = 102 SQL_MODE_DEFAULT = SQL_MODE_READ_WRITE = 0; SQL_MODE_READ_ONLY = 1 SQL_AUTOCOMMIT_OFF, SQL_AUTOCOMMIT_ON = 0, 1 SQL_IS_UINTEGER = -5 -SQL_ATTR_LOGIN_TIMEOUT = 103; SQL_ATTR_CONNECTION_TIMEOUT = 113 +SQL_ATTR_LOGIN_TIMEOUT = 103; SQL_ATTR_CONNECTION_TIMEOUT = 113;SQL_ATTR_QUERY_TIMEOUT = 0 SQL_COMMIT, SQL_ROLLBACK = 0, 1 SQL_INDEX_UNIQUE,SQL_INDEX_ALL = 0,1 @@ -402,15 +402,15 @@ class OperationalError(DatabaseError): self.value = (error_code, error_desc) self.args = (error_code, error_desc) - - - -############################################################################ + + + +############################################################################ # # Find the ODBC library on the platform and connect to it using ctypes # ############################################################################ -# Get the References of the platform's ODBC functions via ctypes +# Get the References of the platform's ODBC functions via ctypes odbc_decoding = 'utf_16' odbc_encoding = 'utf_16_le' @@ -421,7 +421,7 @@ if sys.platform in ('win32','cli'): # On Windows, the size of SQLWCHAR is hardcoded to 2-bytes. SQLWCHAR_SIZE = ctypes.sizeof(ctypes.c_ushort) else: - # Set load the library on linux + # Set load the library on linux try: # First try direct loading libodbc.so ODBC_API = ctypes.cdll.LoadLibrary('libodbc.so') @@ -446,7 +446,7 @@ else: except: # If still fail loading, abort. raise OdbcLibraryError('Error while loading ' + library) - + # only iODBC uses utf-32 / UCS4 encoding data, others normally use utf-16 / UCS2 # So we set those for handling. if 'libiodbc.dylib' in library: @@ -506,17 +506,17 @@ if sys.platform not in ('win32','cli'): raise OdbcLibraryError('Using narrow Python build with ODBC library ' 'expecting wide unicode is not supported.') - - - - - - - - - - - + + + + + + + + + + + ############################################################ # Database value to Python data type mappings @@ -551,11 +551,11 @@ SQL_C_DOUBLE = SQL_DOUBLE = 8 SQL_C_TYPE_DATE = SQL_TYPE_DATE = 91 SQL_C_TYPE_TIME = SQL_TYPE_TIME = 92 SQL_C_BINARY = SQL_BINARY = -2 -SQL_C_SBIGINT = SQL_BIGINT + SQL_SIGNED_OFFSET +SQL_C_SBIGINT = SQL_BIGINT + SQL_SIGNED_OFFSET SQL_C_TINYINT = SQL_TINYINT = -6 SQL_C_BIT = SQL_BIT = -7 SQL_C_WCHAR = SQL_WCHAR = -8 -SQL_C_GUID = SQL_GUID = -11 +SQL_C_GUID = SQL_GUID = -11 SQL_C_TYPE_TIMESTAMP = SQL_TYPE_TIMESTAMP = 93 SQL_C_DEFAULT = 99 @@ -565,35 +565,37 @@ def dttm_cvt(x): if py_v3: x = x.decode('ascii') if x == '': return None - else: return datetime.datetime(int(x[0:4]),int(x[5:7]),int(x[8:10]),int(x[10:13]),int(x[14:16]),int(x[17:19]),int(x[20:].ljust(6,'0'))) + x = x.ljust(26,'0') + return datetime.datetime(int(x[0:4]),int(x[5:7]),int(x[8:10]),int(x[10:13]),int(x[14:16]),int(x[17:19]),int(x[20:26])) def tm_cvt(x): if py_v3: x = x.decode('ascii') if x == '': return None - else: return datetime.time(int(x[0:2]),int(x[3:5]),int(x[6:8]),int(x[9:].ljust(6,'0'))) + x = x.ljust(15,'0') + return datetime.time(int(x[0:2]),int(x[3:5]),int(x[6:8]),int(x[9:15])) def dt_cvt(x): if py_v3: x = x.decode('ascii') if x == '': return None - else: return datetime.date(int(x[0:4]),int(x[5:7]),int(x[8:10])) + else:return datetime.date(int(x[0:4]),int(x[5:7]),int(x[8:10])) def Decimal_cvt(x): if py_v3: - x = x.decode('ascii') + x = x.decode('ascii') return Decimal(x) - + bytearray_cvt = bytearray if sys.platform == 'cli': bytearray_cvt = lambda x: bytearray(buffer(x)) - + # Below Datatype mappings referenced the document at # http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.help.sdk_12.5.1.aseodbc/html/aseodbc/CACFDIGH.htm SQL_data_type_dict = { \ #SQL Data TYPE 0.Python Data Type 1.Default Output Converter 2.Buffer Type 3.Buffer Allocator 4.Default Size 5.Variable Length -SQL_TYPE_NULL : (None, lambda x: None, SQL_C_CHAR, create_buffer, 2 , False ), +SQL_TYPE_NULL : (None, lambda x: None, SQL_C_CHAR, create_buffer, 2 , False ), SQL_CHAR : (str, lambda x: x, SQL_C_CHAR, create_buffer, 2048 , False ), SQL_NUMERIC : (Decimal, Decimal_cvt, SQL_C_CHAR, create_buffer, 150 , False ), SQL_DECIMAL : (Decimal, Decimal_cvt, SQL_C_CHAR, create_buffer, 150 , False ), @@ -620,8 +622,8 @@ SQL_GUID : (str, str, SQL_C_CH SQL_WLONGVARCHAR : (unicode, lambda x: x, SQL_C_WCHAR, create_buffer_u, 20500 , True ), SQL_TYPE_DATE : (datetime.date, dt_cvt, SQL_C_CHAR, create_buffer, 30 , False ), SQL_TYPE_TIME : (datetime.time, tm_cvt, SQL_C_CHAR, create_buffer, 20 , False ), -SQL_TYPE_TIMESTAMP : (datetime.datetime, dttm_cvt, SQL_C_CHAR, create_buffer, 30 , False ), -SQL_SS_VARIANT : (str, lambda x: x, SQL_C_CHAR, create_buffer, 2048 , True ), +SQL_TYPE_TIMESTAMP : (datetime.datetime, dttm_cvt, SQL_C_CHAR, create_buffer, 30 , False ), +SQL_SS_VARIANT : (str, lambda x: x, SQL_C_CHAR, create_buffer, 2048 , True ), SQL_SS_XML : (unicode, lambda x: x, SQL_C_WCHAR, create_buffer_u, 20500 , True ), SQL_SS_UDT : (bytearray, bytearray_cvt, SQL_C_BINARY, create_buffer, 5120 , True ), } @@ -696,6 +698,7 @@ funcs_with_ret = [ "SQLStatisticsW", "SQLTables", "SQLTablesW", + "SQLSetStmtAttr" ] for func_name in funcs_with_ret: @@ -744,7 +747,7 @@ if sys.platform not in ('cli'): ODBC_API.SQLDrivers.argtypes = [ ctypes.c_void_p, ctypes.c_ushort, - ctypes.c_char_p, ctypes.c_short, ctypes.POINTER(ctypes.c_short), + ctypes.c_char_p, ctypes.c_short, ctypes.POINTER(ctypes.c_short), ctypes.c_char_p, ctypes.c_short, ctypes.POINTER(ctypes.c_short), ] @@ -920,7 +923,7 @@ BLANK_BYTE = str_8b() def ctrl_err(ht, h, val_ret, ansi): """Classify type of ODBC error from (type of handle, handle, return value) , and raise with a list""" - + if ansi: state = create_buffer(22) Message = create_buffer(1024*4) @@ -938,7 +941,7 @@ def ctrl_err(ht, h, val_ret, ansi): Buffer_len = c_short() err_list = [] number_errors = 1 - + while 1: ret = ODBC_func(ht, h, number_errors, state, \ ADDR(NativeError), Message, 1024, ADDR(Buffer_len)) @@ -985,19 +988,19 @@ def check_success(ODBC_obj, ret): ctrl_err(SQL_HANDLE_DBC, ODBC_obj.dbc_h, ret, ODBC_obj.ansi) else: ctrl_err(SQL_HANDLE_ENV, ODBC_obj, ret, False) - - + + def AllocateEnv(): if pooling: ret = ODBC_API.SQLSetEnvAttr(SQL_NULL_HANDLE, SQL_ATTR_CONNECTION_POOLING, SQL_CP_ONE_PER_HENV, SQL_IS_UINTEGER) check_success(SQL_NULL_HANDLE, ret) - ''' + ''' Allocate an ODBC environment by initializing the handle shared_env_h ODBC enviroment needed to be created, so connections can be created under it connections pooling can be shared under one environment ''' - global shared_env_h + global shared_env_h shared_env_h = ctypes.c_void_p() ret = ODBC_API.SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, ADDR(shared_env_h)) check_success(shared_env_h, ret) @@ -1005,7 +1008,7 @@ def AllocateEnv(): # Set the ODBC environment's compatibil leve to ODBC 3.0 ret = ODBC_API.SQLSetEnvAttr(shared_env_h, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, 0) check_success(shared_env_h, ret) - + """ Here, we have a few callables that determine how a result row is returned. @@ -1022,20 +1025,20 @@ def TupleRow(cursor): """ class Row(tuple): cursor_description = cursor.description - + def get(self, field): if not hasattr(self, 'field_dict'): self.field_dict = {} for i,item in enumerate(self): self.field_dict[self.cursor_description[i][0]] = item return self.field_dict.get(field) - + def __getitem__(self, field): if isinstance(field, (unicode,str)): return self.get(field) else: return tuple.__getitem__(self,field) - + return Row @@ -1088,19 +1091,19 @@ def MutableNamedTupleRow(cursor): return Row # When Null is used in a binary parameter, database usually would not -# accept the None for a binary field, so the work around is to use a +# accept the None for a binary field, so the work around is to use a # Specical None that the pypyodbc moudle would know this NULL is for # a binary field. class BinaryNullType(): pass BinaryNull = BinaryNullType() -# The get_type function is used to determine if parameters need to be re-binded +# The get_type function is used to determine if parameters need to be re-binded # against the changed parameter types # 'b' for bool, 'U' for long unicode string, 'u' for short unicode string # 'S' for long 8 bit string, 's' for short 8 bit string, 'l' for big integer, 'i' for normal integer # 'f' for float, 'D' for Decimal, 't' for datetime.time, 'd' for datetime.datetime, 'dt' for datetime.datetime # 'bi' for binary -def get_type(v): +def get_type(v): if isinstance(v, bool): return ('b',) @@ -1130,7 +1133,7 @@ def get_type(v): t = v.as_tuple() #1.23 -> (1,2,3),-2 , 1.23*E7 -> (1,2,3),5 return ('D',(len(t[1]),0 - t[2])) # number of digits, and number of decimal digits - + elif isinstance (v, datetime.datetime): return ('dt',) elif isinstance (v, datetime.date): @@ -1139,7 +1142,7 @@ def get_type(v): return ('t',) elif isinstance (v, (bytearray, buffer)): return ('bi',(len(v)//1000+1)*1000) - + return type(v) @@ -1168,17 +1171,26 @@ class Cursor: self.arraysize = 1 ret = ODBC_API.SQLAllocHandle(SQL_HANDLE_STMT, self.connection.dbc_h, ADDR(self.stmt_h)) check_success(self, ret) + + self.timeout = conx.timeout + if self.timeout != 0: + self.set_timeout(self.timeout) + self._PARAM_SQL_TYPE_LIST = [] - self.closed = False - + self.closed = False + def set_timeout(self, timeout): + self.timeout = timeout + ret = ODBC_API.SQLSetStmtAttr(self.stmt_h, SQL_ATTR_QUERY_TIMEOUT, self.timeout, 0) + check_success(self, ret) + def prepare(self, query_string): """prepare a query""" - + #self._free_results(FREE_STATEMENT) if not self.connection: self.close() - + if type(query_string) == unicode: c_query_string = wchar_pointer(UCS_buf(query_string)) ret = ODBC_API.SQLPrepareW(self.stmt_h, c_query_string, len(query_string)) @@ -1187,10 +1199,10 @@ class Cursor: ret = ODBC_API.SQLPrepare(self.stmt_h, c_query_string, len(query_string)) if ret != SQL_SUCCESS: check_success(self, ret) - - - self._PARAM_SQL_TYPE_LIST = [] - + + + self._PARAM_SQL_TYPE_LIST = [] + if self.connection.support_SQLDescribeParam: # SQLServer's SQLDescribeParam only supports DML SQL, so avoid the SELECT statement if True:# 'SELECT' not in query_string.upper(): @@ -1199,7 +1211,7 @@ class Cursor: ret = ODBC_API.SQLNumParams(self.stmt_h, ADDR(NumParams)) if ret != SQL_SUCCESS: check_success(self, ret) - + for col_num in range(NumParams.value): ParameterNumber = ctypes.c_ushort(col_num + 1) DataType = c_short() @@ -1219,7 +1231,7 @@ class Cursor: check_success(self, ret) except DatabaseError: if sys.exc_info()[1].value[0] == '07009': - self._PARAM_SQL_TYPE_LIST = [] + self._PARAM_SQL_TYPE_LIST = [] break else: raise sys.exc_info()[1] @@ -1227,7 +1239,7 @@ class Cursor: raise sys.exc_info()[1] self._PARAM_SQL_TYPE_LIST.append((DataType.value,DecimalDigits.value)) - + self.statement = query_string @@ -1237,39 +1249,39 @@ class Cursor: if not self.connection: self.close() #self._free_results(NO_FREE_STATEMENT) - + # Get the number of query parameters judged by database. NumParams = c_short() ret = ODBC_API.SQLNumParams(self.stmt_h, ADDR(NumParams)) if ret != SQL_SUCCESS: check_success(self, ret) - + if len(param_types) != NumParams.value: # In case number of parameters provided do not same as number required error_desc = "The SQL contains %d parameter markers, but %d parameters were supplied" \ %(NumParams.value,len(param_types)) raise ProgrammingError('HY000',error_desc) - - + + # Every parameter needs to be binded to a buffer ParamBufferList = [] # Temporary holder since we can only call SQLDescribeParam before # calling SQLBindParam. temp_holder = [] for col_num in range(NumParams.value): - dec_num = 0 + dec_num = 0 buf_size = 512 - + if param_types[col_num][0] == 'u': sql_c_type = SQL_C_WCHAR - sql_type = SQL_WVARCHAR - buf_size = 255 - ParameterBuffer = create_buffer_u(buf_size) - + sql_type = SQL_WVARCHAR + buf_size = 255 + ParameterBuffer = create_buffer_u(buf_size) + elif param_types[col_num][0] == 's': sql_c_type = SQL_C_CHAR sql_type = SQL_VARCHAR - buf_size = 255 + buf_size = 255 ParameterBuffer = create_buffer(buf_size) @@ -1284,26 +1296,26 @@ class Cursor: sql_type = SQL_LONGVARCHAR buf_size = param_types[col_num][1]#len(self._inputsizers)>col_num and self._inputsizers[col_num] or 20500 ParameterBuffer = create_buffer(buf_size) - + # bool subclasses int, thus has to go first elif param_types[col_num][0] == 'b': sql_c_type = SQL_C_CHAR sql_type = SQL_BIT buf_size = SQL_data_type_dict[sql_type][4] ParameterBuffer = create_buffer(buf_size) - + elif param_types[col_num][0] == 'i': - sql_c_type = SQL_C_CHAR - sql_type = SQL_INTEGER - buf_size = SQL_data_type_dict[sql_type][4] - ParameterBuffer = create_buffer(buf_size) - + sql_c_type = SQL_C_CHAR + sql_type = SQL_INTEGER + buf_size = SQL_data_type_dict[sql_type][4] + ParameterBuffer = create_buffer(buf_size) + elif param_types[col_num][0] == 'l': - sql_c_type = SQL_C_CHAR - sql_type = SQL_BIGINT - buf_size = SQL_data_type_dict[sql_type][4] + sql_c_type = SQL_C_CHAR + sql_type = SQL_BIGINT + buf_size = SQL_data_type_dict[sql_type][4] ParameterBuffer = create_buffer(buf_size) - + elif param_types[col_num][0] == 'D': #Decimal sql_c_type = SQL_C_CHAR @@ -1311,56 +1323,56 @@ class Cursor: digit_num, dec_num = param_types[col_num][1] if dec_num > 0: # has decimal - buf_size = digit_num + buf_size = digit_num dec_num = dec_num else: # no decimal - buf_size = digit_num - dec_num + buf_size = digit_num - dec_num dec_num = 0 ParameterBuffer = create_buffer(buf_size + 4)# add extra length for sign and dot - + elif param_types[col_num][0] == 'f': sql_c_type = SQL_C_CHAR - sql_type = SQL_DOUBLE + sql_type = SQL_DOUBLE buf_size = SQL_data_type_dict[sql_type][4] ParameterBuffer = create_buffer(buf_size) - - + + # datetime subclasses date, thus has to go first elif param_types[col_num][0] == 'dt': sql_c_type = SQL_C_CHAR sql_type = SQL_TYPE_TIMESTAMP - buf_size = self.connection.type_size_dic[SQL_TYPE_TIMESTAMP][0] + buf_size = self.connection.type_size_dic[SQL_TYPE_TIMESTAMP][0] ParameterBuffer = create_buffer(buf_size) dec_num = self.connection.type_size_dic[SQL_TYPE_TIMESTAMP][1] - - + + elif param_types[col_num][0] == 'd': sql_c_type = SQL_C_CHAR if SQL_TYPE_DATE in self.connection.type_size_dic: #if DEBUG:print('conx.type_size_dic.has_key(SQL_TYPE_DATE)') sql_type = SQL_TYPE_DATE buf_size = self.connection.type_size_dic[SQL_TYPE_DATE][0] - + ParameterBuffer = create_buffer(buf_size) dec_num = self.connection.type_size_dic[SQL_TYPE_DATE][1] - + else: # SQL Sever <2008 doesn't have a DATE type. - sql_type = SQL_TYPE_TIMESTAMP - buf_size = 10 + sql_type = SQL_TYPE_TIMESTAMP + buf_size = 10 ParameterBuffer = create_buffer(buf_size) - - + + elif param_types[col_num][0] == 't': sql_c_type = SQL_C_CHAR if SQL_TYPE_TIME in self.connection.type_size_dic: sql_type = SQL_TYPE_TIME - buf_size = self.connection.type_size_dic[SQL_TYPE_TIME][0] + buf_size = self.connection.type_size_dic[SQL_TYPE_TIME][0] ParameterBuffer = create_buffer(buf_size) - dec_num = self.connection.type_size_dic[SQL_TYPE_TIME][1] + dec_num = self.connection.type_size_dic[SQL_TYPE_TIME][1] elif SQL_SS_TIME2 in self.connection.type_size_dic: # TIME type added in SQL Server 2008 sql_type = SQL_SS_TIME2 @@ -1370,16 +1382,16 @@ class Cursor: else: # SQL Sever <2008 doesn't have a TIME type. sql_type = SQL_TYPE_TIMESTAMP - buf_size = self.connection.type_size_dic[SQL_TYPE_TIMESTAMP][0] + buf_size = self.connection.type_size_dic[SQL_TYPE_TIMESTAMP][0] ParameterBuffer = create_buffer(buf_size) dec_num = 3 - + elif param_types[col_num][0] == 'BN': sql_c_type = SQL_C_BINARY - sql_type = SQL_VARBINARY - buf_size = 1 - ParameterBuffer = create_buffer(buf_size) - + sql_type = SQL_VARBINARY + buf_size = 1 + ParameterBuffer = create_buffer(buf_size) + elif param_types[col_num][0] == 'N': if len(self._PARAM_SQL_TYPE_LIST) > 0: sql_c_type = SQL_C_DEFAULT @@ -1389,64 +1401,64 @@ class Cursor: else: sql_c_type = SQL_C_CHAR sql_type = SQL_CHAR - buf_size = 1 - ParameterBuffer = create_buffer(buf_size) + buf_size = 1 + ParameterBuffer = create_buffer(buf_size) elif param_types[col_num][0] == 'bi': sql_c_type = SQL_C_BINARY - sql_type = SQL_LONGVARBINARY - buf_size = param_types[col_num][1]#len(self._inputsizers)>col_num and self._inputsizers[col_num] or 20500 + sql_type = SQL_LONGVARBINARY + buf_size = param_types[col_num][1]#len(self._inputsizers)>col_num and self._inputsizers[col_num] or 20500 ParameterBuffer = create_buffer(buf_size) - - + + else: sql_c_type = SQL_C_CHAR sql_type = SQL_LONGVARCHAR - buf_size = len(self._inputsizers)>col_num and self._inputsizers[col_num] or 20500 + buf_size = len(self._inputsizers)>col_num and self._inputsizers[col_num] or 20500 ParameterBuffer = create_buffer(buf_size) - + temp_holder.append((sql_c_type, sql_type, buf_size, dec_num, ParameterBuffer)) for col_num, (sql_c_type, sql_type, buf_size, dec_num, ParameterBuffer) in enumerate(temp_holder): BufferLen = c_ssize_t(buf_size) LenOrIndBuf = c_ssize_t() - - + + InputOutputType = SQL_PARAM_INPUT if len(pram_io_list) > col_num: InputOutputType = pram_io_list[col_num] ret = SQLBindParameter(self.stmt_h, col_num + 1, InputOutputType, sql_c_type, sql_type, buf_size,\ dec_num, ADDR(ParameterBuffer), BufferLen,ADDR(LenOrIndBuf)) - if ret != SQL_SUCCESS: + if ret != SQL_SUCCESS: check_success(self, ret) # Append the value buffer and the length buffer to the array ParamBufferList.append((ParameterBuffer,LenOrIndBuf,sql_type)) - + self._last_param_types = param_types self._ParamBufferList = ParamBufferList - + def execute(self, query_string, params=None, many_mode=False, call_mode=False): """ Execute the query string, with optional parameters. If parameters are provided, the query would first be prepared, then executed with parameters; - If parameters are not provided, only th query sting, it would be executed directly + If parameters are not provided, only th query sting, it would be executed directly """ if not self.connection: self.close() - + self._free_stmt(SQL_CLOSE) if params: # If parameters exist, first prepare the query then executed with parameters - + if not isinstance(params, (tuple, list)): raise TypeError("Params must be in a list, tuple, or Row") - + if query_string != self.statement: - # if the query is not same as last query, then it is not prepared + # if the query is not same as last query, then it is not prepared self.prepare(query_string) - - + + param_types = list(map(get_type, params)) if call_mode: @@ -1462,8 +1474,8 @@ class Cursor: elif sum([p_type[0] != 'N' and p_type != self._last_param_types[i] for i,p_type in enumerate(param_types)]) > 0: self._free_stmt(SQL_RESET_PARAMS) self._BindParams(param_types) - - + + # With query prepared, now put parameters into buffers col_num = 0 for param_buffer, param_buffer_len, sql_type in self._ParamBufferList: @@ -1479,24 +1491,24 @@ class Cursor: else: c_char_buf = str(param_val) c_buf_len = len(c_char_buf) - + elif param_types[col_num][0] in ('s','S'): c_char_buf = param_val c_buf_len = len(c_char_buf) elif param_types[col_num][0] in ('u','U'): c_char_buf = UCS_buf(param_val) c_buf_len = len(c_char_buf) - + elif param_types[col_num][0] == 'dt': max_len = self.connection.type_size_dic[SQL_TYPE_TIMESTAMP][0] datetime_str = param_val.strftime('%Y-%m-%d %H:%M:%S.%f') c_char_buf = datetime_str[:max_len] if py_v3: c_char_buf = bytes(c_char_buf,'ascii') - + c_buf_len = len(c_char_buf) # print c_buf_len, c_char_buf - + elif param_types[col_num][0] == 'd': if SQL_TYPE_DATE in self.connection.type_size_dic: max_len = self.connection.type_size_dic[SQL_TYPE_DATE][0] @@ -1507,7 +1519,7 @@ class Cursor: c_char_buf = bytes(c_char_buf,'ascii') c_buf_len = len(c_char_buf) #print c_char_buf - + elif param_types[col_num][0] == 't': if SQL_TYPE_TIME in self.connection.type_size_dic: max_len = self.connection.type_size_dic[SQL_TYPE_TIME][0] @@ -1526,7 +1538,7 @@ class Cursor: if py_v3: c_char_buf = bytes(c_char_buf,'ascii') #print c_buf_len, c_char_buf - + elif param_types[col_num][0] == 'b': if param_val == True: c_char_buf = '1' @@ -1535,7 +1547,7 @@ class Cursor: if py_v3: c_char_buf = bytes(c_char_buf,'ascii') c_buf_len = 1 - + elif param_types[col_num][0] == 'D': #Decimal sign = param_val.as_tuple()[0] == 0 and '+' or '-' digit_string = ''.join([str(x) for x in param_val.as_tuple()[1]]) @@ -1555,18 +1567,18 @@ class Cursor: else: c_char_buf = v c_buf_len = len(c_char_buf) - + elif param_types[col_num][0] == 'bi': c_char_buf = str_8b(param_val) c_buf_len = len(c_char_buf) - + else: c_char_buf = param_val - - + + if param_types[col_num][0] == 'bi': param_buffer.raw = str_8b(param_val) - + else: #print (type(param_val),param_buffer, param_buffer.value) param_buffer.value = c_char_buf @@ -1576,37 +1588,37 @@ class Cursor: param_buffer_len.value = SQL_NTS else: param_buffer_len.value = c_buf_len - + col_num += 1 ret = SQLExecute(self.stmt_h) if ret != SQL_SUCCESS: #print param_valparam_buffer, param_buffer.value check_success(self, ret) - + if not many_mode: self._NumOfRows() self._UpdateDesc() #self._BindCols() - + else: self.execdirect(query_string) return self - - + + def _SQLExecute(self): if not self.connection: self.close() ret = SQLExecute(self.stmt_h) if ret != SQL_SUCCESS: - check_success(self, ret) - - + check_success(self, ret) + + def execdirect(self, query_string): """Execute a query directly""" if not self.connection: self.close() - + self._free_stmt() self._last_param_types = None self.statement = None @@ -1621,25 +1633,25 @@ class Cursor: self._UpdateDesc() #self._BindCols() return self - - + + def callproc(self, procname, args): if not self.connection: self.close() raise Warning('', 'Still not fully implemented') self._pram_io_list = [row[4] for row in self.procedurecolumns(procedure = procname).fetchall() if row[4] not in (SQL_RESULT_COL, SQL_RETURN_VALUE)] - + print('pram_io_list: '+str(self._pram_io_list)) - - + + call_escape = '{CALL '+procname if args: call_escape += '(' + ','.join(['?' for params in args]) + ')' call_escape += '}' self.execute(call_escape, args, call_mode = True) - + result = [] for buf, buf_len, sql_type in self._ParamBufferList: @@ -1649,20 +1661,20 @@ class Cursor: result.append(self.connection.output_converter[sql_type](buf.value)) return result - - + + def executemany(self, query_string, params_list = [None]): if not self.connection: self.close() - + for params in params_list: self.execute(query_string, params, many_mode = True) self._NumOfRows() self.rowcount = -1 self._UpdateDesc() #self._BindCols() - - + + def _CreateColBuf(self): if not self.connection: @@ -1672,60 +1684,60 @@ class Cursor: self._ColBufferList = [] bind_data = True for col_num in range(NOC): - col_name = self.description[col_num][0] - col_size = self.description[col_num][2] - col_sql_data_type = self._ColTypeCodeList[col_num] + col_name = self.description[col_num][0] + col_size = self.description[col_num][2] + col_sql_data_type = self._ColTypeCodeList[col_num] target_type = SQL_data_type_dict[col_sql_data_type][2] - dynamic_length = SQL_data_type_dict[col_sql_data_type][5] + dynamic_length = SQL_data_type_dict[col_sql_data_type][5] # set default size base on the column's sql data type - total_buf_len = SQL_data_type_dict[col_sql_data_type][4] - + total_buf_len = SQL_data_type_dict[col_sql_data_type][4] + # over-write if there's pre-set size value for "large columns" - if total_buf_len > 20500: + if total_buf_len > 20500: total_buf_len = self._outputsize.get(None,total_buf_len) - # over-write if there's pre-set size value for the "col_num" column + # over-write if there's pre-set size value for the "col_num" column total_buf_len = self._outputsize.get(col_num, total_buf_len) # if the size of the buffer is very long, do not bind - # because a large buffer decrease performance, and sometimes you only get a NULL value. + # because a large buffer decrease performance, and sometimes you only get a NULL value. # in that case use sqlgetdata instead. if col_size >= 1024: - dynamic_length = True + dynamic_length = True alloc_buffer = SQL_data_type_dict[col_sql_data_type][3](total_buf_len) used_buf_len = c_ssize_t() - + force_unicode = self.connection.unicode_results - + if force_unicode and col_sql_data_type in (SQL_CHAR,SQL_VARCHAR,SQL_LONGVARCHAR): target_type = SQL_C_WCHAR alloc_buffer = create_buffer_u(total_buf_len) - + buf_cvt_func = self.connection.output_converter[self._ColTypeCodeList[col_num]] - + if bind_data: if dynamic_length: bind_data = False - self._ColBufferList.append([col_name, target_type, used_buf_len, ADDR(used_buf_len), alloc_buffer, ADDR(alloc_buffer), total_buf_len, buf_cvt_func, bind_data]) - + self._ColBufferList.append([col_name, target_type, used_buf_len, ADDR(used_buf_len), alloc_buffer, ADDR(alloc_buffer), total_buf_len, buf_cvt_func, bind_data]) + if bind_data: ret = ODBC_API.SQLBindCol(self.stmt_h, col_num + 1, target_type, ADDR(alloc_buffer), total_buf_len, ADDR(used_buf_len)) if ret != SQL_SUCCESS: check_success(self, ret) - + def _UpdateDesc(self): - "Get the information of (name, type_code, display_size, internal_size, col_precision, scale, null_ok)" + "Get the information of (name, type_code, display_size, internal_size, col_precision, scale, null_ok)" if not self.connection: self.close() - + force_unicode = self.connection.unicode_results if force_unicode: Cname = create_buffer_u(1024) else: Cname = create_buffer(1024) - + Cname_ptr = c_short() Ctype_code = c_short() Csize = ctypes.c_size_t() @@ -1736,34 +1748,34 @@ class Cursor: self._ColTypeCodeList = [] NOC = self._NumOfCols() for col in range(1, NOC+1): - - ret = ODBC_API.SQLColAttribute(self.stmt_h, col, SQL_DESC_DISPLAY_SIZE, ADDR(create_buffer(10)), + + ret = ODBC_API.SQLColAttribute(self.stmt_h, col, SQL_DESC_DISPLAY_SIZE, ADDR(create_buffer(10)), 10, ADDR(c_short()),ADDR(Cdisp_size)) if ret != SQL_SUCCESS: check_success(self, ret) - + if force_unicode: - + ret = ODBC_API.SQLDescribeColW(self.stmt_h, col, Cname, len(Cname), ADDR(Cname_ptr),\ ADDR(Ctype_code),ADDR(Csize),ADDR(CDecimalDigits), ADDR(Cnull_ok)) if ret != SQL_SUCCESS: check_success(self, ret) else: - + ret = ODBC_API.SQLDescribeCol(self.stmt_h, col, Cname, len(Cname), ADDR(Cname_ptr),\ ADDR(Ctype_code),ADDR(Csize),ADDR(CDecimalDigits), ADDR(Cnull_ok)) if ret != SQL_SUCCESS: check_success(self, ret) - + col_name = Cname.value if lowercase: col_name = col_name.lower() - #(name, type_code, display_size, + #(name, type_code, display_size, ColDescr.append((col_name, SQL_data_type_dict.get(Ctype_code.value,(Ctype_code.value,))[0],Cdisp_size.value,\ Csize.value, Csize.value,CDecimalDigits.value,Cnull_ok.value == 1 and True or False)) self._ColTypeCodeList.append(Ctype_code.value) - + if len(ColDescr) > 0: self.description = ColDescr # Create the row type before fetching. @@ -1771,26 +1783,26 @@ class Cursor: else: self.description = None self._CreateColBuf() - - + + def _NumOfRows(self): """Get the number of rows""" if not self.connection: self.close() - + NOR = c_ssize_t() ret = SQLRowCount(self.stmt_h, ADDR(NOR)) if ret != SQL_SUCCESS: check_success(self, ret) self.rowcount = NOR.value - return self.rowcount - + return self.rowcount + def _NumOfCols(self): """Get the number of cols""" if not self.connection: self.close() - + NOC = c_short() ret = SQLNumResultCols(self.stmt_h, ADDR(NOC)) if ret != SQL_SUCCESS: @@ -1801,7 +1813,7 @@ class Cursor: def fetchall(self): if not self.connection: self.close() - + rows = [] while True: row = self.fetchone() @@ -1814,11 +1826,11 @@ class Cursor: def fetchmany(self, num = None): if not self.connection: self.close() - + if num is None: num = self.arraysize rows = [] - + while len(rows) < num: row = self.fetchone() if row is None: @@ -1830,12 +1842,12 @@ class Cursor: def fetchone(self): if not self.connection: self.close() - + ret = SQLFetch(self.stmt_h) - - if ret in (SQL_SUCCESS,SQL_SUCCESS_WITH_INFO): + + if ret in (SQL_SUCCESS,SQL_SUCCESS_WITH_INFO): '''Bind buffers for the record set columns''' - + value_list = [] col_num = 1 for col_name, target_type, used_buf_len, ADDR_used_buf_len, alloc_buffer, ADDR_alloc_buffer, total_buf_len, buf_cvt_func, bind_data in self._ColBufferList: @@ -1844,10 +1856,10 @@ class Cursor: if bind_data: ret = SQL_SUCCESS else: - ret = SQLGetData(self.stmt_h, col_num, target_type, ADDR_alloc_buffer, total_buf_len, ADDR_used_buf_len) + ret = SQLGetData(self.stmt_h, col_num, target_type, ADDR_alloc_buffer, total_buf_len, ADDR_used_buf_len) if ret == SQL_SUCCESS: if used_buf_len.value == SQL_NULL_DATA: - value_list.append(None) + value_list.append(None) else: if raw_data_parts == []: # Means no previous data, no need to combine @@ -1865,21 +1877,21 @@ class Cursor: raw_data_parts.append(from_buffer_u(alloc_buffer)) else: raw_data_parts.append(alloc_buffer.value) - break - + break + elif ret == SQL_SUCCESS_WITH_INFO: # Means the data is only partial if target_type == SQL_C_BINARY: raw_data_parts.append(alloc_buffer.raw) else: - raw_data_parts.append(alloc_buffer.value) - + raw_data_parts.append(alloc_buffer.value) + elif ret == SQL_NO_DATA: # Means all data has been transmitted break else: - check_success(self, ret) - + check_success(self, ret) + if raw_data_parts != []: if py_v3: if target_type != SQL_C_BINARY: @@ -1893,47 +1905,47 @@ class Cursor: col_num += 1 return self._row_type(value_list) - + else: if ret == SQL_NO_DATA_FOUND: - + return None else: check_success(self, ret) - + def __next__(self): return self.next() - - def next(self): + + def next(self): row = self.fetchone() if row is None: raise(StopIteration) return row - + def __iter__(self): return self - + def skip(self, count = 0): if not self.connection: self.close() - + for i in range(count): ret = ODBC_API.SQLFetchScroll(self.stmt_h, SQL_FETCH_NEXT, 0) if ret != SQL_SUCCESS: check_success(self, ret) - return None - - - + return None + + + def nextset(self): if not self.connection: self.close() - + ret = ODBC_API.SQLMoreResults(self.stmt_h) if ret not in (SQL_SUCCESS, SQL_NO_DATA): check_success(self, ret) - + if ret == SQL_NO_DATA: self._free_stmt() return False @@ -1942,15 +1954,15 @@ class Cursor: self._UpdateDesc() #self._BindCols() return True - - + + def _free_stmt(self, free_type = None): if not self.connection: self.close() - + if not self.connection.connected: raise ProgrammingError('HY000','Attempt to use a closed connection.') - + #self.description = None #self.rowcount = -1 if free_type in (SQL_CLOSE, None): @@ -1961,17 +1973,17 @@ class Cursor: ret = ODBC_API.SQLFreeStmt(self.stmt_h, SQL_UNBIND) if ret != SQL_SUCCESS: check_success(self, ret) - if free_type in (SQL_RESET_PARAMS, None): + if free_type in (SQL_RESET_PARAMS, None): ret = ODBC_API.SQLFreeStmt(self.stmt_h, SQL_RESET_PARAMS) if ret != SQL_SUCCESS: check_success(self, ret) - - - + + + def getTypeInfo(self, sqlType = None): if not self.connection: self.close() - + if sqlType is None: type = SQL_ALL_TYPES else: @@ -1982,89 +1994,89 @@ class Cursor: self._UpdateDesc() #self._BindCols() return self.fetchone() - - + + def tables(self, table=None, catalog=None, schema=None, tableType=None): - """Return a list with all tables""" + """Return a list with all tables""" if not self.connection: self.close() - + l_catalog = l_schema = l_table = l_tableType = 0 - + if unicode in [type(x) for x in (table, catalog, schema,tableType)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) API_f = ODBC_API.SQLTablesW else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLTables - - - + + + if catalog is not None: l_catalog = len(catalog) - catalog = string_p(catalog) + catalog = string_p(catalog) - if schema is not None: + if schema is not None: l_schema = len(schema) schema = string_p(schema) - + if table is not None: l_table = len(table) table = string_p(table) - - if tableType is not None: + + if tableType is not None: l_tableType = len(tableType) tableType = string_p(tableType) - + self._free_stmt() self._last_param_types = None self.statement = None ret = API_f(self.stmt_h, catalog, l_catalog, - schema, l_schema, + schema, l_schema, table, l_table, tableType, l_tableType) check_success(self, ret) - + self._NumOfRows() self._UpdateDesc() #self._BindCols() return self - - + + def columns(self, table=None, catalog=None, schema=None, column=None): - """Return a list with all columns""" + """Return a list with all columns""" if not self.connection: self.close() - + l_catalog = l_schema = l_table = l_column = 0 - + if unicode in [type(x) for x in (table, catalog, schema,column)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) API_f = ODBC_API.SQLColumnsW else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLColumns - - - - if catalog is not None: + + + + if catalog is not None: l_catalog = len(catalog) catalog = string_p(catalog) if schema is not None: l_schema = len(schema) schema = string_p(schema) - if table is not None: + if table is not None: l_table = len(table) table = string_p(table) - if column is not None: + if column is not None: l_column = len(column) column = string_p(column) - + self._free_stmt() self._last_param_types = None self.statement = None - + ret = API_f(self.stmt_h, catalog, l_catalog, schema, l_schema, @@ -2076,87 +2088,87 @@ class Cursor: self._UpdateDesc() #self._BindCols() return self - - + + def primaryKeys(self, table=None, catalog=None, schema=None): if not self.connection: self.close() - + l_catalog = l_schema = l_table = 0 - + if unicode in [type(x) for x in (table, catalog, schema)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) API_f = ODBC_API.SQLPrimaryKeysW else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLPrimaryKeys - - - - if catalog is not None: + + + + if catalog is not None: l_catalog = len(catalog) catalog = string_p(catalog) - - if schema is not None: + + if schema is not None: l_schema = len(schema) schema = string_p(schema) - - if table is not None: + + if table is not None: l_table = len(table) table = string_p(table) - + self._free_stmt() self._last_param_types = None self.statement = None - + ret = API_f(self.stmt_h, catalog, l_catalog, schema, l_schema, table, l_table) check_success(self, ret) - + self._NumOfRows() self._UpdateDesc() #self._BindCols() return self - - + + def foreignKeys(self, table=None, catalog=None, schema=None, foreignTable=None, foreignCatalog=None, foreignSchema=None): if not self.connection: self.close() - + l_catalog = l_schema = l_table = l_foreignTable = l_foreignCatalog = l_foreignSchema = 0 - + if unicode in [type(x) for x in (table, catalog, schema,foreignTable,foreignCatalog,foreignSchema)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) API_f = ODBC_API.SQLForeignKeysW else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLForeignKeys - - if catalog is not None: + + if catalog is not None: l_catalog = len(catalog) catalog = string_p(catalog) - if schema is not None: + if schema is not None: l_schema = len(schema) schema = string_p(schema) - if table is not None: + if table is not None: l_table = len(table) table = string_p(table) - if foreignTable is not None: + if foreignTable is not None: l_foreignTable = len(foreignTable) foreignTable = string_p(foreignTable) - if foreignCatalog is not None: + if foreignCatalog is not None: l_foreignCatalog = len(foreignCatalog) foreignCatalog = string_p(foreignCatalog) - if foreignSchema is not None: + if foreignSchema is not None: l_foreignSchema = len(foreignSchema) foreignSchema = string_p(foreignSchema) - + self._free_stmt() self._last_param_types = None self.statement = None - + ret = API_f(self.stmt_h, catalog, l_catalog, schema, l_schema, @@ -2165,17 +2177,17 @@ class Cursor: foreignSchema, l_foreignSchema, foreignTable, l_foreignTable) check_success(self, ret) - + self._NumOfRows() self._UpdateDesc() #self._BindCols() return self - - + + def procedurecolumns(self, procedure=None, catalog=None, schema=None, column=None): if not self.connection: self.close() - + l_catalog = l_schema = l_procedure = l_column = 0 if unicode in [type(x) for x in (procedure, catalog, schema,column)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) @@ -2183,74 +2195,74 @@ class Cursor: else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLProcedureColumns - - - if catalog is not None: + + + if catalog is not None: l_catalog = len(catalog) catalog = string_p(catalog) - if schema is not None: + if schema is not None: l_schema = len(schema) schema = string_p(schema) - if procedure is not None: + if procedure is not None: l_procedure = len(procedure) procedure = string_p(procedure) - if column is not None: + if column is not None: l_column = len(column) column = string_p(column) - - + + self._free_stmt() self._last_param_types = None self.statement = None - + ret = API_f(self.stmt_h, catalog, l_catalog, schema, l_schema, procedure, l_procedure, column, l_column) check_success(self, ret) - + self._NumOfRows() self._UpdateDesc() return self - - + + def procedures(self, procedure=None, catalog=None, schema=None): if not self.connection: self.close() - + l_catalog = l_schema = l_procedure = 0 - + if unicode in [type(x) for x in (procedure, catalog, schema)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) API_f = ODBC_API.SQLProceduresW else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLProcedures - - - - if catalog is not None: + + + + if catalog is not None: l_catalog = len(catalog) catalog = string_p(catalog) - if schema is not None: + if schema is not None: l_schema = len(schema) schema = string_p(schema) - if procedure is not None: + if procedure is not None: l_procedure = len(procedure) procedure = string_p(procedure) - - + + self._free_stmt() self._last_param_types = None self.statement = None - + ret = API_f(self.stmt_h, catalog, l_catalog, schema, l_schema, procedure, l_procedure) check_success(self, ret) - + self._NumOfRows() self._UpdateDesc() return self @@ -2259,27 +2271,27 @@ class Cursor: def statistics(self, table, catalog=None, schema=None, unique=False, quick=True): if not self.connection: self.close() - + l_table = l_catalog = l_schema = 0 - + if unicode in [type(x) for x in (table, catalog, schema)]: string_p = lambda x:wchar_pointer(UCS_buf(x)) API_f = ODBC_API.SQLStatisticsW else: string_p = ctypes.c_char_p API_f = ODBC_API.SQLStatistics - - - if catalog is not None: + + + if catalog is not None: l_catalog = len(catalog) catalog = string_p(catalog) - if schema is not None: + if schema is not None: l_schema = len(schema) schema = string_p(schema) - if table is not None: + if table is not None: l_table = len(table) table = string_p(table) - + if unique: Unique = SQL_INDEX_UNIQUE else: @@ -2288,23 +2300,23 @@ class Cursor: Reserved = SQL_QUICK else: Reserved = SQL_ENSURE - + self._free_stmt() self._last_param_types = None self.statement = None - + ret = API_f(self.stmt_h, catalog, l_catalog, - schema, l_schema, + schema, l_schema, table, l_table, Unique, Reserved) check_success(self, ret) - + self._NumOfRows() self._UpdateDesc() #self._BindCols() return self - + def commit(self): if not self.connection: @@ -2315,12 +2327,12 @@ class Cursor: if not self.connection: self.close() self.connection.rollback() - + def setoutputsize(self, size, column = None): if not self.connection: self.close() self._outputsize[column] = size - + def setinputsizes(self, sizes): if not self.connection: self.close() @@ -2331,7 +2343,7 @@ class Cursor: """ Call SQLCloseCursor API to free the statement handle""" # ret = ODBC_API.SQLCloseCursor(self.stmt_h) # check_success(self, ret) -# +# if self.connection.connected: ret = ODBC_API.SQLFreeStmt(self.stmt_h, SQL_CLOSE) check_success(self, ret) @@ -2344,36 +2356,40 @@ class Cursor: ret = ODBC_API.SQLFreeHandle(SQL_HANDLE_STMT, self.stmt_h) check_success(self, ret) - - + + self.closed = True - - - def __del__(self): + + + def __del__(self): if not self.closed: self.close() - + def __exit__(self, type, value, traceback): if not self.connection: self.close() - + if value: self.rollback() else: self.commit() - + self.close() - - + + def __enter__(self): return self -# This class implement a odbc connection. -# -# + + +# This class implement a odbc connection. +# +# +connection_timeout = 0 + class Connection: def __init__(self, connectString = '', autocommit = False, ansi = False, timeout = 0, unicode_results = use_unicode, readonly = False, **kargs): """Init variables and connect to the engine""" @@ -2384,13 +2400,14 @@ class Connection: self.dbc_h = ctypes.c_void_p() self.autocommit = autocommit self.readonly = False + # the query timeout value self.timeout = 0 # self._cursors = [] for key, value in list(kargs.items()): connectString = connectString + key + '=' + value + ';' self.connectString = connectString - + self.clear_output_converters() try: @@ -2400,40 +2417,45 @@ class Connection: AllocateEnv() finally: lock.release() - + # Allocate an DBC handle self.dbc_h under the environment shared_env_h # This DBC handle is actually the basis of a "connection" - # The handle of self.dbc_h will be used to connect to a certain source + # The handle of self.dbc_h will be used to connect to a certain source # in the self.connect and self.ConnectByDSN method - + ret = ODBC_API.SQLAllocHandle(SQL_HANDLE_DBC, shared_env_h, ADDR(self.dbc_h)) check_success(self, ret) + self.connection_timeout = connection_timeout + if self.connection_timeout != 0: + self.set_connection_timeout(connection_timeout) + + self.connect(connectString, autocommit, ansi, timeout, unicode_results, readonly) - - - + + def set_connection_timeout(self,connection_timeout): + self.connection_timeout = connection_timeout + ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_CONNECTION_TIMEOUT, connection_timeout, SQL_IS_UINTEGER); + check_success(self, ret) + def connect(self, connectString = '', autocommit = False, ansi = False, timeout = 0, unicode_results = use_unicode, readonly = False): """Connect to odbc, using connect strings and set the connection's attributes like autocommit and timeout by calling SQLSetConnectAttr - """ + """ # Before we establish the connection by the connection string # Set the connection's attribute of "timeout" (Actully LOGIN_TIMEOUT) if timeout != 0: - self.settimeout(timeout) ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_LOGIN_TIMEOUT, timeout, SQL_IS_UINTEGER); check_success(self, ret) - - # Create one connection with a connect string by calling SQLDriverConnect # and make self.dbc_h the handle of this connection # Convert the connetsytring to encoded string - # so it can be converted to a ctypes c_char array object - + # so it can be converted to a ctypes c_char array object + self.ansi = ansi if not ansi: c_connectString = wchar_pointer(UCS_buf(self.connectString)) @@ -2459,43 +2481,39 @@ class Connection: else: ret = odbc_func(self.dbc_h, 0, c_connectString, len(self.connectString), None, 0, None, SQL_DRIVER_NOPROMPT) check_success(self, ret) - - - # Set the connection's attribute of "autocommit" + + + # Set the connection's attribute of "autocommit" # self.autocommit = autocommit - + if self.autocommit == True: ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_ON, SQL_IS_UINTEGER) else: ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, SQL_IS_UINTEGER) check_success(self, ret) - - # Set the connection's attribute of "readonly" + + # Set the connection's attribute of "readonly" # self.readonly = readonly - - ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_ACCESS_MODE, self.readonly and SQL_MODE_READ_ONLY or SQL_MODE_READ_WRITE, SQL_IS_UINTEGER) - check_success(self, ret) - + if self.readonly == True: + ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_ACCESS_MODE, SQL_MODE_READ_ONLY, SQL_IS_UINTEGER) + check_success(self, ret) + self.unicode_results = unicode_results self.connected = 1 self.update_db_special_info() - + def clear_output_converters(self): self.output_converter = {} for sqltype, profile in SQL_data_type_dict.items(): self.output_converter[sqltype] = profile[1] - - + + def add_output_converter(self, sqltype, func): self.output_converter[sqltype] = func - - def settimeout(self, timeout): - ret = ODBC_API.SQLSetConnectAttr(self.dbc_h, SQL_ATTR_CONNECTION_TIMEOUT, timeout, SQL_IS_UINTEGER); - check_success(self, ret) - self.timeout = timeout - + + def ConnectByDSN(self, dsn, user, passwd = ''): """Connect to odbc, we need dsn, user and optionally password""" @@ -2504,21 +2522,21 @@ class Connection: self.passwd = passwd sn = create_buffer(dsn) - un = create_buffer(user) + un = create_buffer(user) pw = create_buffer(passwd) - + ret = ODBC_API.SQLConnect(self.dbc_h, sn, len(sn), un, len(un), pw, len(pw)) check_success(self, ret) self.update_db_special_info() self.connected = 1 - - - def cursor(self, row_type_callable=None): + + + def cursor(self, row_type_callable=None): #self.settimeout(self.timeout) if not self.connected: raise ProgrammingError('HY000','Attempt to use a closed connection.') - cur = Cursor(self, row_type_callable=row_type_callable) + cur = Cursor(self, row_type_callable=row_type_callable) # self._cursors.append(cur) return cur @@ -2538,7 +2556,7 @@ class Connection: except: pass cur.close() - + self.support_SQLDescribeParam = False try: driver_name = self.getinfo(SQL_DRIVER_NAME) @@ -2546,11 +2564,11 @@ class Connection: self.support_SQLDescribeParam = True except: pass - + def commit(self): if not self.connected: raise ProgrammingError('HY000','Attempt to use a closed connection.') - + ret = SQLEndTran(SQL_HANDLE_DBC, self.dbc_h, SQL_COMMIT) if ret != SQL_SUCCESS: check_success(self, ret) @@ -2561,14 +2579,14 @@ class Connection: ret = SQLEndTran(SQL_HANDLE_DBC, self.dbc_h, SQL_ROLLBACK) if ret != SQL_SUCCESS: check_success(self, ret) - - - + + + def getinfo(self,infotype): if infotype not in list(aInfoTypes.keys()): - raise ProgrammingError('HY000','Invalid getinfo value: '+str(infotype)) - - + raise ProgrammingError('HY000','Invalid getinfo value: '+str(infotype)) + + if aInfoTypes[infotype] == 'GI_UINTEGER': total_buf_len = 1000 alloc_buffer = ctypes.c_ulong() @@ -2577,7 +2595,7 @@ class Connection: ADDR(used_buf_len)) check_success(self, ret) result = alloc_buffer.value - + elif aInfoTypes[infotype] == 'GI_USMALLINT': total_buf_len = 1000 alloc_buffer = ctypes.c_ushort() @@ -2607,25 +2625,25 @@ class Connection: result = True else: result = False - + return result - + def __exit__(self, type, value, traceback): if value: self.rollback() else: self.commit() - + if self.connected: self.close() - + def __enter__(self): return self def __del__(self): if self.connected: self.close() - + def close(self): if not self.connected: raise ProgrammingError('HY000','Attempt to close a closed connection.') @@ -2633,7 +2651,7 @@ class Connection: # if not cur is None: # if not cur.closed: # cur.close() - + if self.connected: #if DEBUG:print 'disconnect' if not self.autocommit: @@ -2648,7 +2666,7 @@ class Connection: # ret = ODBC_API.SQLFreeHandle(SQL_HANDLE_ENV, shared_env_h) # check_success(shared_env_h, ret) self.connected = 0 - + odbc = Connection connect = odbc ''' @@ -2665,7 +2683,7 @@ def drivers(): AllocateEnv() finally: lock.release() - + DriverDescription = create_buffer_u(1000) BufferLength1 = c_short(1000) DescriptionLength = c_short() @@ -2683,14 +2701,14 @@ def drivers(): if Direction == SQL_FETCH_FIRST: Direction = SQL_FETCH_NEXT return DriverList - + def win_create_mdb(mdb_path, sort_order = "General\0\0"): if sys.platform not in ('win32','cli'): raise Exception('This function is available for use in Windows only.') - + mdb_driver = [d for d in drivers() if 'Microsoft Access Driver (*.mdb' in d] if mdb_driver == []: raise Exception('Access Driver is not found.') @@ -2706,17 +2724,17 @@ def win_create_mdb(mdb_path, sort_order = "General\0\0"): else: c_Path = "CREATE_DB=" + mdb_path + " " + sort_order ODBC_ADD_SYS_DSN = 1 - - + + ret = ctypes.windll.ODBCCP32.SQLConfigDataSource(None,ODBC_ADD_SYS_DSN,driver_name, c_Path) if not ret: raise Exception('Failed to create Access mdb file - "%s". Please check file path, permission and Access driver readiness.' %mdb_path) - - + + def win_connect_mdb(mdb_path): if sys.platform not in ('win32','cli'): raise Exception('This function is available for use in Windows only.') - + mdb_driver = [d for d in drivers() if 'Microsoft Access Driver (*.mdb' in d] if mdb_driver == []: raise Exception('Access Driver is not found.') @@ -2724,20 +2742,20 @@ def win_connect_mdb(mdb_path): driver_name = mdb_driver[0] return connect('Driver={'+driver_name+"};DBQ="+mdb_path, unicode_results = use_unicode, readonly = False) - - - + + + def win_compact_mdb(mdb_path, compacted_mdb_path, sort_order = "General\0\0"): if sys.platform not in ('win32','cli'): raise Exception('This function is available for use in Windows only.') - - + + mdb_driver = [d for d in drivers() if 'Microsoft Access Driver (*.mdb' in d] if mdb_driver == []: raise Exception('Access Driver is not found.') else: driver_name = mdb_driver[0].encode('mbcs') - + #COMPACT_DB= ctypes.windll.ODBCCP32.SQLConfigDataSource.argtypes = [ctypes.c_void_p,ctypes.c_ushort,ctypes.c_char_p,ctypes.c_char_p] #driver_name = "Microsoft Access Driver (*.mdb)" @@ -2751,7 +2769,7 @@ def win_compact_mdb(mdb_path, compacted_mdb_path, sort_order = "General\0\0"): ret = ctypes.windll.ODBCCP32.SQLConfigDataSource(None,ODBC_ADD_SYS_DSN,driver_name, c_Path) if not ret: raise Exception('Failed to compact Access mdb file - "%s". Please check file path, permission and Access driver readiness.' %compacted_mdb_path) - + def dataSources(): """Return a list with [name, descrition]""" From 95e6e8577be889c6afe0f4331ba6eaf88ea27e25 Mon Sep 17 00:00:00 2001 From: gi0baro Date: Tue, 14 Apr 2015 15:25:26 +0200 Subject: [PATCH 006/115] Fixes #904 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index b08cb1f7..9272062b 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit b08cb1f779d1ef7973bf460344772778a344a077 +Subproject commit 9272062bf1f8ed91e47991712a795fe9d80cb92c From 0784680c90333ba6603cc658a6b19e65d51cf7dc Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 15 Apr 2015 23:55:44 +0200 Subject: [PATCH 007/115] added waitress to anyserver --- anyserver.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/anyserver.py b/anyserver.py index a7d7eec8..a500922a 100644 --- a/anyserver.py +++ b/anyserver.py @@ -180,6 +180,11 @@ class Servers: s = wsgi.WSGIServer(callable=app, bind="%s:%d" % address) s.start() + @staticmethod + def waitress(app, address, **options): + from waitress import serve + serve(app, host=address[0], port=address[1], _quiet=True) + def mongrel2_handler(application, conn, debug=False): """ From f33ccf3366bc19c80e2b156bd8b8896ef7650925 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 16 Apr 2015 16:53:12 -0500 Subject: [PATCH 008/115] experimental fix for represent --- gluon/packages/dal | 2 +- gluon/sqlhtml.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 9272062b..5d535747 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 9272062bf1f8ed91e47991712a795fe9d80cb92c +Subproject commit 5d5357476164782936b202a50deef86387164fe7 diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index cd57bbd3..03a9bf2f 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -58,9 +58,12 @@ def represent(field, value, record): f = field.represent if not callable(f): return str(value) - n = f.func_code.co_argcount - len(f.func_defaults or []) - if getattr(f, 'im_self', None): - n -= 1 + if hasattr(f,'func_code'): + n = f.func_code.co_argcount - len(f.func_defaults or []) + if getattr(f, 'im_self', None): + n -= 1 + else: + n = 1 if n == 1: return f(value) elif n == 2: From 33295e516fdfbec2513b5f1c898a38d56ff38513 Mon Sep 17 00:00:00 2001 From: Jeremie Dokime Date: Sat, 18 Apr 2015 02:15:45 +0200 Subject: [PATCH 009/115] Fix crash with list:reference field When using validate_and_update() or validate_and_insert() on a table with a list:reference field, the request crashes after timeout with: File "web2py/gluon/globals.py", line 270, in body raise HTTP(400, "Bad Request - HTTP body is incomplete") The request crashes because there is an hidden exception with the isinstance function: isinstance() arg 2 must be a class, type, or tuple of classes and types When no using GAE, the GoogleDatastoreAdapter variable is None, so isinstance crash. See the last line of pydal/adapters/__init__.py This is a regression intruduced after the v2.9.12. --- gluon/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/validators.py b/gluon/validators.py index d94f365d..5142d600 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -611,7 +611,7 @@ class IS_IN_DB(Validator): def count(values, s=self.dbset, f=field): return s(f.belongs(map(int, values))).count() - if isinstance(self.dbset.db._adapter, GoogleDatastoreAdapter): + if GoogleDatastoreAdapter is not None and isinstance(self.dbset.db._adapter, GoogleDatastoreAdapter): range_ids = range(0, len(values), 30) total = sum(count(values[i:i + 30]) for i in range_ids) if total == len(values): From 4bea52a7b5f74e69f7c320122080db597e431946 Mon Sep 17 00:00:00 2001 From: gi0baro Date: Sat, 18 Apr 2015 15:04:01 +0200 Subject: [PATCH 010/115] Fix serializers injection over new pydal --- gluon/dal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/dal.py b/gluon/dal.py index e7978e6b..a4f4a87d 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -14,7 +14,7 @@ from pydal import DAL as DAL from pydal import Field from pydal.objects import Row, Rows, Table, Query, Expression from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon -import copy_reg as copyreg + def _default_validators(db, field): """ @@ -81,12 +81,12 @@ def _default_validators(db, field): requires[0] = validators.IS_EMPTY_OR(requires[0]) return requires -from gluon import serializers as w2p_serializers +from gluon.serializers import custom_json, xml from gluon.utils import web2py_uuid from gluon import sqlhtml -DAL.serializers = w2p_serializers +DAL.serializers = {'json': custom_json, 'xml': xml} DAL.validators_method = _default_validators DAL.uuid = lambda x: web2py_uuid() DAL.representers = { From 537045082c4078ff515ce3686713535b695da58e Mon Sep 17 00:00:00 2001 From: gi0baro Date: Sat, 18 Apr 2015 15:08:39 +0200 Subject: [PATCH 011/115] Updated dal test due to new serializers --- gluon/tests/test_dal.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index ffbd6e15..35c9c9b9 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -15,10 +15,11 @@ from gluon.dal import DAL, Field class TestDALSubclass(unittest.TestCase): def testRun(self): - import gluon.serializers as mserializers + from gluon.serializers import custom_json, xml from gluon import sqlhtml db = DAL(check_reserved=['all']) - self.assertEqual(db.serializers, mserializers) + self.assertEqual(db.serializers['json'], custom_json) + self.assertEqual(db.serializers['xml'], xml) self.assertEqual(db.representers['rows_render'], sqlhtml.represent) self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE) From 435ebeaae44505385762ab9919e51ae987316705 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 18 Apr 2015 15:13:03 -0500 Subject: [PATCH 012/115] more consulting companies --- applications/examples/views/default/support.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/applications/examples/views/default/support.html b/applications/examples/views/default/support.html index a3b513a8..d2ee6614 100644 --- a/applications/examples/views/default/support.html +++ b/applications/examples/views/default/support.html @@ -33,6 +33,9 @@
  • LoadInfo (Bulgaria)
  • Applied Objects (New Zealand)
  • Sistemas Ágiles ("Agile Systems") (Argentina)
  • +
  • DefineScope (Portugal)
  • +
  • 10BioSystems
  • +
  • Dutveul (Netherlands)
  • From f7bf1020dfaf98ba13475f36b45e866d7472d734 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 18 Apr 2015 15:28:12 -0500 Subject: [PATCH 013/115] reverted gluon/packages/dal --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 5d535747..9272062b 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 5d5357476164782936b202a50deef86387164fe7 +Subproject commit 9272062bf1f8ed91e47991712a795fe9d80cb92c From ef8f802df9d9ea9355af2b250010868b03cca270 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 18 Apr 2015 15:44:04 -0500 Subject: [PATCH 014/115] R-2.10.4.beta --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0058b2a4..360a6b1a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.10.3-stable+timestamp.2015.04.02.16.28.49 +Version 2.10.4-beta+timestamp.2015.04.18.15.43.12 From 9915fdf093b73c6a9cce2a52d8711da0d5e71115 Mon Sep 17 00:00:00 2001 From: gi0baro Date: Sun, 19 Apr 2015 14:45:19 +0200 Subject: [PATCH 015/115] pydal -> track 15.03-maintenance (19/04/15) --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 9272062b..8a5e6c15 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 9272062bf1f8ed91e47991712a795fe9d80cb92c +Subproject commit 8a5e6c15eba3d6430edfd9cb562007c93b22e9fe From f3d815e84b96bcca9c91e586fa9550c589c877d2 Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 19 Apr 2015 15:51:17 +0200 Subject: [PATCH 016/115] added web.config to deploy web2py on IIS --- examples/web.config | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/web.config diff --git a/examples/web.config b/examples/web.config new file mode 100644 index 00000000..f6c094a8 --- /dev/null +++ b/examples/web.config @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 888fa3dfc841a2c09a65204d1ace91f6d0e2614d Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 19 Apr 2015 19:00:59 +0200 Subject: [PATCH 017/115] added setup script --- scripts/setup-web2py-ws2012r2.ps1 | 169 ++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 scripts/setup-web2py-ws2012r2.ps1 diff --git a/scripts/setup-web2py-ws2012r2.ps1 b/scripts/setup-web2py-ws2012r2.ps1 new file mode 100644 index 00000000..0a4df6a0 --- /dev/null +++ b/scripts/setup-web2py-ws2012r2.ps1 @@ -0,0 +1,169 @@ +"This script will work fine for a few cases 'by default':" +" - completely CLEAN WS2012R2 host" +" - python 2.7 installed in the default path" +" - wfasctgi installed on the default path" +"It'll install web2py under the default website " +" You can use it as a boilerplate to automate your deployments" +" but it still is released AS IT IS. " +"BIG FAT WARNING: It will install a bunch of dependecies +Inspect the source before executing it" +"" +"" +$ErrorActionPreference = 'stop' + +$REALLY_SURE = Read-Host "Do you want to start with web2py deployment? [y/N]" +if (!@('y', 'Y') -contains $REALLY_SURE) { + "Ok, Exiting without doing anything" + exit 1 +} +#setting root folder +$rootfolder = $pwd + +### utilities - start +function ask_a_question($question) { + $response = Read-Host "$question [Y/n]" + if (@('Y', 'y', '', $null) -contains $response) { + return $true + } else { + return $false + } +} + +function unzip_me { + #Load the assembly + [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null + #Unzip the file + [System.IO.Compression.ZipFile]::ExtractToDirectory($pathToZip, $targetDir) +} + + +### utilities - end + +#install 4.5 that is needed for a bunch of things anyway +Install-WindowsFeature Net-Framework-45-Core + +#fetch web2py +$web2py_url = 'http://www.web2py.com/examples/static/web2py_src.zip' +$web2py_file = "$pwd\web2py_src.zip" +if (!(Test-Path $web2py_file)) { + (new-object net.webclient).DownloadFile($web2py_url, $web2py_file) +} +#Load the assembly +[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null +#Unzip the file +[System.IO.Compression.ZipFile]::ExtractToDirectory($web2py_file, $pwd) + +#features installation (IIS, needed modules, python, chocolatey, etc) +$installfeatures = ask_a_question('Do you want to install needed features?') + +if ($installfeatures) { + Install-WindowsFeature Web-Server,Web-Default-Doc,Web-Static-Content,Web-Http-Redirect,Web-Http-Logging,Web-Request-Monitor,` + Web-Http-Tracing,Web-Stat-Compression,Web-Dyn-Compression,Web-Filtering,Web-Basic-Auth,Web-Windows-Auth,Web-AppInit,` + Web-CGI,Web-WebSockets,Web-Mgmt-Console,Web-Net-Ext45 +} + +$copy_web2py = ask_a_question("Copy web2py to the default website root?") +if ($copy_web2py) { + Import-Module WebAdministration + $available_websites = Get-Website + if ($available_websites[0] -eq $null) { + $default_one = $available_websites + } else { + $default_one = $available_websites[0] + } + $iis_root = [System.Environment]::ExpandEnvironmentVariables($default_one.PhysicalPath) + Copy-Item "$rootfolder\web2py\*" $iis_root -Recurse + $rootfolder = $iis_root + $acl = (Get-Item $rootfolder).GetAccessControl('Access') + $identity = "BUILTIN\IIS_IUSRS" + $fileSystemRights = "Modify" + $inheritanceFlags = "ContainerInherit, ObjectInherit" + $propagationFlags = "None" + $accessControlType = "Allow" + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fileSystemRights, $inheritanceFlags, $propagationFlags, $accessControlType) + $acl.SetAccessRule($rule) + Set-Acl $rootfolder $acl +} + +$create_cert = ask_a_question("Do you want to create a self-signed SSL cert?") +if ($create_cert) { + $cert = New-SelfSignedCertificate -DnsName ("localtest.me","*.localtest.me") -CertStoreLocation cert:\LocalMachine\My + $rootStore = Get-Item cert:\LocalMachine\Root + $rootStore.Open("ReadWrite") + $rootStore.Add($cert) + $rootStore.Close(); + Import-Module WebAdministration + Set-Location IIS:\SslBindings + New-WebBinding -Name "Default Web Site" -IP "*" -Port 443 -Protocol https + $cert | New-Item 0.0.0.0!443 + Set-Location $pwd +} + +"checking for chocolatey" +if (Get-Command "choco.exe" -ErrorAction SilentlyContinue) +{ + "chocolatey found" +} else { + "installing chocolatey" + (new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1') | iex +} +"installing url-rewrite" +choco install UrlRewrite +$pythonexe = Read-Host 'Python.exe path [C:\Python27\python.exe]' +if (($pythonexe -eq '') -or ($pythonexe -eq $null)) { + $pythonexe = 'C:\Python27\python.exe' +} +if (!(Test-Path $pythonexe)) { + "ERROR: python executable not found" + $pythonwanted = ask_a_question("do you want to install it automatically?") + + if ($pythonwanted) { + choco install webpicmd + WebpiCmd.exe /Install /Products:WFastCgi_21_279 + $pythonexe = 'C:\Python27\python.exe' + } + else { + exit 1 + } + +} +$wfastcgipath = Read-Host 'wfastcgi.py path [C:\Python27\Scripts\wfastcgi.py]' +if (($wfastcgipath -eq '') -or ($wfastcgipath -eq $null)) { + $wfastcgipath = 'C:\Python27\Scripts\wfastcgi.py' +} + +if (-not (Test-Path $wfastcgipath)) { + "ERROR: wfastcgi.py not found" + + $wfastcgiwanted = ask_a_question("do you want to install it automatically?") + if ($wfastcgiwanted) { + choco install webpicmd + WebpiCmd.exe /Install /Products:WFastCgi_21_279 + } else { + exit 1 + } +} +$pythondir = Split-Path c:\python27\python.exe +#installing dependencies +$env:Path = $env:Path + ";$pythondir;$pythondir\Scripts" + +pip install pypiwin32 + +$PW = Read-Host 'Web2py Admin Password' + +$appcmdpath = "$env:windir\system32\inetsrv\appcmd.exe" + +& $appcmdpath set config /section:system.webServer/fastCGI "/+[fullPath='$pythonexe', arguments='$wfastcgipath']" +& $appcmdpath unlock config -section:system.webServer/handlers + +& cd $rootfolder +& $pythonexe -c "from gluon.main import save_password; save_password('$PW',443)" + +$webconfig_template = Join-Path $rootfolder "examples\web.config" +$destination = Join-Path $rootfolder "web.config" +$scriptprocessor = 'scriptProcessor="{0}|{1}"' -f $pythonexe, $wfastcgipath + +(Get-Content $webconfig_template) | Foreach-Object {$_ -replace 'scriptProcessor="SCRIPT_PROCESSOR"', $scriptprocessor} | where {$_ -ne ""} | Set-Content $destination +"" +"Installation finished. Web2py is available either on http://localhost/ or at https://localtest.me/" +"" From ac80adc9b4506e74a610af0b0f316d9595203088 Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 19 Apr 2015 19:49:25 +0200 Subject: [PATCH 018/115] small typo. Fixes #920 --- applications/welcome/models/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/welcome/models/db.py b/applications/welcome/models/db.py index 3efe2377..27dcdf14 100644 --- a/applications/welcome/models/db.py +++ b/applications/welcome/models/db.py @@ -62,7 +62,7 @@ auth.define_tables(username=False, signature=False) ## configure email mail = auth.settings.mailer -mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.sender') +mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.server') mail.settings.sender = myconf.take('smtp.sender') mail.settings.login = myconf.take('smtp.login') From 65b4aaf842c3d838a103770ae8a6ecd36fe7044f Mon Sep 17 00:00:00 2001 From: gi0baro Date: Mon, 20 Apr 2015 17:29:22 +0200 Subject: [PATCH 019/115] pydal -> track 15.03-maintenance (20/04/15) --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 8a5e6c15..62eb7767 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 8a5e6c15eba3d6430edfd9cb562007c93b22e9fe +Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 From f3bda9ad0246f988707960888be78234bf40d2fa Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 20 Apr 2015 18:02:17 -0500 Subject: [PATCH 020/115] changed version --- Makefile | 2 +- VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a2a4fdec..642564ed 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.10.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.10.4-beta+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 360a6b1a..c24d27c1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.10.4-beta+timestamp.2015.04.18.15.43.12 +Version 2.10.4-beta+timestamp.2015.04.20.18.00.31 From 9f1edf267d9e732f4a31ce79747a64aa99da9393 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 21 Apr 2015 21:59:42 +0200 Subject: [PATCH 021/115] fixes #931 . Thanks @butsyk for spotting the bug --- gluon/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 6860a4bf..790c172e 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -5362,7 +5362,7 @@ class Expose(object): if current.request.raw_args: self.args = [arg for arg in current.request.raw_args.split('/') if arg] else: - self.args = [arg for arg in current.request.args if args] + self.args = [arg for arg in current.request.args if arg] filename = os.path.join(base, *self.args) if not os.path.exists(filename): raise HTTP(404, "FILE NOT FOUND") From 2b0bfba649e3a3ffac2469600696fc4a51de6e95 Mon Sep 17 00:00:00 2001 From: niphlod Date: Tue, 21 Apr 2015 23:59:07 +0200 Subject: [PATCH 022/115] extend underline for proper sphinx formatting --- gluon/dal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/dal.py b/gluon/dal.py index a4f4a87d..dbae0d39 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -7,7 +7,7 @@ | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) Takes care of adapting pyDAL to web2py's needs --------------------------------------------- +----------------------------------------------- """ from pydal import DAL as DAL From 77f154a56bb3cbfa28568f5a59dade063c846b07 Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 22 Apr 2015 00:05:05 +0200 Subject: [PATCH 023/115] added newer Recaptcha2 class to deal with v2.0. Fixes #919 Improvements over the "old" v1.0 - behaves well also without javascript - use_ssl is redundant, v2.0 works only in https mode - ajax is not useful anymore as the newer API is a lot easier Adjusted also the addrow() method that was missing newer formstyles. --- gluon/tools.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 147 insertions(+), 4 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 6860a4bf..0078d41a 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -60,7 +60,7 @@ except ImportError: # fallback to pure-Python module import gluon.contrib.simplejson as json_parser -__all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki', +__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] ### mind there are two loggers here (logger and crud.settings.logger)! @@ -965,7 +965,142 @@ class Recaptcha(DIV): return XML(captcha).xml() -# this should only be used for catcha and perhaps not even for that +class Recaptcha2(DIV): + """ + Experimental: + Creates a DIV holding the newer Recaptcha from Google (v2) + + Args: + request : the request. If not passed, uses current request + public_key : the public key Google gave you + private_key : the private key Google gave you + error_message : the error message to show if verification fails + label : the label to use + options (dict) : takes these parameters + + - hl + - theme + - type + - tabindex + - callback + - expired-callback + + see https://developers.google.com/recaptcha/docs/display for docs about those + + comment : the comment + + Examples: + Use as:: + + form = FORM(Recaptcha2(public_key='...',private_key='...')) + + or:: + + form = SQLFORM(...) + form.append(Recaptcha2(public_key='...',private_key='...')) + + to protect the login page instead, use:: + + from gluon.tools import Recaptcha2 + auth.settings.captcha = Recaptcha2(request, public_key='...',private_key='...') + + """ + + API_URI = 'https://www.google.com/recaptcha/api.js' + VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify' + + def __init__(self, + request=None, + public_key='', + private_key='', + error_message='invalid', + label='Verify:', + options=None, + comment='', + ): + 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.errors = Storage() + self.error_message = error_message + self.components = [] + self.attributes = {} + self.label = label + self.options = options or {} + self.comment = comment + + def _validate(self): + recaptcha_response_field = self.request_vars.pop('g-recaptcha-response', None) + remoteip = self.remote_addr + if not recaptcha_response_field: + self.errors['captcha'] = self.error_message + return False + params = urllib.urlencode({ + 'secret': self.private_key, + 'remoteip': remoteip, + '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) + content = httpresp.read() + httpresp.close() + try: + response_dict = json_parser.loads(content) + except: + self.errors['captcha'] = self.error_message + return False + if response_dict.get('success', False): + self.request_vars.captcha = '' + return True + else: + self.errors['captcha'] = self.error_message + return False + + def xml(self): + api_uri = self.API_URI + hl = self.options.pop('hl', None) + if hl: + api_uri = self.API_URI + '?hl=%s' % hl + public_key = self.public_key + self.options['sitekey'] = public_key + captcha = DIV( + SCRIPT(_src=api_uri, _async='', _defer=''), + DIV(_class="g-recaptcha", data=self.options), + TAG.noscript(XML(""" +
    +
    +
    + +
    +
    + +
    +
    +
    """ % dict(public_key=public_key)) + ) + ) + if not self.errors.captcha: + return XML(captcha).xml() + else: + captcha.append(DIV(self.errors['captcha'], _class='error')) + return XML(captcha).xml() + + +# this should only be used for captcha and perhaps not even for that def addrow(form, a, b, c, style, _id, position=-1): if style == "divs": form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'), @@ -987,6 +1122,15 @@ def addrow(form, a, b, c, style, _id, position=-1): DIV(b, SPAN(c, _class='inline-help'), _class='controls'), _class='control-group', _id=_id)) + elif style == "bootstrap3_inline": + form[0].insert(position, DIV(LABEL(a, _class='control-label col-sm-3'), + DIV(b, SPAN(c, _class='help-block'), + _class='col-sm-9'), + _class='form-group', _id=_id)) + elif style == "bootstrap3_stacked": + form[0].insert(position, DIV(LABEL(a, _class='control-label'), + b, SPAN(c, _class='help-block'), + _class='form-group', _id=_id)) else: form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'), TD(b, _class='w2p_fw'), @@ -1330,8 +1474,7 @@ class Auth(object): logged_url=URL(controller, function, args='profile'), download_url=URL(controller, 'download'), mailer=(mailer is True) and Mail() or mailer, - on_failed_authorization = - URL(controller, function, args='not_authorized'), + on_failed_authorization = URL(controller, function, args='not_authorized'), login_next = url_index, login_onvalidation = [], login_onaccept = [], From f42ee15f5f62847cad962dcdfa4e0270859b99fa Mon Sep 17 00:00:00 2001 From: Sean Morrison Date: Wed, 22 Apr 2015 19:00:21 -0500 Subject: [PATCH 024/115] add support for reporting web2py errors via slack.com --- scripts/tickets2slack.py | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100755 scripts/tickets2slack.py diff --git a/scripts/tickets2slack.py b/scripts/tickets2slack.py new file mode 100755 index 00000000..692dce2b --- /dev/null +++ b/scripts/tickets2slack.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Post error tickets to slack on a 5 minute schedule. +# +# Proper use depends on having created a web-hook through Slack, and having set +# that value in your app's model as the value of global_settings.slack_hook. +# Details on creating web-hooks can be found at https://slack.com/integrations +# +# requires the Requests module for posting to slack, other requirements are +# standard or provided by web2py +# +# Usage (on Unices), replace myapp with the name of your application and run: +# nohup python web2py.py -S myapp -M -R scripts/tickets2slack.py & + +import sys +import os +import time +import pickle +import json + +try: + import requests +except ImportError as e: + print "missing module 'Requests', aborting." + sys.exit(1) + +from gluon import URL +from gluon.utils import md5_hash +from gluon.restricted import RestrictedError +from gluon.settings import global_settings + + +path = os.path.join(request.folder, 'errors') +sent_errors_file = os.path.join(path, 'slack_errors.pickle') +hashes = {} +if os.path.exists(sent_errors_file): + try: + with open(sent_errors_file, 'rb') as f: + hashes = pickle.load(f) + except Exception as _: + pass + +# ## CONFIGURE HERE +SLEEP_MINUTES = 5 +ALLOW_DUPLICATES = False +global_settings.slack_hook = global_settings.slack_hook or \ + 'https://hooks.slack.com/services/your_service' +# ## END CONFIGURATION + +while 1: + for file_name in os.listdir(path): + if file_name == 'slack_errors.pickle': + continue + + if not ALLOW_DUPLICATES: + key = md5_hash(file_name) + if key in hashes: + continue + hashes[key] = 1 + + error = RestrictedError() + + try: + error.load(request, request.application, file_name) + except Exception as _: + continue # not an exception file? + + url = URL(a='admin', f='ticket', args=[request.application, file], + scheme=True) + payload = json.dumps(dict(text="Error in %(app)s.\n%(url)s" % + dict(app=request.application, url=url))) + + requests.post(global_settings.slack_hook, data=dict(payload=payload)) + + with open(sent_errors_file, 'wb') as f: + pickle.dump(hashes, f) + time.sleep(SLEEP_MINUTES * 60) From 0ad50630f288b16bf3655f5b1bc5c459032ede7f Mon Sep 17 00:00:00 2001 From: stephenrauch Date: Fri, 24 Apr 2015 11:02:18 -0700 Subject: [PATCH 025/115] Fix ldap_auth for NoSQL databases Unroll query with join to two queries when working with DB's which don't support joins. --- gluon/contrib/login_methods/ldap_auth.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index 65f2cfb9..aa0690f8 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -521,9 +521,17 @@ def ldap_auth(server='ldap', port=None, logging.error( 'There is no username or email for %s!' % username) raise - db_group_search = db((db.auth_membership.user_id == db_user_id) & - (db.auth_user.id == db.auth_membership.user_id) & - (db.auth_group.id == db.auth_membership.group_id)) + if not db.can_join(): + # no joins on NoSQL databases, perform two queries + db_group_search = db(db.auth_membership.user_id == db_user_id) + group_ids = [x.group_id for x in db_group_search.select( + db.auth_membership.group_id, distinct=True)] + db_group_search = db(db.auth_group.id == group_ids) + else: + db_group_search = db( + (db.auth_membership.user_id == db_user_id) & + (db.auth_user.id == db.auth_membership.user_id) & + (db.auth_group.id == db.auth_membership.group_id)) db_groups_of_the_user = list() db_group_id = dict() From e943aa9c250ad42adf100abc1ad95ded90d12f8a Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 08:39:47 -0500 Subject: [PATCH 026/115] minor compatibility fix in ldap_auth --- gluon/contrib/login_methods/ldap_auth.py | 16 +++++++++------- gluon/packages/dal | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index aa0690f8..4f64cedb 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -521,17 +521,19 @@ def ldap_auth(server='ldap', port=None, logging.error( 'There is no username or email for %s!' % username) raise - if not db.can_join(): - # no joins on NoSQL databases, perform two queries - db_group_search = db(db.auth_membership.user_id == db_user_id) - group_ids = [x.group_id for x in db_group_search.select( - db.auth_membership.group_id, distinct=True)] - db_group_search = db(db.auth_group.id == group_ids) - else: + # if old pydal version, assume this is a relational database which can do joins + db_can_join = db.can_join() if hasattr(db, 'can_join') else True + if db_can_join: db_group_search = db( (db.auth_membership.user_id == db_user_id) & (db.auth_user.id == db.auth_membership.user_id) & (db.auth_group.id == db.auth_membership.group_id)) + else: + # no joins on NoSQL databases, perform two queries + db_group_search = db(db.auth_membership.user_id == db_user_id) + group_ids = [x.group_id for x in db_group_search.select( + db.auth_membership.group_id, distinct=True)] + db_group_search = db(db.auth_group.id.belongs(group_ids)) db_groups_of_the_user = list() db_group_id = dict() diff --git a/gluon/packages/dal b/gluon/packages/dal index 62eb7767..6c86a5c8 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 +Subproject commit 6c86a5c83bc814be65fc0d6d3947951b6220aa17 From 520950ba74dc418edfd8ffa36d9568b587520a76 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 08:44:45 -0500 Subject: [PATCH 027/115] track track 15.03-maintenance (19/04/15) --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 6c86a5c8..9272062b 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 6c86a5c83bc814be65fc0d6d3947951b6220aa17 +Subproject commit 9272062bf1f8ed91e47991712a795fe9d80cb92c From 6612fd1cfeee73e0adce49479aa0068501cdce18 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 08:50:16 -0500 Subject: [PATCH 028/115] told git to track the right submodule branch --- .gitmodules | 2 ++ gluon/packages/dal | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 71525aff..2608f537 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,5 @@ [submodule "gluon/packages/dal"] path = gluon/packages/dal url = https://github.com/web2py/pydal.git +[submodule "gluon.packages.dal"] + branch = 15.03-maintenance diff --git a/gluon/packages/dal b/gluon/packages/dal index 9272062b..62eb7767 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 9272062bf1f8ed91e47991712a795fe9d80cb92c +Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 From 236dc4b9433c6fbea365854128c0f7650b7199f8 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 09:04:55 -0500 Subject: [PATCH 029/115] fixed submodule tracking --- .gitmodules | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 2608f537..71525aff 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,3 @@ [submodule "gluon/packages/dal"] path = gluon/packages/dal url = https://github.com/web2py/pydal.git -[submodule "gluon.packages.dal"] - branch = 15.03-maintenance From 58533954dc6f405f071d7f8f6432675afae69d0d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 09:07:07 -0500 Subject: [PATCH 030/115] R-2.10.4 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index c24d27c1..6b40673f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.10.4-beta+timestamp.2015.04.20.18.00.31 +Version 2.10.4-stable+timestamp.2015.04.26.09.05.21 From df039e734ca278d42659a40dc653162037fb1b35 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 10:10:18 -0500 Subject: [PATCH 031/115] 2.10.4 stable --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 642564ed..fbc3eb92 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.10.4-beta+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.10.4-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION ### rm -f all junk files make clean ### clean up baisc apps From 54b385b32192d9f97a001157ddb45bc9cd6f814c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 26 Apr 2015 17:16:19 -0500 Subject: [PATCH 032/115] grid(user_cursor=False) by default because it is broken --- gluon/sqlhtml.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 03a9bf2f..4b29a54c 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1967,7 +1967,8 @@ class SQLFORM(FORM): cache_count=None, client_side_delete=False, ignore_common_filters=None, - auto_pagination=True): + auto_pagination=True, + use_cursor=False): formstyle = formstyle or current.response.formstyle @@ -2542,7 +2543,7 @@ class SQLFORM(FORM): cursor = True # figure out what page we are one to setup the limitby - if paginate and dbset._db._adapter.dbengine == 'google:datastore': + if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: cursor = request.vars.cursor or True limitby = (0, paginate) try: @@ -2564,7 +2565,7 @@ class SQLFORM(FORM): table_fields = [field for field in fields if (field.tablename in tablenames and not(isinstance(field, Field.Virtual)))] - if dbset._db._adapter.dbengine == 'google:datastore': + if dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: rows = dbset.select(left=left, orderby=orderby, groupby=groupby, limitby=limitby, reusecursor=cursor, @@ -2574,6 +2575,7 @@ class SQLFORM(FORM): rows = dbset.select(left=left, orderby=orderby, groupby=groupby, limitby=limitby, cacheable=True, *table_fields) + next_cursor = None except SyntaxError: rows = None next_cursor = None @@ -2592,7 +2594,7 @@ class SQLFORM(FORM): console.append(DIV(message or '', _class='web2py_counter')) paginator = UL() - if paginate and dbset._db._adapter.dbengine == 'google:datastore': + if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: # this means we may have a large table with an unknown number of rows. try: page = int(request.vars.page or 1) - 1 From 9357d810d8c7c55e0dc4cb03b3dfabbe50f4342f Mon Sep 17 00:00:00 2001 From: cassiobotaro Date: Sat, 2 May 2015 20:24:04 -0300 Subject: [PATCH 033/115] Fix List behaviour and added new feature --- gluon/storage.py | 49 +++++++++++++++++-------------------- gluon/tests/test_storage.py | 4 +++ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/gluon/storage.py b/gluon/storage.py index 6996d805..cb0dae30 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -269,53 +269,50 @@ class FastStorage(dict): class List(list): + """ Like a regular python list but a[i] if i is out of bounds returns None - instead of `IndexOutOfBounds` + instead of `IndexOutOfBounds`. """ - def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): + def __call__(self, i, default=None, cast=None, otherwise=None): """Allows to use a special syntax for fast-check of `request.args()` validity - Args: i: index default: use this value if arg not found cast: type cast otherwise: can be: - - None: results in a 404 - str: redirect to this address - callable: calls the function (nothing is passed) - Example: You can use:: - request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise=lambda:...) - """ + value = self[i] or default + try: + if cast: + value = cast(value) + if not value and otherwise: + raise ValueError('Otherwise will raised.') + except (ValueError, TypeError): + if otherwise is None: + raise HTTP(404) + elif isinstance(otherwise, str): + redirect(otherwise) + elif callable(otherwise): + return otherwise() + else: + raise RuntimeError("invalid otherwise") + return value + + def __getitem__(self, i): n = len(self) if 0 <= i < n or -n <= i < 0: - value = self[i] - elif default is DEFAULT: - value = None - else: - value, cast = default, False - if cast: - try: - value = cast(value) - except (ValueError, TypeError): - from http import HTTP, redirect - if otherwise is None: - raise HTTP(404) - elif isinstance(otherwise, str): - redirect(otherwise) - elif callable(otherwise): - return otherwise() - else: - raise RuntimeError("invalid otherwise") - return value + return super(List, self).__getitem__(i) + return None if __name__ == '__main__': diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index e046506b..804a3fb8 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -125,8 +125,12 @@ class TestList(unittest.TestCase): def test_listcall(self): a = List((1, 2, 3)) self.assertEqual(a(1), 2) + self.assertEqual(a[1], 2) + self.assertEqual(a(3), None) + self.assertEqual(a[3], None) self.assertEqual(a(-1), 3) self.assertEqual(a(-5), None) + self.assertEqual(a[-5], None) self.assertEqual(a(-5, default='x'), 'x') self.assertEqual(a(-3, cast=str), '1') a.append('1234') From 258e2e57aeb20a7271a238a28933233e0e9086c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sat, 2 May 2015 20:36:35 -0300 Subject: [PATCH 034/115] Fix import errors --- gluon/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gluon/storage.py b/gluon/storage.py index cb0dae30..3b0780fd 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -298,6 +298,7 @@ class List(list): if not value and otherwise: raise ValueError('Otherwise will raised.') except (ValueError, TypeError): + from http import HTTP, redirect if otherwise is None: raise HTTP(404) elif isinstance(otherwise, str): From 8e3925820cd8f987665f0df3eb8e8d465c2de434 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 2 May 2015 23:16:05 -0500 Subject: [PATCH 035/115] allow disabling of confirmation in js delete --- applications/admin/static/js/web2py.js | 44 ++++++++++++----------- applications/examples/static/js/web2py.js | 44 ++++++++++++----------- applications/welcome/static/js/web2py.js | 14 ++++---- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 2e463733..257f87cd 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -490,12 +490,12 @@ * and prevent clicking on it */ disableElement: function(el) { el.addClass('disabled'); - var method = el.is('button') ? 'html' : 'val'; + var method = el.is('input') ? 'val' : 'html'; //method = el.attr('name') ? 'html' : 'val'; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; /*store enabled state if not already disabled */ - if(el.data('w2p:enable-with') === undefined) { - el.data('w2p:enable-with', el[method]()); + if(el.data('w2p_enable_with') === undefined) { + el.data('w2p_enable_with', el[method]()); } /*if you don't want to see "working..." on buttons, replace the following * two lines with this one @@ -515,11 +515,11 @@ /* restore element to its original state which was disabled by 'disableElement' above*/ enableElement: function(el) { - var method = el.is('button') ? 'val' : 'html'; - if(el.data('w2p:enable-with') !== undefined) { + var method = el.is('input') ? 'val' : 'html'; + if(el.data('w2p_enable_with') !== undefined) { /* set to old enabled state */ - el[method](el.data('w2p:enable-with')); - el.removeData('w2p:enable-with'); + el[method](el.data('w2p_enable_with')); + el.removeData('w2p_enable_with'); } el.removeClass('disabled'); el.unbind('click.w2pDisable'); @@ -586,12 +586,14 @@ if(pre_call != undefined) { eval(pre_call); } - if(confirm_message != undefined) { - if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; - if(!web2py.confirm(confirm_message)) { - web2py.stopEverything(e); - return; - } + if(confirm_message) { + if(confirm_message == 'default') + confirm_message = w2p_ajax_confirm_message || + 'Are you sure you want to delete this object?'; + if(!web2py.confirm(confirm_message)) { + web2py.stopEverything(e); + return; + } } if(target == undefined) { if(method == 'GET') { @@ -634,7 +636,7 @@ }); }, /* Disables form elements: - - Caches element value in 'w2p:enable-with' data store + - Caches element value in 'w2p_enable_with' data store - Replaces element text with value of 'data-disable-with' attribute - Sets disabled property to true */ @@ -646,8 +648,8 @@ if(disable_with == undefined) { element.data('w2p_disable_with', element[method]()) } - if(element.data('w2p:enable-with') === undefined) { - element.data('w2p:enable-with', element[method]()); + if(element.data('w2p_enable_with') === undefined) { + element.data('w2p_enable_with', element[method]()); } element[method](element.data('w2p_disable_with')); element.prop('disabled', true); @@ -655,16 +657,16 @@ }, /* Re-enables disabled form elements: - - Replaces element text with cached value from 'w2p:enable-with' data store (created in `disableFormElements`) + - Replaces element text with cached value from 'w2p_enable_with' data store (created in `disableFormElements`) - Sets disabled property to false */ enableFormElements: function(form) { form.find(web2py.enableSelector).each(function() { var element = $(this), method = element.is('button') ? 'html' : 'val'; - if(element.data('w2p:enable-with')) { - element[method](element.data('w2p:enable-with')); - element.removeData('w2p:enable-with'); + if(element.data('w2p_enable_with')) { + element[method](element.data('w2p_enable_with')); + element.removeData('w2p_enable_with'); } element.prop('disabled', false); }); @@ -730,4 +732,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 2e463733..257f87cd 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -490,12 +490,12 @@ * and prevent clicking on it */ disableElement: function(el) { el.addClass('disabled'); - var method = el.is('button') ? 'html' : 'val'; + var method = el.is('input') ? 'val' : 'html'; //method = el.attr('name') ? 'html' : 'val'; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; /*store enabled state if not already disabled */ - if(el.data('w2p:enable-with') === undefined) { - el.data('w2p:enable-with', el[method]()); + if(el.data('w2p_enable_with') === undefined) { + el.data('w2p_enable_with', el[method]()); } /*if you don't want to see "working..." on buttons, replace the following * two lines with this one @@ -515,11 +515,11 @@ /* restore element to its original state which was disabled by 'disableElement' above*/ enableElement: function(el) { - var method = el.is('button') ? 'val' : 'html'; - if(el.data('w2p:enable-with') !== undefined) { + var method = el.is('input') ? 'val' : 'html'; + if(el.data('w2p_enable_with') !== undefined) { /* set to old enabled state */ - el[method](el.data('w2p:enable-with')); - el.removeData('w2p:enable-with'); + el[method](el.data('w2p_enable_with')); + el.removeData('w2p_enable_with'); } el.removeClass('disabled'); el.unbind('click.w2pDisable'); @@ -586,12 +586,14 @@ if(pre_call != undefined) { eval(pre_call); } - if(confirm_message != undefined) { - if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; - if(!web2py.confirm(confirm_message)) { - web2py.stopEverything(e); - return; - } + if(confirm_message) { + if(confirm_message == 'default') + confirm_message = w2p_ajax_confirm_message || + 'Are you sure you want to delete this object?'; + if(!web2py.confirm(confirm_message)) { + web2py.stopEverything(e); + return; + } } if(target == undefined) { if(method == 'GET') { @@ -634,7 +636,7 @@ }); }, /* Disables form elements: - - Caches element value in 'w2p:enable-with' data store + - Caches element value in 'w2p_enable_with' data store - Replaces element text with value of 'data-disable-with' attribute - Sets disabled property to true */ @@ -646,8 +648,8 @@ if(disable_with == undefined) { element.data('w2p_disable_with', element[method]()) } - if(element.data('w2p:enable-with') === undefined) { - element.data('w2p:enable-with', element[method]()); + if(element.data('w2p_enable_with') === undefined) { + element.data('w2p_enable_with', element[method]()); } element[method](element.data('w2p_disable_with')); element.prop('disabled', true); @@ -655,16 +657,16 @@ }, /* Re-enables disabled form elements: - - Replaces element text with cached value from 'w2p:enable-with' data store (created in `disableFormElements`) + - Replaces element text with cached value from 'w2p_enable_with' data store (created in `disableFormElements`) - Sets disabled property to false */ enableFormElements: function(form) { form.find(web2py.enableSelector).each(function() { var element = $(this), method = element.is('button') ? 'html' : 'val'; - if(element.data('w2p:enable-with')) { - element[method](element.data('w2p:enable-with')); - element.removeData('w2p:enable-with'); + if(element.data('w2p_enable_with')) { + element[method](element.data('w2p_enable_with')); + element.removeData('w2p_enable_with'); } element.prop('disabled', false); }); @@ -730,4 +732,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/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index cd654b82..257f87cd 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -586,12 +586,14 @@ if(pre_call != undefined) { eval(pre_call); } - if(confirm_message != undefined) { - if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; - if(!web2py.confirm(confirm_message)) { - web2py.stopEverything(e); - return; - } + if(confirm_message) { + if(confirm_message == 'default') + confirm_message = w2p_ajax_confirm_message || + 'Are you sure you want to delete this object?'; + if(!web2py.confirm(confirm_message)) { + web2py.stopEverything(e); + return; + } } if(target == undefined) { if(method == 'GET') { From 302f56ecc110dd3f6d5b2af379a8d9ca9307004a Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 3 May 2015 15:33:19 +0200 Subject: [PATCH 036/115] more tests, general cleanup --- gluon/tests/test_cache.py | 35 ++++++++++++++++++++++++++++++++++- gluon/tests/test_dal.py | 17 ++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/gluon/tests/test_cache.py b/gluon/tests/test_cache.py index 1522c6f0..68dd743e 100644 --- a/gluon/tests/test_cache.py +++ b/gluon/tests/test_cache.py @@ -13,6 +13,7 @@ fix_sys_path(__file__) from storage import Storage from cache import CacheInRam, CacheOnDisk, Cache +from gluon.dal import DAL, Field oldcwd = None @@ -30,6 +31,11 @@ def tearDownModule(): if oldcwd: os.chdir(oldcwd) oldcwd = None + try: + os.unlink('dummy.db') + except: + pass + class TestCache(unittest.TestCase): @@ -107,7 +113,34 @@ class TestCache(unittest.TestCase): cache.clear(regex=r'a*') self.assertEqual(cache('a1', lambda: 2, 0), 2) self.assertEqual(cache('a2', lambda: 3, 100), 3) - return + + def testDALcache(self): + s = Storage({'application': 'admin', + 'folder': 'applications/admin'}) + cache = Cache(s) + db = DAL(check_reserved=['all']) + db.define_table('t_a', Field('f_a')) + db.t_a.insert(f_a='test') + db.commit() + a = db(db.t_a.id > 0).select(cache=(cache.ram, 60), cacheable=True) + b = db(db.t_a.id > 0).select(cache=(cache.ram, 60), cacheable=True) + self.assertEqual(a.as_csv(), b.as_csv()) + c = db(db.t_a.id > 0).select(cache=(cache.disk, 60), cacheable=True) + d = db(db.t_a.id > 0).select(cache=(cache.disk, 60), cacheable=True) + self.assertEqual(c.as_csv(), d.as_csv()) + self.assertEqual(a.as_csv(), c.as_csv()) + self.assertEqual(b.as_csv(), d.as_csv()) + e = db(db.t_a.id > 0).select(cache=(cache.disk, 60)) + f = db(db.t_a.id > 0).select(cache=(cache.disk, 60)) + self.assertEqual(e.as_csv(), f.as_csv()) + self.assertEqual(a.as_csv(), f.as_csv()) + g = db(db.t_a.id > 0).select(cache=(cache.ram, 60)) + h = db(db.t_a.id > 0).select(cache=(cache.ram, 60)) + self.assertEqual(g.as_csv(), h.as_csv()) + self.assertEqual(a.as_csv(), h.as_csv()) + db.t_a.drop() + db.close() + if __name__ == '__main__': setUpModule() # pre-python-2.7 diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 35c9c9b9..472750a9 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -4,6 +4,7 @@ Unit tests for gluon.dal """ +import os import unittest from fix_path import fix_sys_path @@ -12,8 +13,15 @@ fix_sys_path(__file__) from gluon.dal import DAL, Field +def tearDownModule(): + try: + os.unlink('dummy.db') + except: + pass + class TestDALSubclass(unittest.TestCase): + def testRun(self): from gluon.serializers import custom_json, xml from gluon import sqlhtml @@ -22,19 +30,26 @@ class TestDALSubclass(unittest.TestCase): self.assertEqual(db.serializers['xml'], xml) self.assertEqual(db.representers['rows_render'], sqlhtml.represent) self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE) + db.close() def testSerialization(self): import pickle db = DAL(check_reserved=['all']) db.define_table('t_a', Field('f_a')) db.t_a.insert(f_a='test') - a = db(db.t_a.id>0).select(cacheable=True) + a = db(db.t_a.id > 0).select(cacheable=True) s = pickle.dumps(a) b = pickle.loads(s) self.assertEqual(a.db, b.db) + db.t_a.drop() + db.close() """ TODO: class TestDefaultValidators(unittest.TestCase): def testRun(self): pass """ + +if __name__ == '__main__': + unittest.main() + tearDownModule() From 340d7b5e6f764c346115e996a15a46af9322e0a1 Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 3 May 2015 15:51:13 +0200 Subject: [PATCH 037/115] fixes #734 --- scripts/web2py.ubuntu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/web2py.ubuntu.sh b/scripts/web2py.ubuntu.sh index d3dc19ea..e6b2662b 100755 --- a/scripts/web2py.ubuntu.sh +++ b/scripts/web2py.ubuntu.sh @@ -62,7 +62,7 @@ do_start() start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \ && return 1 - start-stop-daemon --start --quiet --pidfile $PIDFILE \ + start-stop-daemon --start --quiet -m --pidfile $PIDFILE \ ${DAEMON_USER:+--chuid $DAEMON_USER} --chdir $DAEMON_DIR \ --background --exec $DAEMON -- $DAEMON_ARGS \ || return 2 From 32b9b5c799044cedcac8498c782e977bf0ea5aa6 Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 3 May 2015 16:06:10 +0200 Subject: [PATCH 038/115] fixes issue #691 --- applications/welcome/routes.example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/welcome/routes.example.py b/applications/welcome/routes.example.py index 95130e18..7a3e73d1 100644 --- a/applications/welcome/routes.example.py +++ b/applications/welcome/routes.example.py @@ -7,11 +7,11 @@ # Language from default.py or 'en' (if the file is not found) is used as # a default_language # -# See /router.example.py for parameter's detail +# See /examples/routes.parametric.example.py for parameter's detail #------------------------------------------------------------------------------------- # To enable this route file you must do the steps: # -# 1. rename /router.example.py to routes.py +# 1. rename /examples/routes.parametric.example.py to routes.py # 2. rename this APP/routes.example.py to APP/routes.py # (where APP - is your application directory) # 3. restart web2py (or reload routes in web2py admin interfase) From 1394942feb43e31ef2273dcff54580a11c888b27 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 3 May 2015 10:09:07 -0500 Subject: [PATCH 039/115] removed reference to python 2.5 --- applications/examples/views/default/download.html | 2 +- gluon/packages/dal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/examples/views/default/download.html b/applications/examples/views/default/download.html index 7e4c6348..139dccd4 100644 --- a/applications/examples/views/default/download.html +++ b/applications/examples/views/default/download.html @@ -36,7 +36,7 @@

    - The source code version works on all supported platforms, including Linux, but it requires Python 2.5, 2.6, or 2.7. + The source code version works on all supported platforms, including Linux, but it requires Python 2.6, or 2.7 (recommended). It runs on Windows and most Unix systems, including Linux and BSD.

    diff --git a/gluon/packages/dal b/gluon/packages/dal index 62eb7767..cda69216 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 +Subproject commit cda69216d45b3be312f7a91f48db6837801357dc From 1bb4117cbdd3ffa3bc7f1c7e49b8fc236aac77ba Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 3 May 2015 10:23:08 -0500 Subject: [PATCH 040/115] Merge pull request #950 from cassiobotaro/master Fix List behaviour and added new feature --- gluon/storage.py | 50 ++++++++++++++++++------------------- gluon/tests/test_storage.py | 4 +++ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/gluon/storage.py b/gluon/storage.py index 6996d805..3b0780fd 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -269,53 +269,51 @@ class FastStorage(dict): class List(list): + """ Like a regular python list but a[i] if i is out of bounds returns None - instead of `IndexOutOfBounds` + instead of `IndexOutOfBounds`. """ - def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): + def __call__(self, i, default=None, cast=None, otherwise=None): """Allows to use a special syntax for fast-check of `request.args()` validity - Args: i: index default: use this value if arg not found cast: type cast otherwise: can be: - - None: results in a 404 - str: redirect to this address - callable: calls the function (nothing is passed) - Example: You can use:: - request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise=lambda:...) - """ + value = self[i] or default + try: + if cast: + value = cast(value) + if not value and otherwise: + raise ValueError('Otherwise will raised.') + except (ValueError, TypeError): + from http import HTTP, redirect + if otherwise is None: + raise HTTP(404) + elif isinstance(otherwise, str): + redirect(otherwise) + elif callable(otherwise): + return otherwise() + else: + raise RuntimeError("invalid otherwise") + return value + + def __getitem__(self, i): n = len(self) if 0 <= i < n or -n <= i < 0: - value = self[i] - elif default is DEFAULT: - value = None - else: - value, cast = default, False - if cast: - try: - value = cast(value) - except (ValueError, TypeError): - from http import HTTP, redirect - if otherwise is None: - raise HTTP(404) - elif isinstance(otherwise, str): - redirect(otherwise) - elif callable(otherwise): - return otherwise() - else: - raise RuntimeError("invalid otherwise") - return value + return super(List, self).__getitem__(i) + return None if __name__ == '__main__': diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index e046506b..804a3fb8 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -125,8 +125,12 @@ class TestList(unittest.TestCase): def test_listcall(self): a = List((1, 2, 3)) self.assertEqual(a(1), 2) + self.assertEqual(a[1], 2) + self.assertEqual(a(3), None) + self.assertEqual(a[3], None) self.assertEqual(a(-1), 3) self.assertEqual(a(-5), None) + self.assertEqual(a[-5], None) self.assertEqual(a(-5, default='x'), 'x') self.assertEqual(a(-3, cast=str), '1') a.append('1234') From 9d873cbd1ca04aec7c060f3903884764cc00813e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 3 May 2015 10:30:43 -0500 Subject: [PATCH 041/115] reverted last commit --- gluon/storage.py | 50 +++++++++++++++++++------------------ gluon/tests/test_storage.py | 4 --- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/gluon/storage.py b/gluon/storage.py index 3b0780fd..6996d805 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -269,51 +269,53 @@ class FastStorage(dict): class List(list): - """ Like a regular python list but a[i] if i is out of bounds returns None - instead of `IndexOutOfBounds`. + instead of `IndexOutOfBounds` """ - def __call__(self, i, default=None, cast=None, otherwise=None): + def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): """Allows to use a special syntax for fast-check of `request.args()` validity + Args: i: index default: use this value if arg not found cast: type cast otherwise: can be: + - None: results in a 404 - str: redirect to this address - callable: calls the function (nothing is passed) + Example: You can use:: + request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise=lambda:...) - """ - value = self[i] or default - try: - if cast: - value = cast(value) - if not value and otherwise: - raise ValueError('Otherwise will raised.') - except (ValueError, TypeError): - from http import HTTP, redirect - if otherwise is None: - raise HTTP(404) - elif isinstance(otherwise, str): - redirect(otherwise) - elif callable(otherwise): - return otherwise() - else: - raise RuntimeError("invalid otherwise") - return value - def __getitem__(self, i): + """ n = len(self) if 0 <= i < n or -n <= i < 0: - return super(List, self).__getitem__(i) - return None + value = self[i] + elif default is DEFAULT: + value = None + else: + value, cast = default, False + if cast: + try: + value = cast(value) + except (ValueError, TypeError): + from http import HTTP, redirect + if otherwise is None: + raise HTTP(404) + elif isinstance(otherwise, str): + redirect(otherwise) + elif callable(otherwise): + return otherwise() + else: + raise RuntimeError("invalid otherwise") + return value if __name__ == '__main__': diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 804a3fb8..e046506b 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -125,12 +125,8 @@ class TestList(unittest.TestCase): def test_listcall(self): a = List((1, 2, 3)) self.assertEqual(a(1), 2) - self.assertEqual(a[1], 2) - self.assertEqual(a(3), None) - self.assertEqual(a[3], None) self.assertEqual(a(-1), 3) self.assertEqual(a(-5), None) - self.assertEqual(a[-5], None) self.assertEqual(a(-5, default='x'), 'x') self.assertEqual(a(-3, cast=str), '1') a.append('1234') From 71b02e3044d1a686a58f895d8a0431104b2c1d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sun, 3 May 2015 13:35:05 -0300 Subject: [PATCH 042/115] Maintain backward compatibility Little change to maintain backward compatibility, related to #590 --- gluon/storage.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gluon/storage.py b/gluon/storage.py index 3b0780fd..117ccac1 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -275,7 +275,7 @@ class List(list): instead of `IndexOutOfBounds`. """ - def __call__(self, i, default=None, cast=None, otherwise=None): + def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): """Allows to use a special syntax for fast-check of `request.args()` validity Args: @@ -291,7 +291,9 @@ class List(list): request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise=lambda:...) """ - value = self[i] or default + value = self[i] + if not value and default is not DEFAULT: + value, cast = default, False try: if cast: value = cast(value) From ccc4b96709edef936f29cf51c2b199fda1b93e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sun, 3 May 2015 13:36:24 -0300 Subject: [PATCH 043/115] Added one more test Added one more test to avoid mistake with backward compatibility --- gluon/tests/test_storage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 804a3fb8..328f1e09 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -138,6 +138,8 @@ class TestList(unittest.TestCase): self.assertEqual(a(3, cast=int), 1234) a.append('x') self.assertRaises(HTTP, a, 4, cast=int) + b = List() + self.assertEqual(b(0, cast=int, default=None), None) if __name__ == '__main__': From f6db7c995f162c50b20b7cf857b543c37f5d3848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sun, 3 May 2015 14:02:11 -0300 Subject: [PATCH 044/115] Its necessary because of default=None trick --- gluon/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/storage.py b/gluon/storage.py index 117ccac1..4a1e46a7 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -293,7 +293,7 @@ class List(list): """ value = self[i] if not value and default is not DEFAULT: - value, cast = default, False + value, cast, otherwise = default, False, False try: if cast: value = cast(value) From c36c39178610867330ebf1927a2a3edeb7dcb35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Sun, 3 May 2015 14:03:26 -0300 Subject: [PATCH 045/115] More test to prove backward compatibility --- gluon/tests/test_storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 328f1e09..e518e4d2 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -140,6 +140,7 @@ class TestList(unittest.TestCase): self.assertRaises(HTTP, a, 4, cast=int) b = List() self.assertEqual(b(0, cast=int, default=None), None) + self.assertEqual(b(0, cast=int, default=None, otherwise='something'), None) if __name__ == '__main__': From 380b4917241acf850ec837702e0ab75c591edb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Mon, 4 May 2015 12:41:06 -0300 Subject: [PATCH 046/115] Return old behaviours - Better documented List - Otherwise not binded with cast --- gluon/storage.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/gluon/storage.py b/gluon/storage.py index 4a1e46a7..eebec539 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -22,7 +22,8 @@ import gluon.portalocker as portalocker __all__ = ['List', 'Storage', 'Settings', 'Messages', 'StorageList', 'load_storage', 'save_storage'] -DEFAULT = lambda: 0 +def DEFAULT(): + return 0 class Storage(dict): @@ -271,28 +272,38 @@ class FastStorage(dict): class List(list): """ - Like a regular python list but a[i] if i is out of bounds returns None - instead of `IndexOutOfBounds`. + Like a regular python list but callable. + When a(i) is called if i is out of bounds returns None + instead of `IndexError`. """ def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): - """Allows to use a special syntax for fast-check of `request.args()` - validity - Args: + """Allows to use a special syntax for fast-check of + `request.args()` validity. + :params: i: index default: use this value if arg not found cast: type cast - otherwise: can be: - - None: results in a 404 - - str: redirect to this address - - callable: calls the function (nothing is passed) + otherwise: + will be executed when: + - casts fail + - value not found, dont have default and otherwise is + especified + can be: + - None: results in a 404 + - str: redirect to this address + - callable: calls the function (nothing is passed) Example: You can use:: request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise=lambda:...) """ - value = self[i] - if not value and default is not DEFAULT: + n = len(self) + if 0 <= i < n or -n <= i < 0: + value = self[i] + elif default is DEFAULT: + value = None + else: value, cast, otherwise = default, False, False try: if cast: @@ -311,13 +322,6 @@ class List(list): raise RuntimeError("invalid otherwise") return value - def __getitem__(self, i): - n = len(self) - if 0 <= i < n or -n <= i < 0: - return super(List, self).__getitem__(i) - return None - - if __name__ == '__main__': import doctest doctest.testmod() From a0ee64988439a4c80c27c4843841936c4bba5ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Mon, 4 May 2015 12:49:14 -0300 Subject: [PATCH 047/115] Update tests - new tests to verify old behaviour - test otherwise without cast and defaut - verify if behave like a list - more test with call function --- gluon/tests/test_storage.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index e518e4d2..810b5b63 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -120,17 +120,14 @@ class TestStorageList(unittest.TestCase): class TestList(unittest.TestCase): + """ Tests Storage.List (fast-check for request.args()) """ def test_listcall(self): a = List((1, 2, 3)) self.assertEqual(a(1), 2) - self.assertEqual(a[1], 2) - self.assertEqual(a(3), None) - self.assertEqual(a[3], None) self.assertEqual(a(-1), 3) self.assertEqual(a(-5), None) - self.assertEqual(a[-5], None) self.assertEqual(a(-5, default='x'), 'x') self.assertEqual(a(-3, cast=str), '1') a.append('1234') @@ -139,8 +136,24 @@ class TestList(unittest.TestCase): a.append('x') self.assertRaises(HTTP, a, 4, cast=int) b = List() + # default is always returned when especified self.assertEqual(b(0, cast=int, default=None), None) - self.assertEqual(b(0, cast=int, default=None, otherwise='something'), None) + self.assertEqual(b(0, cast=int, default=None, otherwise='teste'), None) + self.assertEqual(b(0, cast=int, default='a', otherwise='teste'), 'a') + # if don't have value and otherwise is especified it will called + self.assertEqual(b(0, otherwise=lambda: 'something'), 'something') + self.assertEqual(b(0, cast=int, otherwise=lambda: 'something'), + 'something') + # except if default is especified + self.assertEqual(b(0, default=0, otherwise=lambda: 'something'), 0) + + def test_listgetitem(self): + '''Mantains list behaviour.''' + a = List((1, 2, 3)) + self.assertEqual(a[0], 1) + with self.assertRaises(IndexError): + a[3] + self.assertEqual(a[::-1], [3, 2, 1]) if __name__ == '__main__': From cdca2793e0d66c631c7669f5d5e3aa5869e8f428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Mon, 4 May 2015 12:55:44 -0300 Subject: [PATCH 048/115] Maintain py2.6k compability --- gluon/tests/test_storage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gluon/tests/test_storage.py b/gluon/tests/test_storage.py index 810b5b63..ca047e95 100644 --- a/gluon/tests/test_storage.py +++ b/gluon/tests/test_storage.py @@ -151,8 +151,6 @@ class TestList(unittest.TestCase): '''Mantains list behaviour.''' a = List((1, 2, 3)) self.assertEqual(a[0], 1) - with self.assertRaises(IndexError): - a[3] self.assertEqual(a[::-1], [3, 2, 1]) From 6e2f9ad043db3c032f78deb1d5c0aaba8893d581 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 4 May 2015 11:23:44 -0500 Subject: [PATCH 049/115] fixed examples support --- .../private/content/en/default/documentation/more.markmin | 1 + applications/examples/views/default/support.html | 1 + 2 files changed, 2 insertions(+) diff --git a/applications/examples/private/content/en/default/documentation/more.markmin b/applications/examples/private/content/en/default/documentation/more.markmin index 58dd1ebf..dca22f11 100644 --- a/applications/examples/private/content/en/default/documentation/more.markmin +++ b/applications/examples/private/content/en/default/documentation/more.markmin @@ -22,5 +22,6 @@ - [[More Plugins http://dev.s-cubism.com/web2py_plugins]] - [[Appliances http://www.web2py.com/appliances popup]] - [[web2py utils http://packages.python.org/web2py_utils/ popup]] +- [[Sublime text 3 plugin https://bitbucket.org/kfog/w2p popup]] #### [[Sites Powered by web2py http://www.web2py.com/poweredby popup]] diff --git a/applications/examples/views/default/support.html b/applications/examples/views/default/support.html index d2ee6614..f8cc6d59 100644 --- a/applications/examples/views/default/support.html +++ b/applications/examples/views/default/support.html @@ -30,6 +30,7 @@
  • Emotionull (Greece and Cyprus)
  • VSA Services (Singapore)
  • Albendas (Spain)
  • +
  • Corebyte (Netherland)
  • LoadInfo (Bulgaria)
  • Applied Objects (New Zealand)
  • Sistemas Ágiles ("Agile Systems") (Argentina)
  • From 6f91fdd8332beb5e6a17a1444655e9b9f22e2f4c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 6 May 2015 08:56:56 -0500 Subject: [PATCH 050/115] minor refactoring in grid(orderby) --- gluon/packages/dal | 2 +- gluon/sqlhtml.py | 21 +++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index cda69216..04ffbb37 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit cda69216d45b3be312f7a91f48db6837801357dc +Subproject commit 04ffbb371c59bfe253cae7865670c132b9b7eb44 diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 4b29a54c..5ebc4fb1 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2070,18 +2070,15 @@ class SQLFORM(FORM): # is unique and usually indexed. See issue #679 if not orderby: orderby = field_id - else: - if isinstance(orderby, Expression): - if orderby.first: - # here we're with a DESC order on a field - # stored as orderby.first - if orderby.first is not field_id: - orderby = orderby | field_id - else: - # here we're with an ASC order on a field - # stored as orderby - if orderby is not field_id: - orderby = orderby | field_id + elif isinstance(orderby, list): + orderby = map(lambda a,b: a|b, orderby) + elif isinstance(orderby, Field) and orderby is not field_id: + # here we're with an ASC order on a field stored as orderby + orderby = orderby | field_id + elif (isinstance(orderby, Expression) and + orderby.first and orderby.first is not field_id): + # here we're with a DESC order on a field stored as orderby.first + orderby = orderby | field_id return orderby def url(**b): From 587ff56a94fb774609a474ee82c8166a05f31904 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 7 May 2015 22:26:42 -0500 Subject: [PATCH 051/115] linked 15.03-maintenance again --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 04ffbb37..62eb7767 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 04ffbb371c59bfe253cae7865670c132b9b7eb44 +Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 From 5c167907eb4c4c3864b1e6480d7b2002e4fe9b5c Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 8 May 2015 21:51:56 +0200 Subject: [PATCH 052/115] fixes #628 response.include_files is now cleaner and easier to maintain You can specify a tuple of (type, url) to include external assets without extension (such as the usecase described in #628) Added tests for include_files, that was never included in CI tests --- gluon/globals.py | 63 ++++++++++-------- gluon/tests/__init__.py | 1 + gluon/tests/test_globals.py | 124 ++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 gluon/tests/test_globals.py diff --git a/gluon/globals.py b/gluon/globals.py index 1a3ebb6e..05f94994 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -26,6 +26,8 @@ import gluon.settings as settings from gluon.utils import web2py_uuid, secure_dumps, secure_loads from gluon.settings import global_settings from gluon import recfile +from gluon.cache import CacheInRam +from gluon.fileutils import copystream import hashlib import portalocker try: @@ -47,8 +49,7 @@ import cgi import urlparse import copy import tempfile -from gluon.cache import CacheInRam -from gluon.fileutils import copystream + FMT = '%a, %d-%b-%Y %H:%M:%S PST' PAST = 'Sat, 1-Jan-1971 00:00:00' @@ -82,13 +83,22 @@ less_template = '' css_inline = '' js_inline = '' +template_mapping = { + 'css': css_template, + 'js': js_template, + 'coffee': coffee_template, + 'ts': typescript_template, + 'less': less_template, + 'css:inline': css_inline, + 'js:inline': js_inline +} # IMPORTANT: # this is required so that pickled dict(s) and class.__dict__ # are sorted and web2py can detect without ambiguity when a session changes class SortingPickler(Pickler): def save_dict(self, obj): - self.write(EMPTY_DICT if self.bin else MARK+DICT) + self.write(EMPTY_DICT if self.bin else MARK + DICT) self.memoize(obj) self._batch_setitems([(key, obj[key]) for key in sorted(obj)]) @@ -275,7 +285,7 @@ class Request(Storage): """ self._vars = copy.copy(self.get_vars) for key, value in self.post_vars.iteritems(): - if not key in self._vars: + if key not in self._vars: self._vars[key] = value else: if not isinstance(self._vars[key], list): @@ -436,19 +446,20 @@ class Response(Storage): return page def include_meta(self): - s = "\n"; + s = "\n" for meta in (self.meta or {}).iteritems(): k, v = meta - if isinstance(v,dict): - s = s+'\n' + if isinstance(v, dict): + s += '\n' else: - s = s+'\n' % (k, xmlescape(v)) + s += '\n' % (k, xmlescape(v)) self.write(s, escape=False) def include_files(self, extensions=None): """ - Caching method for writing out files. + Includes files (usually in the head). + Can minify and cache local files By default, caches in ram for 5 minutes. To change, response.cache_includes = (cache_method, time_expire). Example: (cache.disk, 60) # caches to disk for 1 minute. @@ -456,9 +467,13 @@ class Response(Storage): from gluon import URL files = [] + ext_files = [] has_js = has_css = False for item in self.files: - if extensions and not item.split('.')[-1] in extensions: + if isinstance(item, (list, tuple)): + ext_files.append(item) + continue + if extensions and not item.rpartition('.')[2] in extensions: continue if item in files: continue @@ -487,10 +502,13 @@ class Response(Storage): time_expire) else: files = call_minify() - s = '' + + files.extend(ext_files) + s = [] for item in files: if isinstance(item, str): f = item.lower().split('?')[0] + ext = f.rpartition('.')[2] # if static_version we need also to check for # static_version_urls. In that case, the _.x.x.x # bit would have already been added by the URL() @@ -498,24 +516,15 @@ class Response(Storage): if self.static_version and not self.static_version_urls: item = item.replace( '/static/', '/static/_%s/' % self.static_version, 1) - if f.endswith('.css'): - s += css_template % item - elif f.endswith('.js'): - s += js_template % item - elif f.endswith('.coffee'): - s += coffee_template % item - elif f.endswith('.ts'): - # http://www.typescriptlang.org/ - s += typescript_template % item - elif f.endswith('.less'): - s += less_template % item + tmpl = template_mapping.get(ext) + if tmpl: + s.append(tmpl % item) elif isinstance(item, (list, tuple)): f = item[0] - if f == 'css:inline': - s += css_inline % item[1] - elif f == 'js:inline': - s += js_inline % item[1] - self.write(s, escape=False) + tmpl = template_mapping.get(f) + if tmpl: + s.append(tmpl % item[1]) + self.write(''.join(s), escape=False) def stream(self, stream, diff --git a/gluon/tests/__init__.py b/gluon/tests/__init__.py index 03e67935..64a104d3 100644 --- a/gluon/tests/__init__.py +++ b/gluon/tests/__init__.py @@ -4,6 +4,7 @@ from test_http import * from test_cache import * from test_contenttype import * from test_fileutils import * +from test_globals import * from test_html import * from test_is_url import * from test_languages import * diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py new file mode 100644 index 00000000..5f9d5526 --- /dev/null +++ b/gluon/tests/test_globals.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + Unit tests for gluon.globals +""" + + +import unittest +from fix_path import fix_sys_path + +fix_sys_path(__file__) + +from gluon.globals import Response +from gluon import URL + + +class testResponse(unittest.TestCase): + + def test_include_files(self): + + def return_includes(response, extensions=None): + response.include_files(extensions) + return response.body.getvalue() + + response = Response() + response.files.append(URL('a', 'static', 'css/file.css')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(URL('a', 'static', 'css/file.js')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(URL('a', 'static', 'css/file.coffee')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(URL('a', 'static', 'css/file.ts')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(URL('a', 'static', 'css/file.less')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(('css:inline', 'background-color; white;')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(('js:inline', 'alert("hello")')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append('https://code.jquery.com/jquery-1.11.3.min.js') + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0') + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0') + response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0') + response.files.append(URL('a', 'static', 'css/file.css')) + response.files.append(URL('a', 'static', 'css/file.css')) + content = return_includes(response) + self.assertEqual(content, + '' + + '') + + response = Response() + response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false')) + response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0') + response.files.append(URL('a', 'static', 'css/file.css')) + response.files.append(URL('a', 'static', 'css/file.ts')) + content = return_includes(response) + self.assertEqual(content, + '' + + '' + + '' + + '' + ) + + + response = Response() + response.files.append(URL('a', 'static', 'css/file.js')) + response.files.append(URL('a', 'static', 'css/file.css')) + content = return_includes(response, extensions=['css']) + self.assertEqual(content, '') + + #regr test for #628 + response = Response() + response.files.append('http://maps.google.com/maps/api/js?sensor=false') + content = return_includes(response) + self.assertEqual(content, '') + + #regr test for #628 + response = Response() + response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false')) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(['js', 'http://maps.google.com/maps/api/js?sensor=false']) + content = return_includes(response) + self.assertEqual(content, '') + + response = Response() + response.files.append(('js1', 'http://maps.google.com/maps/api/js?sensor=false')) + content = return_includes(response) + self.assertEqual(content, '') + +if __name__ == '__main__': + unittest.main() From a6226d63912be69da1312d3cb1f0f6ebcf5bff40 Mon Sep 17 00:00:00 2001 From: Mathieu Clabaut Date: Sat, 9 May 2015 17:59:18 +0200 Subject: [PATCH 053/115] Convert subject and body to unicode before sending mail Not doing this was raising an exception : Traceback (most recent call last): File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/restricted.py, line 227, in restricted exec ccode in environment File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/applications/foundit/controllers/default.py, line 127, in File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/globals.py, line 393, in self._caller = lambda f: f() File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/applications/foundit/controllers/default.py, line 34, in user return dict(form=auth()) File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 1595, in __call__ return getattr(self, args[0])() File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 3272, in request_reset_password if self.email_reset_password(user): File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 3296, in email_reset_password message=self.messages.reset_password % d): File /base/data/home/apps/e~sacred-bonus-88417/1.384178859090314065/gluon/tools.py, line 798, in send subject=subject, body=text, **xcc) File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 402, in send_mail message.send(make_sync_call=make_sync_call) File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 1108, in send message = self.ToProto() File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 1350, in ToProto message = super(EmailMessage, self).ToProto() File /base/data/home/runtimes/python27/python27_lib/versions/1/google/appengine/api/mail.py, line 1046, in ToProto message.set_subject(_to_str(self.subject)) File cpp_message.pyx, line 124, in cpp_message.SetScalarAccessors.Setter (third_party/apphosting/python/protobuf/proto1/cpp_message.cc:2229) TypeError: has type , but expected one of: str, unicode --- gluon/tools.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 6d6c42fe..71abdc76 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -767,8 +767,8 @@ class Mail(object): if self.settings.server == 'logging': logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % ('-' * 40, sender, - ', '.join(to), subject, - text or html, '-' * 40)) + ', '.join(to), subject, + text or html, '-' * 40)) elif self.settings.server == 'gae': xcc = dict() if cc: @@ -779,23 +779,23 @@ class Mail(object): xcc['reply_to'] = reply_to from google.appengine.api import mail attachments = attachments and [mail.Attachment( - a.my_filename, + a.my_filename, a.my_payload, contebt_id='' % k ) for k,a in enumerate(attachments) if not raw] if attachments: result = mail.send_mail( sender=sender, to=origTo, - subject=subject, body=text, html=html, + subject=unicode(subject), body=unicode(text), html=html, attachments=attachments, **xcc) elif html and (not raw): result = mail.send_mail( sender=sender, to=origTo, - subject=subject, body=text, html=html, **xcc) + subject=unicode(subject), body=unicode(text), html=html, **xcc) else: result = mail.send_mail( sender=sender, to=origTo, - subject=subject, body=text, **xcc) + subject=unicode(subject), body=unicode(text), **xcc) else: smtp_args = self.settings.server.split(':') kwargs = dict(timeout=self.settings.timeout) @@ -3714,7 +3714,7 @@ class Auth(object): return record.id else: id = membership.insert(group_id=group_id, user_id=user_id) - if role: + if role: self.user_groups[group_id] = role else: self.update_groups() From cadf38b4f65c3110ee375824936e7a2fc577e775 Mon Sep 17 00:00:00 2001 From: niphlod Date: Mon, 11 May 2015 21:24:20 +0200 Subject: [PATCH 054/115] fixes #962 --- applications/admin/controllers/default.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 69634040..e88fee99 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -744,7 +744,7 @@ def edit(): viewlist.append(aviewpath + '.html') if len(viewlist): editviewlinks = [] - for v in viewlist: + for v in sorted(viewlist): vf = os.path.split(v)[-1] vargs = "/".join([viewpath.replace(os.sep, "/"), vf]) editviewlinks.append(A(vf.split(".")[0], @@ -754,6 +754,7 @@ def edit(): if len(request.args) > 2 and request.args[1] == 'controllers': controller = (request.args[2])[:-3] functions = find_exposed_functions(data) + functions = functions and sorted(functions) or [] else: (controller, functions) = (None, None) @@ -1067,7 +1068,7 @@ def design(): for c in controllers: data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) items = find_exposed_functions(data) - functions[c] = items + functions[c] = items and sorted(items) or [] # Get all views views = sorted( @@ -1205,7 +1206,7 @@ def plugin(): for c in controllers: data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) items = find_exposed_functions(data) - functions[c] = items + functions[c] = items and sorted(items) or [] # Get all views views = sorted( From 4b14a87463d4035a2c6f7061f6a51b3ca542bba1 Mon Sep 17 00:00:00 2001 From: Bernard Letourmy Date: Tue, 12 May 2015 09:12:28 +0800 Subject: [PATCH 055/115] fixes #966 - deadlock in cache.disk --- gluon/cache.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gluon/cache.py b/gluon/cache.py index f94937d8..ba3ea534 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -473,9 +473,14 @@ class CacheOnDisk(CacheAbstract): if item and ((dt is None) or (item[0] > now - dt)): value = item[1] else: - value = f() + try: + value = f() + except: + self.storage.release(CacheAbstract.cache_stats_name) + self.storage.release(key) + raise self.storage[key] = (now, value) - self.storage.safe_apply(CacheAbstract.cache_stats_name, inc_misses, + self.storage.safe_apply(CacheAbstract.cache_stats_name, inc_misses, default_value={'hit_total': 0, 'misses': 0}) self.storage.release(CacheAbstract.cache_stats_name) From 169818b275a896ab609887a43f9ddf8fb9e46d74 Mon Sep 17 00:00:00 2001 From: ailnlv Date: Wed, 13 May 2015 19:23:56 -0300 Subject: [PATCH 056/115] Fixing https://github.com/web2py/web2py/issues/969 Running readline.parse_and_bind("bind ^I rl_complete") makes the letter "b" stop working on the console. This patch solves the issue and correctly enables tab completition on OSX. --- gluon/shell.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gluon/shell.py b/gluon/shell.py index 5297454f..1cac24e2 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -41,9 +41,7 @@ def enable_autocomplete_and_history(adir, env): except ImportError: pass else: - readline.parse_and_bind("bind ^I rl_complete" - if sys.platform == 'darwin' - else "tab: complete") + readline.parse_and_bind("tab: complete") history_file = os.path.join(adir, '.pythonhistory') try: readline.read_history_file(history_file) From 5ef7a8e9a10b4c600018a5febfe0843f6736f195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Botaro?= Date: Thu, 14 May 2015 12:24:58 -0300 Subject: [PATCH 057/115] maintains web2py pattern --- gluon/storage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gluon/storage.py b/gluon/storage.py index eebec539..1bb507f5 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -22,8 +22,7 @@ import gluon.portalocker as portalocker __all__ = ['List', 'Storage', 'Settings', 'Messages', 'StorageList', 'load_storage', 'save_storage'] -def DEFAULT(): - return 0 +DEFAULT = lambda: 0 class Storage(dict): From eb4d159b377c08ced7d99580ec96047dd2580dc4 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 18 May 2015 23:28:17 -0500 Subject: [PATCH 058/115] fixed process_batch_upload --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 62eb7767..ed94902f 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 +Subproject commit ed94902fbd4e36af957ebb044583546a4f087aa9 From ff10eab373bd3aae69469ecffed2b31a66ef3214 Mon Sep 17 00:00:00 2001 From: omniavx Date: Wed, 20 May 2015 00:11:46 -0500 Subject: [PATCH 059/115] Update dal.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was running my application and got this error { cannot import name Set Version web2py™ Version 2.10.4-stable+timestamp.2015.04.26.15.11.54 Python Python 2.7.3: /usr/bin/python (prefix: /usr) Traceback 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. Traceback (most recent call last): File "/home/www-data/web2py/gluon/restricted.py", line 227, in restricted exec ccode in environment File "/home/www-data/web2py/applications/omniavx_cxn/controllers/valuecache.py", line 6897, in File "/home/www-data/web2py/gluon/globals.py", line 393, in self._caller = lambda f: f() File "/home/www-data/web2py/applications/omniavx_cxn/controllers/valuecache.py", line 6584, in browse_bacct_callback from plugin_PowerGrid.CallBack import CallBack File "/home/www-data/web2py/gluon/custom_import.py", line 95, in custom_importer return base_importer(pname, globals, locals, fromlist, level) File "/home/www-data/web2py/gluon/custom_import.py", line 134, in __call__ result = NATIVE_IMPORTER(name, globals, locals, fromlist, level) File "applications/omniavx_cxn/modules/plugin_PowerGrid/CallBack.py", line 41, in from gluon.dal import Table ,Query, Set, Rows, Row ImportError: cannot import name Set } same code produced no error in earlier version of web2py line 15 of web2py/gluon/dal.py is { from pydal.objects import Row, Rows, Table, Query, Expression } replacing that with { from pydal.objects import Row, Rows, Table, Query, Set, Expression } solves the problem --- gluon/dal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/dal.py b/gluon/dal.py index dbae0d39..f8579a70 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -12,7 +12,7 @@ Takes care of adapting pyDAL to web2py's needs from pydal import DAL as DAL from pydal import Field -from pydal.objects import Row, Rows, Table, Query, Expression +from pydal.objects import Row, Rows, Table, Query, Set, Expression from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon From de3d722ac90d105076fb50ddbf31f744668eba23 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 20 May 2015 08:24:33 -0500 Subject: [PATCH 060/115] fixed import, thanks Auden RovelleQuartz --- gluon/dal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/dal.py b/gluon/dal.py index dbae0d39..f8579a70 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -12,7 +12,7 @@ Takes care of adapting pyDAL to web2py's needs from pydal import DAL as DAL from pydal import Field -from pydal.objects import Row, Rows, Table, Query, Expression +from pydal.objects import Row, Rows, Table, Query, Set, Expression from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon From 3daf953c6611ed952aee4cda701eb9fa0cfcd5b1 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Wed, 20 May 2015 08:26:03 -0500 Subject: [PATCH 061/115] syncing maintenance again --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index ed94902f..62eb7767 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit ed94902fbd4e36af957ebb044583546a4f087aa9 +Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 From cd1d6c5af19262dc96b92e30fd7c71df036b800b Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 21 May 2015 22:26:04 +0200 Subject: [PATCH 062/115] redis multi-app. Thanks Lisandro for spotting it redis_cache didn't play well with multiple apps for a silly mistake. Glad that Lisadro pointed out --- gluon/contrib/redis_cache.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gluon/contrib/redis_cache.py b/gluon/contrib/redis_cache.py index 4d4d4664..0f35395a 100644 --- a/gluon/contrib/redis_cache.py +++ b/gluon/contrib/redis_cache.py @@ -67,11 +67,12 @@ def RedisCache(*args, **vars): locker.acquire() try: - if not hasattr(RedisCache, 'redis_instance'): - RedisCache.redis_instance = RedisClient(*args, **vars) + instance_name = 'redis_instance_' + current.request.application + if not hasattr(RedisCache, instance_name): + setattr(RedisCache, instance_name, RedisClient(*args, **vars)) + return getattr(RedisCache, instance_name) finally: locker.release() - return RedisCache.redis_instance class RedisClient(object): From 81e15879d4bd200a8ced60f48bf5af0bf8bfee60 Mon Sep 17 00:00:00 2001 From: gi0baro Date: Sat, 23 May 2015 16:37:52 +0200 Subject: [PATCH 063/115] Track pyDAL 15.05 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 62eb7767..aa4a5f68 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 +Subproject commit aa4a5f68f0e95db512a3c46e5e86d06336206552 From 4f316d029457dfceaa75a85cbea9941ae77313da Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 24 May 2015 21:25:27 +0200 Subject: [PATCH 064/115] thanks @wmunguiam for spotting --- 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 5d516531..15075f8e 100644 --- a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh +++ b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh @@ -84,7 +84,7 @@ server { ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA; - ssl_protocols SSLv3 TLSv1; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; keepalive_timeout 70; location / { #uwsgi_pass 127.0.0.1:9001; From d293e98b43c0aa3ff77e627d7b9592ae8e30fc8a Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 24 May 2015 18:13:03 -0500 Subject: [PATCH 065/115] Changed "Import about this GIT repo" Line 16, typo My proposal is to change it to "Important reminder about this GIT Repo", as I think the "Import" part in the current one is a typo. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 237a27df..d78d2b85 100644 --- a/README.markdown +++ b/README.markdown @@ -13,7 +13,7 @@ Learn more at http://web2py.com Then edit ./app.yaml and replace "yourappname" with yourappname. -## Import about this GIT repo +## Important reminder about this GIT repo An important part of web2py is the Database Abstraction Layer (DAL). In early 2015 this was decoupled into a separate code-base (PyDAL). In terms of git, it is a sub-module of the main repository. From 1e66fa3a93560aa5d32c27a6b1b7251e0dd8a428 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 24 May 2015 19:14:49 -0500 Subject: [PATCH 066/115] new version number --- Makefile | 2 +- VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fbc3eb92..a7573ea6 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.10.4-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.11.0-alpha+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 6b40673f..3fd8a1c0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.10.4-stable+timestamp.2015.04.26.09.05.21 +Version 2.11.0-alpha+timestamp.2015.05.24.19.14.16 From 9b71646fc5f5f2a325a655b997bbf07a177c8941 Mon Sep 17 00:00:00 2001 From: gi0baro Date: Tue, 26 May 2015 14:36:04 +0200 Subject: [PATCH 067/115] Track pyDAL 15.05.26 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index aa4a5f68..4d36919c 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit aa4a5f68f0e95db512a3c46e5e86d06336206552 +Subproject commit 4d36919c119a31565cba48c78c814971875bf859 From 4c61c0962dc511a2c68a977e9eb27a7d6b6ec742 Mon Sep 17 00:00:00 2001 From: Juozas Masiulis Date: Thu, 28 May 2015 14:29:43 +0300 Subject: [PATCH 068/115] fix cPickle not defined error --- gluon/contrib/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index 8a571e42..e8a1c23a 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -126,7 +126,7 @@ class History: def globals_dict(self): """Returns a dictionary view of the globals. """ - return dict((name, cPickle.loads(val)) + return dict((name, pickle.loads(val)) for name, val in zip(self.global_names, self.globals)) def add_unpicklable(self, statement, names): From 538f3752844989de2313706cc4954552b6b8e210 Mon Sep 17 00:00:00 2001 From: ilvalle Date: Sun, 24 May 2015 20:15:28 +0200 Subject: [PATCH 069/115] lazy request.uuid --- gluon/globals.py | 17 ++++++++++++----- gluon/main.py | 1 - 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index 1a3ebb6e..6c64ff20 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -193,6 +193,7 @@ class Request(Storage): self.is_https = False self.is_local = False self.global_settings = settings.global_settings + self._uuid = None def parse_get_vars(self): """Takes the QUERY_STRING and unpacks it to get_vars @@ -306,13 +307,21 @@ class Request(Storage): self.parse_all_vars() return self._vars + @property + def uuid(self): + """Lazily uuid + """ + if self._uuid is None: + self.compute_uuid() + return self._uuid + def compute_uuid(self): - self.uuid = '%s/%s.%s.%s' % ( + self._uuid = '%s/%s.%s.%s' % ( self.application, self.client.replace(':', '_'), self.now.strftime('%Y-%m-%d.%H-%M-%S'), web2py_uuid()) - return self.uuid + return self._uuid def user_agent(self): from gluon.contrib import user_agent_parser @@ -453,8 +462,6 @@ class Response(Storage): response.cache_includes = (cache_method, time_expire). Example: (cache.disk, 60) # caches to disk for 1 minute. """ - from gluon import URL - files = [] has_js = has_css = False for item in self.files: @@ -663,7 +670,7 @@ class Response(Storage): return handler(request, self, methods) def toolbar(self): - from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A + from gluon.html import DIV, SCRIPT, BEAUTIFY, TAG, A BUTTON = TAG.button admin = URL("admin", "default", "design", extension='html', args=current.request.application) diff --git a/gluon/main.py b/gluon/main.py index d4565dc4..ee3600a2 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -376,7 +376,6 @@ def wsgibase(environ, responder): request.env.http_x_forwarded_proto in HTTPS_SCHEMES \ or env.https == 'on' ) - request.compute_uuid() # requires client request.url = environ['PATH_INFO'] # ################################################## From 926de90ee4b9c3eafbd7cc8413e0ca7545c39c8e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 28 May 2015 13:59:03 -0500 Subject: [PATCH 070/115] fixed bug in orderby when it is a list --- gluon/sqlhtml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 5ebc4fb1..0a7b3cdb 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2071,7 +2071,7 @@ class SQLFORM(FORM): if not orderby: orderby = field_id elif isinstance(orderby, list): - orderby = map(lambda a,b: a|b, orderby) + orderby = reduce(lambda a,b: a|b, orderby) elif isinstance(orderby, Field) and orderby is not field_id: # here we're with an ASC order on a field stored as orderby orderby = orderby | field_id From a2e7794b9243eca2cc6d5c6b86ba5a629ae692fb Mon Sep 17 00:00:00 2001 From: peregrinius Date: Fri, 29 May 2015 15:22:36 +1200 Subject: [PATCH 071/115] Invite user Invite by email another user to access your application. Note, my initial version was built on Auth.register_bare which doesn't seem to be in this repository??? --- gluon/tools.py | 140 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 545e359d..fe7c9c39 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1283,7 +1283,7 @@ class Auth(object): 'retrieve_username', 'retrieve_password', 'reset_password', 'request_reset_password', 'change_password', 'profile', 'groups', - 'impersonate', 'not_authorized'): + 'impersonate', 'not_authorized', 'confirm_registration', 'invite'): if len(request.args) >= 2 and args[0] == 'impersonate': return getattr(self, args[0])(request.args[1]) else: @@ -2624,6 +2624,144 @@ class Auth(object): table_user.email.requires = old_requires return form + + def confirm_registration( + self, + next=DEFAULT, + onvalidation=DEFAULT, + onaccept=DEFAULT, + log=DEFAULT, + ): + """ + Returns a form to confirm user registration + """ + + table_user = self.table_user() + request = current.request + # response = current.response + session = current.session + + if next is DEFAULT: + next = self.get_vars_next() or self.settings.reset_password_next + + if self.settings.prevent_password_reset_attacks: + key = request.vars.key + if not key and len(request.args)>1: + key = request.args[-1] + if key: + session._reset_password_key = key + redirect(self.url(args='confirm_registration')) + else: + key = session._reset_password_key + else: + key = request.vars.key or getarg(-1) + try: + t0 = int(key.split('-')[0]) + if time.time() - t0 > 60 * 60 * 24: + raise Exception + user = table_user(reset_password_key=key) + if not user: + raise Exception + except Exception as e: + session.flash = self.messages.invalid_reset_password + redirect(self.url('login', vars=dict(test=e))) + redirect(next, client_side=self.settings.client_side) + passfield = self.settings.password_field + form = SQLFORM.factory( + Field('first_name', + label='First Name', + required=True), + Field('last_name', + label='Last Name', + required=True), + Field('new_password', 'password', + label=self.messages.new_password, + requires=self.table_user()[passfield].requires), + Field('new_password2', 'password', + label=self.messages.verify_password, + requires=[IS_EXPR( + 'value==%s' % repr(request.vars.new_password), + self.messages.mismatched_password)]), + submit_button='Confirm Registration', + hidden=dict(_next=next), + formstyle=self.settings.formstyle, + separator=self.settings.label_separator + ) + if form.accepts(request, session, + hideerror=self.settings.hideerror): + user.update_record( + **{passfield: str(form.vars.new_password), + 'first_name': str(form.vars.first_name), + 'last_name': str(form.vars.last_name), + 'registration_key': '', + 'reset_password_key': ''}) + session.flash = self.messages.password_changed + if self.settings.login_after_password_change: + self.login_user(user) + redirect(next, client_side=self.settings.client_side) + return form + + def email_registration(self, user): + """ + Sends and email request to a user informing they have been invited to register with the application + """ + import time + from gluon.utils import web2py_uuid + + reset_password_key = str(int(time.time())) + '-' + web2py_uuid() + link = self.url('confirm_registration', + vars={'key': reset_password_key}, + scheme=True) + + d = dict(user) + d.update(dict(key=reset_password_key, link=link)) + if self.settings.mailer and self.settings.mailer.send( + to=user.email, + subject='Invite to join %s' % current.response.title, # What if title is not a string?????? + message='Click on the link %(link)s to finalise your registration.' % d): + user.update_record(reset_password_key=reset_password_key) + return True + return False + + + def invite(self): + """ + Creates a form for ther user to send invites to other users to join + """ + if not self.user: + redirect(self.settings.login_url) + + #request = current.request + # response = current.response + #session = current.session + + form=FORM('Enter a comma separated list of emails to send invites:', + BR(), + INPUT(_name='emails', _value=''), + BR(), + INPUT(_type='submit', _value='Send')) + + if form.accepts(current.request,current.session): + # send the invitations + user = None + for email in form.vars.emails.split(','): + #auth.invite_user(email=email) + user = self.register_bare(email=email, password=self.random_password()) + if user: + current.session.flash = 'Invitations sent' + else: + current.session.flash = 'An error occured trying to send invites.' + + return form + """ + user = self.register_bare(email=email, password=self.random_password()) + if user: + self.email_registration(user) + return True + else: + return False + """ + def reset_password( self, next=DEFAULT, From 01474c99b01eb422a413b5de322e5d5be611b6b2 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 28 May 2015 23:22:16 -0500 Subject: [PATCH 072/115] R-2.11.1 --- CHANGELOG | 4 ++++ Makefile | 2 +- VERSION | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9e7e76c8..a1efbb0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +## 2.11.1 + +- Many small but significative improvements and bug fixes + ## 2.10.1-2.10.2 - welcome app defaults to Bootstrap 3 diff --git a/Makefile b/Makefile index a7573ea6..0e829019 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.11.0-alpha+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.11.1-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 3fd8a1c0..203bc601 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.11.0-alpha+timestamp.2015.05.24.19.14.16 +Version 2.11.1-stable+timestamp.2015.05.28.23.17.58 From 156d771ab3fc534c442c321271fe9f9058e20567 Mon Sep 17 00:00:00 2001 From: gi0baro Date: Fri, 29 May 2015 13:45:52 -0500 Subject: [PATCH 073/115] Track pyDAL 15.05.29 --- gluon/packages/dal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 4d36919c..06654a16 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 4d36919c119a31565cba48c78c814971875bf859 +Subproject commit 06654a1676d6be2210739ee36ad34d12a72dd5e9 From 236fdcfafc60436c23d0ed5ce6e04eb1e1cde4b1 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 30 May 2015 11:30:16 -0500 Subject: [PATCH 074/115] R-2.11.2 --- Makefile | 2 +- VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 0e829019..e16e0967 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### Use semantic versioning - echo 'Version 2.11.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION + echo 'Version 2.11.2-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 203bc601..4b49a8a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.11.1-stable+timestamp.2015.05.28.23.17.58 +Version 2.11.2-stable+timestamp.2015.05.30.11.29.46 From cf2d5b637b5f9251f9954798af6920e0577f58ef Mon Sep 17 00:00:00 2001 From: Chase choi Date: Tue, 2 Jun 2015 01:00:25 +0900 Subject: [PATCH 075/115] remove invalid initialize --- applications/welcome/controllers/appadmin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 8af1047c..07cb0338 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -80,7 +80,6 @@ if False and request.tickets_db: def get_databases(request): dbs = {} for (key, value) in global_env.items(): - cond = False try: cond = isinstance(value, GQLDB) except: From 8e827f7a0993e5adac4493818fcecd383474ab3c Mon Sep 17 00:00:00 2001 From: Chase choi Date: Tue, 2 Jun 2015 01:28:51 +0900 Subject: [PATCH 076/115] Update appadmin.py --- applications/welcome/controllers/appadmin.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 8af1047c..366fd128 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -420,7 +420,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] @@ -480,12 +480,12 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - total['entries'] = ram['entries'] + disk['entries'] - total['bytes'] = ram['bytes'] + disk['bytes'] - total['objects'] = ram['objects'] + disk['objects'] - total['hits'] = ram['hits'] + disk['hits'] - total['misses'] = ram['misses'] + disk['misses'] - total['keys'] = ram['keys'] + disk['keys'] + ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys.remove('ratio') + ram_keys.remove('oldest') + for key in ram_keys: + total[key] = ram[key] + disk[key] + try: total['ratio'] = total['hits'] * 100 / (total['hits'] + total['misses']) @@ -577,9 +577,7 @@ def bg_graph_model(): group = meta_graphmodel['group'].replace(' ', '') if not subgraphs.has_key(group): subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) - subgraphs[group]['tables'].append(tablename) - else: - subgraphs[group]['tables'].append(tablename) + subgraphs[group]['tables'].append(tablename) graph.add_node(tablename, name=tablename, shape='plaintext', label=table_template(tablename)) From 918fdf2f0ca2004bd4c5fb3922817295cbe7aace Mon Sep 17 00:00:00 2001 From: Stephen Tanner Date: Tue, 2 Jun 2015 12:24:35 -0400 Subject: [PATCH 077/115] Fixed authentication using different login methods. --- gluon/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 71abdc76..3f1ab741 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -2323,8 +2323,8 @@ class Auth(object): # user not in database try other login methods for login_method in self.settings.login_methods: if login_method != self and login_method(username, password): - self.user = username - return username + self.user = user + return user return False def register_bare(self, **fields): From e0074ebcaca86eebe8baf4c378fc3175aeb09a7e Mon Sep 17 00:00:00 2001 From: niphlod Date: Fri, 5 Jun 2015 22:10:33 +0200 Subject: [PATCH 078/115] fixes #994 we were overriding default classes for specific widgets --- gluon/sqlhtml.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 0a7b3cdb..87e5858e 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1838,13 +1838,16 @@ class SQLFORM(FORM): operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control') _id = "%s_%s" % (value_id, name) if field_type in ['boolean', 'double', 'time', 'integer']: - value_input = SQLFORM.widgets[field_type].widget(field, field.default, _id=_id, _class='form-control') + widget_ = SQLFORM.widgets[field_type] + value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control') elif field_type == 'date': iso_format = {'_data-w2p_date_format' : '%Y-%m-%d'} - value_input = SQLFORM.widgets.date.widget(field, field.default, _id=_id, _class='form-control', **iso_format) + widget_ = SQLFORM.widgets.date + value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format) elif field_type == 'datetime': iso_format = {'_data-w2p_datetime_format' : '%Y-%m-%d %H:%M:%S'} - value_input = SQLFORM.widgets.datetime.widget(field, field.default, _id=_id, _class='form-control', **iso_format) + widget_ = SQLFORM.widgets.datetime + value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format) elif (field_type.startswith('reference ') or field_type.startswith('list:reference ')) and \ hasattr(field.requires, 'options'): @@ -1856,7 +1859,8 @@ class SQLFORM(FORM): elif field_type.startswith('reference ') or \ field_type.startswith('list:integer') or \ field_type.startswith('list:reference '): - value_input = SQLFORM.widgets.integer.widget(field, field.default, _id=_id, _class='form-control') + widget_ = SQLFORM.widgets.integer + value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control') else: value_input = INPUT( _type='text', _id=_id, From 509b0a6987706354256428dd817934be5384c935 Mon Sep 17 00:00:00 2001 From: niphlod Date: Sat, 6 Jun 2015 09:50:44 +0200 Subject: [PATCH 079/115] fixes is_in_set repr too --- gluon/sqlhtml.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 87e5858e..0c888074 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -859,14 +859,14 @@ def formstyle_bootstrap3_stacked(form, fields): label = '' elif isinstance(controls, (SELECT, TEXTAREA)): controls.add_class('form-control') - + elif isinstance(controls, SPAN): _controls = P(controls.components) elif isinstance(controls, UL): for e in controls.elements("input"): e.add_class('form-control') - + if isinstance(label, LABEL): label['_class'] = 'control-label' @@ -909,9 +909,9 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3): label = '' elif isinstance(controls, (SELECT, TEXTAREA)): controls.add_class('form-control') - + elif isinstance(controls, SPAN): - _controls = P(controls.components, + _controls = P(controls.components, _class="form-control-static %s" % col_class) elif isinstance(controls, UL): for e in controls.elements("input"): @@ -1841,15 +1841,16 @@ class SQLFORM(FORM): widget_ = SQLFORM.widgets[field_type] value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control') elif field_type == 'date': - iso_format = {'_data-w2p_date_format' : '%Y-%m-%d'} + iso_format = {'_data-w2p_date_format': '%Y-%m-%d'} widget_ = SQLFORM.widgets.date value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format) elif field_type == 'datetime': - iso_format = {'_data-w2p_datetime_format' : '%Y-%m-%d %H:%M:%S'} + iso_format = {'_data-w2p_datetime_format': '%Y-%m-%d %H:%M:%S'} widget_ = SQLFORM.widgets.datetime value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format) elif (field_type.startswith('reference ') or field_type.startswith('list:reference ')) and \ + hasattr(field.requires, 'options') or \ hasattr(field.requires, 'options'): value_input = SELECT( *[OPTION(v, _value=k) From 0e9c5caf4d96cd67b7446158f8800884d3e6285c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 7 Jun 2015 21:28:18 -0500 Subject: [PATCH 080/115] added request_reset_password_on... --- gluon/packages/dal | 2 +- gluon/tools.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/gluon/packages/dal b/gluon/packages/dal index 06654a16..62eb7767 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 06654a1676d6be2210739ee36ad34d12a72dd5e9 +Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 diff --git a/gluon/tools.py b/gluon/tools.py index 71abdc76..783e6b28 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1499,6 +1499,8 @@ class Auth(object): change_password_onvalidation = [], change_password_onaccept = [], retrieve_password_onvalidation = [], + request_reset_password_onvalidation = [], + request_reset_password_onaccept = [], reset_password_onvalidation = [], reset_password_onaccept = [], hmac_key = hmac_key, @@ -3173,6 +3175,12 @@ class Auth(object): except Exception: session.flash = self.messages.invalid_reset_password redirect(next, client_side=self.settings.client_side) + + if onvalidation is DEFAULT: + onvalidation = self.settings.reset_password_onvalidation + if onaccept is DEFAULT: + onaccept = self.settings.reset_password_onaccept + passfield = self.settings.password_field form = SQLFORM.factory( Field('new_password', 'password', @@ -3188,7 +3196,7 @@ class Auth(object): formstyle=self.settings.formstyle, separator=self.settings.label_separator ) - if form.accepts(request, session, + if form.accepts(request, session, onvalidation=onvalidation, hideerror=self.settings.hideerror): user.update_record( **{passfield: str(form.vars.new_password), @@ -3197,6 +3205,7 @@ class Auth(object): session.flash = self.messages.password_changed if self.settings.login_after_password_change: self.login_user(user) + callback(onaccept, form) redirect(next, client_side=self.settings.client_side) return form @@ -3222,9 +3231,9 @@ class Auth(object): response.flash = self.messages.function_disabled return '' if onvalidation is DEFAULT: - onvalidation = self.settings.reset_password_onvalidation + onvalidation = self.settings.request_reset_password_onvalidation if onaccept is DEFAULT: - onaccept = self.settings.reset_password_onaccept + onaccept = self.settings.request_reset_password_onaccept if log is DEFAULT: log = self.messages['reset_password_log'] userfield = self.settings.login_userfield or 'username' \ From d61c372c955f16bcdbbe5d810775286ec9ae21c4 Mon Sep 17 00:00:00 2001 From: niphlod Date: Thu, 11 Jun 2015 21:54:21 +0200 Subject: [PATCH 081/115] fix display of checkboxes in search form of grid --- applications/welcome/static/css/web2py-bootstrap3.css | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/welcome/static/css/web2py-bootstrap3.css b/applications/welcome/static/css/web2py-bootstrap3.css index ce4aea06..b705d653 100644 --- a/applications/welcome/static/css/web2py-bootstrap3.css +++ b/applications/welcome/static/css/web2py-bootstrap3.css @@ -283,6 +283,7 @@ li.w2p_grid_breadcrumb_elem { .web2py_console .form-control { width: 20%; display: inline; + height: 100%; } .web2py_console #w2p_keywords { width: 50%; From 2ce53e99571e25950d3a1f1caef006b7a65ce2e1 Mon Sep 17 00:00:00 2001 From: niphlod Date: Sun, 14 Jun 2015 20:44:08 +0200 Subject: [PATCH 082/115] move to codecov and enable appveyor too --- .travis.yml | 4 ++-- appveyor.yml | 25 +++++++++++++++++++++++++ gluon/tests/coverage.ini | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 appveyor.yml diff --git a/.travis.yml b/.travis.yml index 77028bbd..99bed1e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,14 +17,14 @@ install: before_script: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi; - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache python-coveralls; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache codecov; fi script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage after_success: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --config_file=gluon/tests/coverage.ini; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then codecov; fi notifications: email: true diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..49fe13c3 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,25 @@ +build: false + +environment: + matrix: + - PYTHON: "C:/Python27" + COVERAGE_PROCESS_START: gluon/tests/coverage.ini + +clone_depth: 50 + +init: + - "ECHO %PYTHON%" + - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% + +install: + - ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py + - python get-pip.py + - pip install codecov + - git submodule update --init --recursive + +test_script: + - python web2py.py --run_system_tests --with_coverage + +after_test: + - coverage combine + - codecov \ No newline at end of file diff --git a/gluon/tests/coverage.ini b/gluon/tests/coverage.ini index 69f39f94..3c314b75 100644 --- a/gluon/tests/coverage.ini +++ b/gluon/tests/coverage.ini @@ -26,6 +26,7 @@ exclude_lines = ignore_errors = True omit = gluon/contrib/* gluon/tests/* + gluon/packages/* [html] directory = coverage_html_report From 8a7612c97629ad91b787ccf873b75adcc823da51 Mon Sep 17 00:00:00 2001 From: niphlod Date: Wed, 17 Jun 2015 21:23:29 +0200 Subject: [PATCH 083/115] after appveyor hooks have been defined... --- README.markdown | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index d78d2b85..1c906755 100644 --- a/README.markdown +++ b/README.markdown @@ -38,9 +38,10 @@ PyDAL uses a separate stable release cycle to the rest of web2py. PyDAL releases ## Tests -[![Build Status](https://img.shields.io/travis/web2py/web2py.svg?style=flat-square)](https://travis-ci.org/web2py/web2py) +[![Build Status](https://img.shields.io/travis/web2py/web2py/master.svg?style=flat-square&label=Travis-CI)](https://travis-ci.org/web2py/web2py) +[![MS Build Status](https://img.shields.io/appveyor/ci/web2py/web2py/master.svg?style=flat-square&label=Appveyor-CI)](https://ci.appveyor.com/project/web2py/web2py) +[![Coverage Status](https://img.shields.io/codecov/c/github/web2py/web2py.svg?style=flat-square)](https://codecov.io/github/web2py/web2py) -[![Coverage Status](https://img.shields.io/coveralls/web2py/web2py.svg?style=flat-square)](https://coveralls.io/r/web2py/web2py) ## Installation Instructions @@ -63,7 +64,7 @@ That's it!!! packages/ > web2py submodules dal/ contrib/ > third party libraries - tests/ > unittests + tests/ > unittests applications/ > are the apps admin/ > web based IDE ... From 29bf50425b59d3cdb60a0f5413220af62ed82fa4 Mon Sep 17 00:00:00 2001 From: Chase choi Date: Thu, 18 Jun 2015 16:31:28 +0900 Subject: [PATCH 084/115] Update appadmin.py --- applications/admin/controllers/appadmin.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 8af1047c..099ebc61 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -480,18 +480,18 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - total['entries'] = ram['entries'] + disk['entries'] - total['bytes'] = ram['bytes'] + disk['bytes'] - total['objects'] = ram['objects'] + disk['objects'] - total['hits'] = ram['hits'] + disk['hits'] - total['misses'] = ram['misses'] + disk['misses'] - total['keys'] = ram['keys'] + disk['keys'] + ram_keys = ram.keys() + ram_keys.remove('ratio') + ram_keys.remove('oldest') + for key in ram_keys: + total[key] = ram[key] + disk[key] + try: total['ratio'] = total['hits'] * 100 / (total['hits'] + total['misses']) except (KeyError, ZeroDivisionError): total['ratio'] = 0 - + if disk['oldest'] < ram['oldest']: total['oldest'] = disk['oldest'] else: @@ -577,9 +577,7 @@ def bg_graph_model(): group = meta_graphmodel['group'].replace(' ', '') if not subgraphs.has_key(group): subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) - subgraphs[group]['tables'].append(tablename) - else: - subgraphs[group]['tables'].append(tablename) + subgraphs[group]['tables'].append(tablename) graph.add_node(tablename, name=tablename, shape='plaintext', label=table_template(tablename)) From 6bf6ebab1b05e02ec4bd904528b3d483463ebe73 Mon Sep 17 00:00:00 2001 From: Chase choi Date: Thu, 18 Jun 2015 16:40:59 +0900 Subject: [PATCH 085/115] change initialization ok must be True or False. set False is better for initialization --- applications/admin/controllers/debug.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 8c8522ac..7176e09c 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -220,7 +220,7 @@ def list_breakpoints(): "Return a list of linenumbers for current breakpoints" breakpoints = [] - ok = None + ok = False try: filename = os.path.join(request.env['applications_parent'], 'applications', request.vars.filename) @@ -235,5 +235,4 @@ def list_breakpoints(): ok = True except Exception, e: session.flash = str(e) - ok = False return response.json({'ok': ok, 'breakpoints': breakpoints}) From 34dd8af1012b24d904e4275f364b3cbbd184c5fe Mon Sep 17 00:00:00 2001 From: Chase choi Date: Thu, 18 Jun 2015 16:55:04 +0900 Subject: [PATCH 086/115] remove duplicated execution is that really duplicated or missing something to add for difference? --- applications/admin/controllers/default.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index e88fee99..d222afe1 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -292,9 +292,6 @@ def site(): log_progress(appname) session.flash = T(msg, dict(appname=appname, digest=md5_hash(installed))) - elif f and form_update.vars.overwrite: - msg = 'unable to install application "%(appname)s"' - session.flash = T(msg, dict(appname=form_update.vars.name)) else: msg = 'unable to install application "%(appname)s"' session.flash = T(msg, dict(appname=form_update.vars.name)) From f60ae809b64a4a70bb0c415f646ad5e553824629 Mon Sep 17 00:00:00 2001 From: Chase choi Date: Thu, 18 Jun 2015 17:05:34 +0900 Subject: [PATCH 087/115] simplified conditions --- applications/admin/controllers/default.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index e88fee99..63fad372 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -867,13 +867,9 @@ def resolve(): def getclass(item): """ Determine item class """ - - if item[0] == ' ': - return 'normal' - if item[0] == '+': - return 'plus' - if item[0] == '-': - return 'minus' + operators = {' ':'normal', '+':'plus', '-':'minus'} + + return operators[item[0]] if request.vars: c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0] From f78d423c9276c09057f17eae2c26bbc94b4564bc Mon Sep 17 00:00:00 2001 From: Chase choi Date: Thu, 18 Jun 2015 17:13:38 +0900 Subject: [PATCH 088/115] change misspelling --- applications/admin/controllers/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index e88fee99..a421871a 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -1510,7 +1510,7 @@ def upload_file(): if filename: d = dict(filename=filename[len(path):]) else: - d = dict(filename='unkown') + d = dict(filename='unknown') session.flash = T('cannot upload file "%(filename)s"', d) redirect(request.vars.sender) From b636a5d6e9e7c27eaa31dadb42c5a01bf2bf41b5 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 25 Jun 2015 03:26:18 -0500 Subject: [PATCH 089/115] more companies --- applications/examples/views/default/support.html | 6 +++++- gluon/packages/dal | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/applications/examples/views/default/support.html b/applications/examples/views/default/support.html index f8cc6d59..91f99e8c 100644 --- a/applications/examples/views/default/support.html +++ b/applications/examples/views/default/support.html @@ -17,15 +17,19 @@
    • Experts4Soutions (worldwide)
    • PlanetHost (USA)
    • +
    • 10BioSystems (USA)
    • Formatics (Netherlands)
    • +
    • Corebyte (Netherlands)
    • +
    • Dutveul (Netherlands)
    • OneMeWebServices (Canada)
    • BudgetBytes (The Netherlands)
    • ANDROSoft (Poland)
    • -
    • Sonne Tech (Brazil)
    • +
    • Sonne Tech (Brazil)
    • NRG Internet Solutions (Brazil)
    • ITJP (Brazil)
    • I am Consultoria (Portugal)
    • +
    • DefineScope (Portugal)
    • LPFX (Brazil)
    • Emotionull (Greece and Cyprus)
    • VSA Services (Singapore)
    • diff --git a/gluon/packages/dal b/gluon/packages/dal index 62eb7767..c409beaf 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 62eb7767db6ba88399034a785c7d35bf1f546437 +Subproject commit c409beaff625ffa95bad266a24c61f091f8937f7 From 23ddb6c3c26e3d9f21e88e7ee12f8c4780f8f700 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 25 Jun 2015 04:19:01 -0500 Subject: [PATCH 090/115] fixed issue #999, gluon.sanitiizer.sanitze improvement, thanks macfiron --- gluon/sanitizer.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index da71d159..ce23ec3f 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -66,14 +66,15 @@ class XssCleaner(HTMLParser): #to strip or escape disallowed tags? self.strip_disallowed = strip_disallowed - self.in_disallowed = False + # there might be data after final closing tag, that is to be ignored + self.in_disallowed = [True] def handle_data(self, data): - if data and not self.in_disallowed: + if data and not self.in_disallowed[-1]: self.result += xssescape(data) def handle_charref(self, ref): - if self.in_disallowed: + if self.in_disallowed[-1]: return elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe self.result += '&#%s;' % ref @@ -81,7 +82,7 @@ class XssCleaner(HTMLParser): self.result += xssescape('&#%s' % ref) def handle_entityref(self, ref): - if self.in_disallowed: + if self.in_disallowed[-1]: return elif ref in entitydefs: self.result += '&%s;' % ref @@ -89,7 +90,7 @@ class XssCleaner(HTMLParser): self.result += xssescape('&%s' % ref) def handle_comment(self, comment): - if self.in_disallowed: + if self.in_disallowed[-1]: return elif comment: self.result += xssescape('' % comment) @@ -100,11 +101,11 @@ class XssCleaner(HTMLParser): attrs ): if tag not in self.permitted_tags: - if self.strip_disallowed: - self.in_disallowed = True - else: + self.in_disallowed.append(True) + if (not self.strip_disallowed): self.result += xssescape('<%s>' % tag) else: + self.in_disallowed.append(False) bt = '<' + tag if tag in self.allowed_attributes: attrs = dict(attrs) @@ -119,6 +120,7 @@ class XssCleaner(HTMLParser): else: bt += ' %s=%s' % (xssescape(attribute), quoteattr(attrs[attribute])) + # deal with without href and without src if bt == ' Date: Thu, 25 Jun 2015 04:23:49 -0500 Subject: [PATCH 091/115] fixed issue #982, LOAD with ajax=False and args --- gluon/sqlhtml.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 0c888074..e0cba56c 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2111,10 +2111,8 @@ class SQLFORM(FORM): # - url has valid signature (vars are not signed, only path_info) # = url does not contain 'create','delete','edit' (readonly) if user_signature: - if not ( - '/'.join(str(a) for a in args) == '/'.join(request.args) or - URL.verify(request, user_signature=user_signature, - hash_vars=False) or + if not ('/'.join(map(str,args)) == '/'.join(map(str,request.args)) or + URL.verify(request, user_signature=user_signature, hash_vars=False) or (request.args(len(args)) == 'view' and not logged)): session.flash = T('not authorized') redirect(referrer) From 28e6999e7d813d3e7633083e976cab953882fc79 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 25 Jun 2015 04:30:10 -0500 Subject: [PATCH 092/115] fixed issue #980, Admin application: can't access directories with space in directory name, thanks mmihaltz --- applications/admin/views/default/design.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/applications/admin/views/default/design.html b/applications/admin/views/default/design.html index b7fc4991..2d918b83 100644 --- a/applications/admin/views/default/design.html +++ b/applications/admin/views/default/design.html @@ -1,5 +1,7 @@ {{extend 'layout.html'}} {{ +import re +regex_space = re.compile('\s+') def all(items): return reduce(lambda a,b:a and b,items,True) def peekfile(path,file,vars={},title=None): @@ -304,7 +306,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi while path!=file_path: if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]): path.append(file_path[len(path)]) - thispath='static__'+'__'.join(path) + thispath = regex_space.sub('-', 'static__'+'__'.join(path)) }}
    •   {{=path[-1]}}/ From df34869d65c36468ea720322ca7990716cc8634e Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 25 Jun 2015 04:31:41 -0500 Subject: [PATCH 093/115] fixes #978, autotypes and unicode strings, thanks remcoboerma --- gluon/sqlhtml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index e0cba56c..717858e1 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1691,6 +1691,7 @@ class SQLFORM(FORM): AUTOTYPES = { type(''): ('string', None), + type(u''): ('string',None), type(True): ('boolean', None), type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)), type(1.0): ('double', IS_FLOAT_IN_RANGE()), From fbb5a8b9bb8422449cbd55e7a3ea74505828b90c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Thu, 25 Jun 2015 04:36:16 -0500 Subject: [PATCH 094/115] fixed issue #968, IS_MATCH validator bug with unicode, thanks alex0834 --- gluon/validators.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gluon/validators.py b/gluon/validators.py index 5142d600..2afeedc4 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -200,8 +200,11 @@ class IS_MATCH(Validator): self.is_unicode = is_unicode def __call__(self, value): - if self.is_unicode and not isinstance(value, unicode): - match = self.regex.search(str(value).decode('utf8')) + if self.is_unicode: + if isinstance(value,unicode): + match = self.regex.search(value) + else: + match = self.regex.search(str(value).decode('utf8')) else: match = self.regex.search(str(value)) if match is not None: From 6134f824529b62e95d743c04cdd59f51af3968d7 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 26 Jun 2015 06:58:07 -0500 Subject: [PATCH 095/115] fixed issuer #239, locking error on FreeBSD, thanks josejachuf --- gluon/portalocker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/portalocker.py b/gluon/portalocker.py index 2bcd63f5..de584323 100644 --- a/gluon/portalocker.py +++ b/gluon/portalocker.py @@ -119,7 +119,7 @@ class LockedFile(object): lock(self.file, LOCK_EX) if not 'a' in mode: self.file.seek(0) - self.file.truncate() + self.file.truncate(0) else: raise RuntimeError("invalid LockedFile(...,mode)") From c1ecf823d83c4fc5b9cda1ebd458cf6e4114381d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 26 Jun 2015 08:07:31 -0500 Subject: [PATCH 096/115] added link to new tutorial --- .../private/content/en/default/documentation/more.markmin | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/examples/private/content/en/default/documentation/more.markmin b/applications/examples/private/content/en/default/documentation/more.markmin index dca22f11..afd4a8a8 100644 --- a/applications/examples/private/content/en/default/documentation/more.markmin +++ b/applications/examples/private/content/en/default/documentation/more.markmin @@ -8,6 +8,7 @@ #### Learning and Demos - [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]] +- [[Step by step tutorial https://milesm.pythonanywhere.com/wiki]] - [[Killer Web Development Tutorial http://killer-web-development.com/]] - [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!) - [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE) From ad2003c6187b3c5466ac34782a21626b0e55a409 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sat, 27 Jun 2015 00:33:16 -0500 Subject: [PATCH 097/115] fixed recently introduced sanitize bug --- gluon/sanitizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index ce23ec3f..c0e6f38f 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -67,7 +67,7 @@ class XssCleaner(HTMLParser): #to strip or escape disallowed tags? self.strip_disallowed = strip_disallowed # there might be data after final closing tag, that is to be ignored - self.in_disallowed = [True] + self.in_disallowed = [False] def handle_data(self, data): if data and not self.in_disallowed[-1]: From 68526a0c6d9eb2986f127ab70bfbccaf96b0b37d Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 07:52:34 -0500 Subject: [PATCH 098/115] allow unsorted multiword query in grid search --- gluon/sqlhtml.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 717858e1..88bcab5f 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1756,10 +1756,16 @@ class SQLFORM(FORM): keywords = keywords[0] request.vars.keywords = keywords key = keywords.strip() - if key and ' ' not in key and not '"' in key and not "'" in key: + if not '"' in key: SEARCHABLE_TYPES = ('string', 'text', 'list:string') - parts = [field.contains( - key) for field in fields if field.type in SEARCHABLE_TYPES] + sfields = [field for field in fields if field.type in SEARCHABLE_TYPES] + if settings.global_settings.web2py_runtime_gae: + return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields]) + else: + 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... @@ -1773,10 +1779,6 @@ class SQLFORM(FORM): # filters.append(reduce(lambda a, b: (a & b), all_words_filters)) #parts = filters - else: - parts = None - if parts: - return reduce(lambda a, b: a | b, parts) else: return smart_query(fields, key) From 9a1229470ae56100bb8533fbdd1b277e27d89d31 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 09:48:08 -0500 Subject: [PATCH 099/115] support for api_tokens --- gluon/tools.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 52aba1a1..793d3b53 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1179,6 +1179,7 @@ class Auth(object): table_permission_name='auth_permission', table_event_name='auth_event', table_cas_name='auth_cas', + table_token_name='auth_token', table_user=None, table_group=None, table_membership=None, @@ -1462,6 +1463,7 @@ class Auth(object): settings.update(Auth.default_settings) settings.update( cas_domains=[request.env.http_host], + api_tokens=False, cas_provider=cas_provider, cas_actions=dict(login='login', validate='validate', @@ -1564,6 +1566,9 @@ class Auth(object): def table_cas(self): return self.db[self.settings.table_cas_name] + def table_token(self): + return self.db[self.settings.table_token_name] + def _HTTP(self, *a, **b): """ only used in lambda: self._HTTP(404) @@ -1591,7 +1596,8 @@ class Auth(object): 'retrieve_username', 'retrieve_password', 'reset_password', 'request_reset_password', 'change_password', 'profile', 'groups', - 'impersonate', 'not_authorized', 'confirm_registration', 'bulk_register'): + 'impersonate', 'not_authorized', 'confirm_registration', + 'bulk_register','manage_tokens'): if len(request.args) >= 2 and args[0] == 'impersonate': return getattr(self, args[0])(request.args[1]) else: @@ -1918,7 +1924,7 @@ class Auth(object): writable=False, readable=False, label=T('Modified By'), ondelete=ondelete)) - def define_tables(self, username=None, signature=None, + def define_tables(self, username=None, signature=None, api_tokens=False, migrate=None, fake_migrate=None): """ To be called unless tables are defined manually @@ -1945,6 +1951,7 @@ class Auth(object): username = settings.use_username else: settings.use_username = username + settings.api_tokens = api_tokens if not self.signature: self.define_signature() if signature == True: @@ -2128,6 +2135,21 @@ class Auth(object): migrate=self.__get_migrate( settings.table_cas_name, migrate), fake_migrate=fake_migrate)) + if settings.api_tokens: + extra_fields = settings.extra_fields.get( + settings.table_token_name, []) + signature_list + if not settings.table_token_name in db.tables: + db.define_table( + settings.table_token_name, + Field('user_id', reference_table_user, default=None, + label=self.messages.label_user_id), + Field('expires_on', 'datetime', default=datetime.datetime(2999,12,31)), + Field('token',writable=False,default=web2py_uuid()), + *extra_fields, + **dict( + migrate=self.__get_migrate( + settings.table_token_name, migrate), + fake_migrate=fake_migrate)) if not db._lazy_tables: settings.table_user = db[settings.table_user_name] settings.table_group = db[settings.table_group_name] @@ -3267,6 +3289,18 @@ class Auth(object): H4('Emails existing'),UL(*[A(x,_href='mailto:'+x) for x in emails_exist])) return form + def manage_tokens(self): + if not self.user: + redirect(self.settings.login_url) + table_token =self.table_token() + table_token.user_id.writable = False + table_token.user_id.default = self.user.id + table_token.token.writable = False + if current.request.args(1) == 'new': + table_token.token.readable = False + form = SQLFORM.grid(table_token, args=['manage_tokens']) + return form + def reset_password(self, next=DEFAULT, onvalidation=DEFAULT, @@ -3732,6 +3766,13 @@ class Auth(object): """ return self.requires(True, otherwise=otherwise) + def requires_login_or_token(self, otherwise=None): + if self.settings.api_tokens == True: + row = self.table_token()(token=current.request.vars.token) + if row: + self.login_user(self.table_user()(row.user_id)) + return self.requires(True, otherwise=otherwise) + def requires_membership(self, role=None, group_id=None, otherwise=None): """ Decorator that prevents access to action if not logged in or From bde9562b7802dbbb183a1044982bab514b5af890 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 09:49:50 -0500 Subject: [PATCH 100/115] api_tokens in example --- applications/welcome/models/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/welcome/models/db.py b/applications/welcome/models/db.py index 27dcdf14..878d1366 100644 --- a/applications/welcome/models/db.py +++ b/applications/welcome/models/db.py @@ -58,7 +58,7 @@ service = Service() plugins = PluginManager() ## create all tables needed by auth if not custom tables -auth.define_tables(username=False, signature=False) +auth.define_tables(username=False, signature=False, api_tokens=False) ## configure email mail = auth.settings.mailer From f0aba167b4ec26d11974b8d21ddb69e28edac250 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 09:51:45 -0500 Subject: [PATCH 101/115] _token, not token --- gluon/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 793d3b53..a174c44d 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3768,7 +3768,7 @@ class Auth(object): def requires_login_or_token(self, otherwise=None): if self.settings.api_tokens == True: - row = self.table_token()(token=current.request.vars.token) + row = self.table_token()(token=current.request.vars._token) if row: self.login_user(self.table_user()(row.user_id)) return self.requires(True, otherwise=otherwise) From c89614ada65cbfd7867604c724171247d8280c72 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 10:20:33 -0500 Subject: [PATCH 102/115] more strict conditions on bulk_register --- gluon/tools.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index a174c44d..f9865ae3 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1146,6 +1146,7 @@ class Auth(object): reset_password_requires_verification=False, registration_requires_verification=False, registration_requires_approval=False, + bulk_register_enabled=False, login_after_registration=False, login_after_password_change=True, alternate_requires_registration=False, @@ -3257,17 +3258,22 @@ class Auth(object): return True return False - def bulk_register(self): + def bulk_register(self, max_emails=100): """ Creates a form for ther user to send invites to other users to join """ if not self.user: redirect(self.settings.login_url) + if (not self.setting.bulk_register_enabled and + (self.settings.registration_requires_approval or + 'register' in self.settings.actions_disabled)): + return HTTP(404) form = SQLFORM.factory( Field('subject','string',default=self.messages.bulk_invite_subject,requires=IS_NOT_EMPTY()), Field('emails','text',requires=IS_NOT_EMPTY()), - Field('message','text',default=self.messages.bulk_invite_body,requires=IS_NOT_EMPTY())) + Field('message','text',default=self.messages.bulk_invite_body,requires=IS_NOT_EMPTY()), + formstyle=self.settings.formstyle) if form.process().accepted: emails = re.compile('[^\s\'"@<>,;:]+\@[^\s\'"@<>,;:]+').findall(form.vars.emails) @@ -3275,7 +3281,7 @@ class Auth(object): emails_sent = [] emails_fail = [] emails_exist = [] - for email in emails: + for email in emails[:max_emails]: if self.table_user()(email=email): emails_exist.append(email) else: @@ -3284,6 +3290,7 @@ class Auth(object): emails_sent.append(email) else: emails_fail.append(email) + emails_fail += emails[max_emails:] form = DIV(H4('Emails sent'),UL(*[A(x,_href='mailto:'+x) for x in emails_sent]), H4('Emails failed'),UL(*[A(x,_href='mailto:'+x) for x in emails_fail]), H4('Emails existing'),UL(*[A(x,_href='mailto:'+x) for x in emails_exist])) From 044b2331c3e84019f585312219b7d104e08f5abe Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 10:30:05 -0500 Subject: [PATCH 103/115] bulk_register_enabled=False --- gluon/tools.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index f9865ae3..3f81975e 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3264,9 +3264,7 @@ class Auth(object): """ if not self.user: redirect(self.settings.login_url) - if (not self.setting.bulk_register_enabled and - (self.settings.registration_requires_approval or - 'register' in self.settings.actions_disabled)): + if not self.setting.bulk_register_enabled: return HTTP(404) form = SQLFORM.factory( From 26d87967c5bdc252496a674ef4944e9a0d28e9a5 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 16:40:23 -0500 Subject: [PATCH 104/115] always allow access to appadmin/manage.auth if used is admin --- applications/welcome/controllers/appadmin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 312a6554..4d208944 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -49,7 +49,8 @@ if request.function == 'manage': auth.table_group(), auth.table_permission()]) manager_role = manager_action.get('role', None) if manager_action else None - auth.requires_membership(manager_role)(lambda: None)() + if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)): + raise HTTP(403, "Not authorized") menu = False elif (request.application == 'admin' and not session.authorized) or \ (request.application != 'admin' and not gluon.fileutils.check_credentials(request)): From d2375b4187727ba6a920b3c8570090774dfc3563 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 16:41:09 -0500 Subject: [PATCH 105/115] always allow access to /appadmin/manage/auth if used is admin --- applications/admin/controllers/appadmin.py | 10 ++++----- applications/examples/controllers/appadmin.py | 22 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 099ebc61..4d208944 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -49,7 +49,8 @@ if request.function == 'manage': auth.table_group(), auth.table_permission()]) manager_role = manager_action.get('role', None) if manager_action else None - auth.requires_membership(manager_role)(lambda: None)() + if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)): + raise HTTP(403, "Not authorized") menu = False elif (request.application == 'admin' and not session.authorized) or \ (request.application != 'admin' and not gluon.fileutils.check_credentials(request)): @@ -80,7 +81,6 @@ if False and request.tickets_db: def get_databases(request): dbs = {} for (key, value) in global_env.items(): - cond = False try: cond = isinstance(value, GQLDB) except: @@ -420,7 +420,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] @@ -480,7 +480,7 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - ram_keys = ram.keys() + ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] ram_keys.remove('ratio') ram_keys.remove('oldest') for key in ram_keys: @@ -491,7 +491,7 @@ def ccache(): total['misses']) except (KeyError, ZeroDivisionError): total['ratio'] = 0 - + if disk['oldest'] < ram['oldest']: total['oldest'] = disk['oldest'] else: diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py index 8af1047c..4d208944 100644 --- a/applications/examples/controllers/appadmin.py +++ b/applications/examples/controllers/appadmin.py @@ -49,7 +49,8 @@ if request.function == 'manage': auth.table_group(), auth.table_permission()]) manager_role = manager_action.get('role', None) if manager_action else None - auth.requires_membership(manager_role)(lambda: None)() + if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)): + raise HTTP(403, "Not authorized") menu = False elif (request.application == 'admin' and not session.authorized) or \ (request.application != 'admin' and not gluon.fileutils.check_credentials(request)): @@ -80,7 +81,6 @@ if False and request.tickets_db: def get_databases(request): dbs = {} for (key, value) in global_env.items(): - cond = False try: cond = isinstance(value, GQLDB) except: @@ -420,7 +420,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] @@ -480,12 +480,12 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - total['entries'] = ram['entries'] + disk['entries'] - total['bytes'] = ram['bytes'] + disk['bytes'] - total['objects'] = ram['objects'] + disk['objects'] - total['hits'] = ram['hits'] + disk['hits'] - total['misses'] = ram['misses'] + disk['misses'] - total['keys'] = ram['keys'] + disk['keys'] + ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys.remove('ratio') + ram_keys.remove('oldest') + for key in ram_keys: + total[key] = ram[key] + disk[key] + try: total['ratio'] = total['hits'] * 100 / (total['hits'] + total['misses']) @@ -577,9 +577,7 @@ def bg_graph_model(): group = meta_graphmodel['group'].replace(' ', '') if not subgraphs.has_key(group): subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) - subgraphs[group]['tables'].append(tablename) - else: - subgraphs[group]['tables'].append(tablename) + subgraphs[group]['tables'].append(tablename) graph.add_node(tablename, name=tablename, shape='plaintext', label=table_template(tablename)) From ef433da19034b05c49042baa1de679e05c18caf6 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 28 Jun 2015 17:01:21 -0500 Subject: [PATCH 106/115] improvements to token logic, thanks Niphlod --- gluon/tools.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/gluon/tools.py b/gluon/tools.py index 3f81975e..53759ca4 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -2145,7 +2145,7 @@ class Auth(object): Field('user_id', reference_table_user, default=None, label=self.messages.label_user_id), Field('expires_on', 'datetime', default=datetime.datetime(2999,12,31)), - Field('token',writable=False,default=web2py_uuid()), + Field('token',writable=False,default=web2py_uuid(),unique=True), *extra_fields, **dict( migrate=self.__get_migrate( @@ -3773,9 +3773,22 @@ class Auth(object): def requires_login_or_token(self, otherwise=None): if self.settings.api_tokens == True: - row = self.table_token()(token=current.request.vars._token) - if row: - self.login_user(self.table_user()(row.user_id)) + user = None + request = current.request + token = request.env.http_web2py_api_token or request.vars._token + table_token = self.table_token() + table_user = self.table_user() + from gluon.settings import global_settings + if global_settings.web2py_runtime_gae: + row = table_token(token=token) + if row: + user = table_user(row.user_id) + else: + row = self.db(table_token.token==token)(table_user.id==table_token.user_id).select().first() + if row: + user = row[table_user._tablename] + if user: + self.login_user(user) return self.requires(True, otherwise=otherwise) def requires_membership(self, role=None, group_id=None, otherwise=None): From f39db6331a3e95d14f193b754b5e9615c7129b92 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 29 Jun 2015 03:56:22 -0500 Subject: [PATCH 107/115] dealing with issue of accidentally redefining request/response, thanks Auden RovelleQuartz --- gluon/compileapp.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 52083321..4230b6a6 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -261,7 +261,7 @@ class LoadFactory(object): import globals target = target or 'c' + str(random.random())[2:] attr['_id'] = target - request = self.environment['request'] + request = current.request if '.' in f: f, extension = f.rsplit('.', 1) if url or ajax: @@ -532,10 +532,11 @@ def run_models_in(environment): It tries pre-compiled models first before compiling them. """ - folder = environment['request'].folder - c = environment['request'].controller + request = current.request + folder = request.folder + c = request.controller #f = environment['request'].function - response = environment['response'] + response = current.response path = pjoin(folder, 'models') cpath = pjoin(folder, 'compiled') @@ -577,7 +578,7 @@ def run_controller_in(controller, function, environment): """ # if compiled should run compiled! - folder = environment['request'].folder + folder = current.request.folder path = pjoin(folder, 'compiled') badc = 'invalid controller (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function) @@ -631,7 +632,7 @@ def run_controller_in(controller, function, environment): layer = filename + ':' + function code = getcfs(layer, filename, lambda: compile2(code, layer)) restricted(code, environment, filename) - response = environment['response'] + response = current.response vars = response._vars if response.postprocessing: vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars) @@ -649,8 +650,8 @@ def run_view_in(environment): or `view/generic.extension` It tries the pre-compiled views_controller_function.pyc before compiling it. """ - request = environment['request'] - response = environment['response'] + request = current.request + response = current.response view = response.view folder = request.folder path = pjoin(folder, 'compiled') From cdbf48f09bda2d2bae3202ed2be4eebed538fcd3 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Mon, 29 Jun 2015 13:03:23 -0500 Subject: [PATCH 108/115] fixed margin-top in welcome --- applications/welcome/static/css/web2py-bootstrap3.css | 4 +++- applications/welcome/views/layout.html | 2 +- gluon/packages/dal | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/applications/welcome/static/css/web2py-bootstrap3.css b/applications/welcome/static/css/web2py-bootstrap3.css index b705d653..a2c321de 100644 --- a/applications/welcome/static/css/web2py-bootstrap3.css +++ b/applications/welcome/static/css/web2py-bootstrap3.css @@ -24,7 +24,9 @@ div.flash.alert:hover { .ie-lte8 div.flash:hover { filter: alpha(opacity=25); } - +.main-container { + margin-top: 20px; +} div.error { width: auto; diff --git a/applications/welcome/views/layout.html b/applications/welcome/views/layout.html index 8f9fdfee..1e12f6c8 100644 --- a/applications/welcome/views/layout.html +++ b/applications/welcome/views/layout.html @@ -75,7 +75,7 @@ {{end}} -
      +
      {{if left_sidebar_enabled:}}
    - {{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}} + {{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn btn-default")}}