From ff44821f05fea110cd87a0e6f7fd46fb7c799e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20V=C3=A9zina?= Date: Mon, 26 Mar 2018 16:21:21 -0400 Subject: [PATCH 1/8] Close #1887 - remove Recaptcha V1 --- gluon/tests/test_tools.py | 37 ++++------ gluon/tools.py | 145 +------------------------------------- 2 files changed, 16 insertions(+), 166 deletions(-) diff --git a/gluon/tests/test_tools.py b/gluon/tests/test_tools.py index 083d83af..2ad46c5e 100644 --- a/gluon/tests/test_tools.py +++ b/gluon/tests/test_tools.py @@ -17,7 +17,7 @@ DEFAULT_URI = os.getenv('DB', 'sqlite:memory') from gluon.dal import DAL, Field from pydal.objects import Table from gluon import tools -from gluon.tools import Auth, Mail, Recaptcha, Recaptcha2, prettydate, Expose +from gluon.tools import Auth, Mail, Recaptcha2, prettydate, Expose from gluon._compat import PY2 from gluon.globals import Request, Response, Session from gluon.storage import Storage @@ -208,14 +208,6 @@ class TestMail(unittest.TestCase): self.assertTrue('Content-Id: ' in message.payload) -# class TestRecaptcha(unittest.TestCase): -# def test_Recaptcha(self): -# from html import FORM -# form = FORM(Recaptcha(public_key='public_key', private_key='private_key')) -# self.assertEqual(form.xml(), -# '
') -# -# # class TestRecaptcha2(unittest.TestCase): # def test_Recaptcha2(self): # from html import FORM @@ -248,7 +240,6 @@ class TestAuthJWT(unittest.TestCase): self.user_data['password'])[0])) self.jwtauth = AuthJWT(self.auth, secret_key='secret', verify_expiration=True) - def test_jwt_token_manager(self): import gluon.serializers self.request.vars.update(self.user_data) @@ -260,7 +251,6 @@ class TestAuthJWT(unittest.TestCase): self.token = self.jwtauth.jwt_token_manager() self.assertIsNotNone(self.token) - def test_allows_jwt(self): import gluon.serializers self.request.vars.update(self.user_data) @@ -270,11 +260,13 @@ class TestAuthJWT(unittest.TestCase): del self.request.vars['password'] self.token = self.jwtauth.jwt_token_manager() self.request.vars._token = gluon.serializers.json_parser.loads(self.token)['token'] + @self.jwtauth.allows_jwt() def optional_auth(): self.assertEqual(self.user_data['username'], self.auth.user.username) optional_auth() + @unittest.skipIf(IS_IMAP, "TODO: Imap raises 'Connection refused'") # class TestAuth(unittest.TestCase): # @@ -495,8 +487,6 @@ class TestAuthJWT(unittest.TestCase): # # impersonate_form = auth.impersonate(user_id=omer_id) # # self.assertTrue(auth.is_impersonating()) # # self.assertEqual(impersonate_form, 'test') - - class TestAuth(unittest.TestCase): def myassertRaisesRegex(self, *args, **kwargs): @@ -904,7 +894,7 @@ class TestAuth(unittest.TestCase): self.assertEqual(count_log_event_test_after, count_log_event_test_before) def test_add_membership(self): - user = self.db(self.db.auth_user.username == 'bart').select().first() # bypass login_bare() + user = self.db(self.db.auth_user.username == 'bart').select().first() # bypass login_bare() user_id = user.id role_name = 'test_add_membership_group' group_id = self.auth.add_group(role_name) @@ -1174,6 +1164,7 @@ class TestToolsFunctions(unittest.TestCase): pjoin = os.path.join + def have_symlinks(): return os.name == 'posix' @@ -1181,18 +1172,20 @@ def have_symlinks(): class Test_Expose__in_base(unittest.TestCase): def test_in_base(self): - are_under = [ # (sub, base) + are_under = [ + # (sub, base) ('/foo/bar', '/foo'), ('/foo', '/foo'), ('/foo', '/'), ('/', '/'), ] for sub, base in are_under: - self.assertTrue( Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), - '%s is not under %s' % (sub, base) ) + self.assertTrue(Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), + '%s is not under %s' % (sub, base)) def test_not_in_base(self): - are_not_under = [ # (sub, base) + are_not_under = [ + # (sub, base) ('/foobar', '/foo'), ('/foo', '/foo/bar'), ('/bar', '/foo'), @@ -1200,8 +1193,8 @@ class Test_Expose__in_base(unittest.TestCase): ('/', '/x'), ] for sub, base in are_not_under: - self.assertFalse( Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), - '%s should not be under %s' % (sub, base) ) + self.assertFalse(Expose._Expose__in_base(subdir=sub, basedir=base, sep='/'), + '%s should not be under %s' % (sub, base)) class TestExpose(unittest.TestCase): @@ -1237,7 +1230,7 @@ class TestExpose(unittest.TestCase): shutil.rmtree(self.base_dir) def make_dirs(self): - """setup direcotry strucutre""" + """setup directory structure""" for d in (['inside'], ['inside', 'dir1'], ['inside', 'dir2'], @@ -1257,7 +1250,7 @@ class TestExpose(unittest.TestCase): f.write('README content') def make_symlinks(self): - """setup extenstion for posix systems""" + """setup extension for posix systems""" # inside links os.symlink( pjoin(self.base_dir, 'inside', 'dir1'), diff --git a/gluon/tools.py b/gluon/tools.py index 753408aa..d82e9fd4 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -52,7 +52,7 @@ import gluon.serializers as serializers Table = DAL.Table Field = DAL.Field -__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', +__all__ = ['Mail', 'Auth', 'Recaptcha2', 'Crud', 'Service', 'Wiki', 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] # mind there are two loggers here (logger and crud.settings.logger)! @@ -826,149 +826,6 @@ class Mail(object): return True -class Recaptcha(DIV): - - """ - Examples: - Use as:: - - form = FORM(Recaptcha(public_key='...', private_key='...')) - - or:: - - form = SQLFORM(...) - form.append(Recaptcha(public_key='...', private_key='...')) - - """ - - API_SSL_SERVER = 'https://www.google.com/recaptcha/api' - API_SERVER = 'http://www.google.com/recaptcha/api' - VERIFY_SERVER = 'http://www.google.com/recaptcha/api/verify' - - def __init__(self, - request=None, - public_key='', - private_key='', - use_ssl=False, - error=None, - error_message='invalid', - label='Verify:', - options='', - comment='', - ajax=False - ): - request = request or current.request - self.request_vars = request and request.vars or current.request.vars - self.remote_addr = request.env.remote_addr - self.public_key = public_key - self.private_key = private_key - self.use_ssl = use_ssl - self.error = error - self.errors = Storage() - self.error_message = error_message - self.components = [] - self.attributes = {} - self.label = label - self.options = options - self.comment = comment - self.ajax = ajax - - def _validate(self): - - # for local testing: - - recaptcha_challenge_field = \ - self.request_vars.recaptcha_challenge_field - recaptcha_response_field = \ - self.request_vars.recaptcha_response_field - private_key = self.private_key - remoteip = self.remote_addr - if not (recaptcha_response_field and recaptcha_challenge_field - and len(recaptcha_response_field) - and len(recaptcha_challenge_field)): - self.errors['captcha'] = self.error_message - return False - params = urlencode({ - 'privatekey': private_key, - 'remoteip': remoteip, - 'challenge': recaptcha_challenge_field, - 'response': recaptcha_response_field, - }) - request = urllib2.Request( - url=self.VERIFY_SERVER, - data=params, - headers={'Content-type': 'application/x-www-form-urlencoded', - 'User-agent': 'reCAPTCHA Python'}) - httpresp = urllib2.urlopen(request) - return_values = httpresp.read().splitlines() - httpresp.close() - return_code = return_values[0] - if return_code == 'true': - del self.request_vars.recaptcha_challenge_field - del self.request_vars.recaptcha_response_field - self.request_vars.captcha = '' - return True - else: - # In case we get an error code, store it so we can get an error message - # from the /api/challenge URL as described in the reCAPTCHA api docs. - self.error = return_values[1] - self.errors['captcha'] = self.error_message - return False - - def xml(self): - public_key = self.public_key - use_ssl = self.use_ssl - error_param = '' - if self.error: - error_param = '&error=%s' % self.error - if use_ssl: - server = self.API_SSL_SERVER - else: - server = self.API_SERVER - if not self.ajax: - captcha = DIV( - SCRIPT("var RecaptchaOptions = {%s};" % self.options), - SCRIPT(_type="text/javascript", - _src="%s/challenge?k=%s%s" % (server, public_key, error_param)), - TAG.noscript( - IFRAME( - _src="%s/noscript?k=%s%s" % ( - server, public_key, error_param), - _height="300", _width="500", _frameborder="0"), BR(), - INPUT( - _type='hidden', _name='recaptcha_response_field', - _value='manual_challenge')), _id='recaptcha') - - else: # use Google's ajax interface, needed for LOADed components - - url_recaptcha_js = "%s/js/recaptcha_ajax.js" % server - RecaptchaOptions = "var RecaptchaOptions = {%s}" % self.options - script = """%(options)s; - jQuery.getScript('%(url)s',function() { - Recaptcha.create('%(public_key)s', - 'recaptcha',jQuery.extend(RecaptchaOptions,{'callback':Recaptcha.focus_response_field})) - }) """ % ({'options': RecaptchaOptions, 'url': url_recaptcha_js, 'public_key': public_key}) - captcha = DIV( - SCRIPT( - script, - _type="text/javascript", - ), - TAG.noscript( - IFRAME( - _src="%s/noscript?k=%s%s" % ( - server, public_key, error_param), - _height="300", _width="500", _frameborder="0"), BR(), - INPUT( - _type='hidden', _name='recaptcha_response_field', - _value='manual_challenge')), _id='recaptcha') - - if not self.errors.captcha: - return XML(captcha).xml() - else: - captcha.append(DIV(self.errors['captcha'], _class='error')) - return XML(captcha).xml() - - class Recaptcha2(DIV): """ Experimental: From 9b4b41223934f6eadd3ebfc891e78a2f1eb47c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Thu, 12 Apr 2018 12:25:30 -0400 Subject: [PATCH 2/8] Fixed Python 3.x LDAP bug on user fistname and lastname part --- gluon/contrib/login_methods/ldap_auth.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index 4c07b757..d12ed83d 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -398,17 +398,25 @@ def ldap_auth(server='ldap', if manage_user: logger.info('[%s] Manage user data' % str(username)) try: + user_firstname = result[user_firstname_attrib][0] if user_firstname_part is not None: - store_user_firstname = result[user_firstname_attrib][0].split(' ', 1)[user_firstname_part] + store_user_firstname = user_firstname.split( + b' ' if isinstance(user_firstname, bytes) else ' ', + 1 + )[user_firstname_part] else: - store_user_firstname = result[user_firstname_attrib][0] + store_user_firstname = user_firstname except KeyError as e: store_user_firstname = None try: + user_lastname = result[user_lastname_attrib][0] if user_lastname_part is not None: - store_user_lastname = result[user_lastname_attrib][0].split(' ', 1)[user_lastname_part] + store_user_lastname = user_lastname.split( + b' ' if isinstance(user_lastname, bytes) else ' ', + 1 + )[user_lastname_part] else: - store_user_lastname = result[user_lastname_attrib][0] + store_user_lastname = user_lastname except KeyError as e: store_user_lastname = None try: From de0e63469c69c64a63e42dcc769fcda44a7e67da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cesar=20Caballero=20D=C3=ADaz?= Date: Fri, 13 Apr 2018 14:35:52 -0400 Subject: [PATCH 3/8] Add support for Python logger logging levels in LDAP auth --- gluon/contrib/login_methods/ldap_auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index 4c07b757..2bec1c6f 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -170,7 +170,9 @@ def ldap_auth(server='ldap', is "error" and can be set to error, warning, info, debug. """ logger = logging.getLogger('web2py.auth.ldap_auth') - if logging_level == 'error': + if isinstance(logging_level, int): + logger.setLevel(logging_level) + elif logging_level == 'error': logger.setLevel(logging.ERROR) elif logging_level == 'warning': logger.setLevel(logging.WARNING) From 90288a013446ce15434fee196c932709213b4c14 Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Sat, 14 Apr 2018 11:42:31 +0300 Subject: [PATCH 4/8] fix sessions in cookies for python3 --- gluon/globals.py | 2 +- gluon/utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gluon/globals.py b/gluon/globals.py index 767a9b0b..95e310c6 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -1167,7 +1167,7 @@ class Session(Storage): compression_level=compression_level) rcookies = response.cookies rcookies.pop(name, None) - rcookies[name] = value + rcookies[name] = value.decode('utf8') rcookies[name]['path'] = '/' expires = response.session_cookie_expires if isinstance(expires, datetime.datetime): diff --git a/gluon/utils.py b/gluon/utils.py index 3b26a51b..3b93ea18 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -207,6 +207,8 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): def secure_loads(data, encryption_key, hash_key=None, compression_level=None): + if not isinstance(data, bytes): + data = bytes(data, 'utf8') components = data.count(b':') if components == 1: return secure_loads_deprecated(data, encryption_key, hash_key, compression_level) From 086bfb58512162437d11e6693a4060c181bc04ff Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Sat, 14 Apr 2018 14:33:37 +0300 Subject: [PATCH 5/8] fix redis sessions and redis for python3 --- gluon/contrib/redis_cache.py | 4 ++-- gluon/contrib/redis_session.py | 12 +++++++----- gluon/contrib/redis_utils.py | 4 ++-- gluon/globals.py | 6 ++++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/gluon/contrib/redis_cache.py b/gluon/contrib/redis_cache.py index 05aa53fc..110360eb 100644 --- a/gluon/contrib/redis_cache.py +++ b/gluon/contrib/redis_cache.py @@ -10,7 +10,7 @@ except: import time import re import logging -import thread +from threading import Lock import random from gluon import current from gluon.cache import CacheAbstract @@ -19,7 +19,7 @@ from gluon.contrib.redis_utils import register_release_lock, RConnectionError logger = logging.getLogger("web2py.cache.redis") -locker = thread.allocate_lock() +locker = Lock() def RedisCache(redis_conn=None, debug=False, with_lock=False, fail_gracefully=False, db=None): diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py index 1273e6de..b26b6b21 100644 --- a/gluon/contrib/redis_session.py +++ b/gluon/contrib/redis_session.py @@ -8,7 +8,7 @@ Redis-backed sessions """ import logging -import thread +from threading import Lock from gluon import current from gluon.storage import Storage from gluon.contrib.redis_utils import acquire_lock, release_lock @@ -16,7 +16,7 @@ from gluon.contrib.redis_utils import register_release_lock logger = logging.getLogger("web2py.session.redis") -locker = thread.allocate_lock() +locker = Lock() def RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None): @@ -43,7 +43,7 @@ def RedisSession(redis_conn, session_expiry=False, with_lock=False, db=None): try: instance_name = 'redis_instance_' + current.request.application if not hasattr(RedisSession, instance_name): - setattr(RedisSession, instance_name, + setattr(RedisSession, instance_name, RedisClient(redis_conn, session_expiry=session_expiry, with_lock=with_lock)) return getattr(RedisSession, instance_name) finally: @@ -185,8 +185,10 @@ class MockQuery(object): if rtn: if self.unique_key: # make sure the id and unique_key are correct - if rtn['unique_key'] == self.unique_key: - rtn['update_record'] = self.update # update record support + if not isinstance(self.unique_key, bytes): + self.unique_key = bytes(self.unique_key, 'utf8') + if rtn[b'unique_key'] == self.unique_key: + rtn[b'update_record'] = self.update # update record support else: rtn = None return [Storage(rtn)] if rtn else [] diff --git a/gluon/contrib/redis_utils.py b/gluon/contrib/redis_utils.py index 217ca952..8dc7b2d3 100644 --- a/gluon/contrib/redis_utils.py +++ b/gluon/contrib/redis_utils.py @@ -11,7 +11,7 @@ to ensure compatibility with another - similar - library """ import logging -import thread +from threading import Lock import time from gluon import current @@ -26,7 +26,7 @@ except ImportError: raise RuntimeError('Needs redis library to work') -locker = thread.allocate_lock() +locker = Lock() def RConn(*args, **vars): diff --git a/gluon/globals.py b/gluon/globals.py index 95e310c6..edf0e504 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -967,7 +967,7 @@ class Session(Storage): if row: # rows[0].update_record(locked=True) # Unpickle the data - session_data = pickle.loads(row.session_data) + session_data = pickle.loads(row[b'session_data']) self.update(session_data) response.session_new = False else: @@ -1049,7 +1049,9 @@ class Session(Storage): if record_id.isdigit() and long(record_id) > 0: new_unique_key = web2py_uuid() row = table(record_id) - if row and row.unique_key == unique_key: + if not isinstance(unique_key, bytes): + unique_key = bytes(unique_key, 'utf8') + if row and row[b'unique_key'] == unique_key: table._db(table.id == record_id).update(unique_key=new_unique_key) else: record_id = None From 0900a3ddb94c10cfb509726277798c2a71a25dbb Mon Sep 17 00:00:00 2001 From: Radu Ioan Fericean Date: Sat, 14 Apr 2018 14:58:53 +0300 Subject: [PATCH 6/8] discovered and use _compat to_bytes and to_native functions --- gluon/contrib/redis_session.py | 5 ++--- gluon/globals.py | 22 ++++++++++------------ gluon/utils.py | 3 +-- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/gluon/contrib/redis_session.py b/gluon/contrib/redis_session.py index b26b6b21..e5771719 100644 --- a/gluon/contrib/redis_session.py +++ b/gluon/contrib/redis_session.py @@ -13,6 +13,7 @@ from gluon import current from gluon.storage import Storage from gluon.contrib.redis_utils import acquire_lock, release_lock from gluon.contrib.redis_utils import register_release_lock +from gluon._compat import to_bytes logger = logging.getLogger("web2py.session.redis") @@ -185,9 +186,7 @@ class MockQuery(object): if rtn: if self.unique_key: # make sure the id and unique_key are correct - if not isinstance(self.unique_key, bytes): - self.unique_key = bytes(self.unique_key, 'utf8') - if rtn[b'unique_key'] == self.unique_key: + if rtn[b'unique_key'] == to_bytes(self.unique_key): rtn[b'update_record'] = self.update # update record support else: rtn = None diff --git a/gluon/globals.py b/gluon/globals.py index edf0e504..c54d4635 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -14,7 +14,7 @@ Contains the classes for the global used variables: """ from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \ - unicodeT, long, hashlib_md5, urllib_quote + to_bytes, unicodeT, long, hashlib_md5, urllib_quote from gluon.storage import Storage, List from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE from gluon.contenttype import contenttype @@ -225,7 +225,7 @@ class Request(Storage): # instead of load. # This line can be simplified to json_vars = json_parser.load(body) # if and when we drop support for python versions under 3.6 - json_vars = json_parser.loads(to_native(body.read())) + json_vars = json_parser.loads(to_native(body.read())) except: # incoherent request bodies can still be parsed "ad-hoc" json_vars = {} @@ -344,8 +344,8 @@ class Request(Storage): user_agent = Storage(user_agent) user_agent.is_mobile = 'Mobile' in http_user_agent user_agent.is_tablet = 'Tablet' in http_user_agent - session._user_agent = user_agent - + session._user_agent = user_agent + return user_agent def requires_https(self): @@ -485,12 +485,12 @@ class Response(Storage): # # We will only minify and concat adjacent internal files as there's # no way to know if changing the order with which the files are apppended - # will break things since the order matters in both CSS and JS and + # will break things since the order matters in both CSS and JS and # internal files may be interleaved with external ones. files = [] # For the adjacent list we're going to use storage List to both distinguish # from the regular list and so we can add attributes - internal = List() + internal = List() internal.has_js = False internal.has_css = False done = set() # to remove duplicates @@ -513,7 +513,7 @@ class Response(Storage): if item.endswith('.js'): internal.has_js = True if item.endswith('.css'): - internal.has_css = True + internal.has_css = True if internal: files.append(internal) @@ -537,7 +537,7 @@ class Response(Storage): time_expire) else: files[i] = call_minify() - + def static_map(s, item): if isinstance(item, str): f = item.lower().split('?')[0] @@ -1049,9 +1049,7 @@ class Session(Storage): if record_id.isdigit() and long(record_id) > 0: new_unique_key = web2py_uuid() row = table(record_id) - if not isinstance(unique_key, bytes): - unique_key = bytes(unique_key, 'utf8') - if row and row[b'unique_key'] == unique_key: + if row and row[b'unique_key'] == to_bytes(unique_key): table._db(table.id == record_id).update(unique_key=new_unique_key) else: record_id = None @@ -1169,7 +1167,7 @@ class Session(Storage): compression_level=compression_level) rcookies = response.cookies rcookies.pop(name, None) - rcookies[name] = value.decode('utf8') + rcookies[name] = to_native(value) rcookies[name]['path'] = '/' expires = response.session_cookie_expires if isinstance(expires, datetime.datetime): diff --git a/gluon/utils.py b/gluon/utils.py index 3b93ea18..ea738047 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -207,8 +207,7 @@ def secure_dumps(data, encryption_key, hash_key=None, compression_level=None): def secure_loads(data, encryption_key, hash_key=None, compression_level=None): - if not isinstance(data, bytes): - data = bytes(data, 'utf8') + data = to_bytes(data) components = data.count(b':') if components == 1: return secure_loads_deprecated(data, encryption_key, hash_key, compression_level) From b26e28fda13c508ed0b98b943b0e53ef04bf150b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Bo=C3=9F?= Date: Tue, 17 Apr 2018 20:31:38 +0200 Subject: [PATCH 7/8] fix editing mistakes --- applications/examples/views/default/download.html | 2 +- applications/examples/views/default/examples.html | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/examples/views/default/download.html b/applications/examples/views/default/download.html index 362d64bf..c1ecdec5 100644 --- a/applications/examples/views/default/download.html +++ b/applications/examples/views/default/download.html @@ -81,7 +81,7 @@

License

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

-

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

+

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

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

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

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

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

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

-

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

+

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

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

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

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

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

  • unnamed arguments, they correspond to nested tags
  • @@ -201,11 +201,11 @@ def ajaxwiki_onclick():

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

    In controller: session_examples.py {{=CODE(""" def counter(): - session.counter = (sesstion.counter or 0) + 1 + session.counter = (session.counter or 0) + 1 return dict(counter=session.counter) """.strip(),language='web2py',link=URL('global','vars'),_class='boxCode')}}and view: session_examples/counter.html {{=CODE(open(os.path.join(request.folder,'views/session_examples/counter.html'),'r').read(),language='html',link=URL('global','vars'),_class='boxCode')}} -

    Click to count. The session.counter is persistent for this user and application. Every applicaiton within the system has its own separate session management. +

    Click to count. The session.counter is persistent for this user and application. Every application within the system has its own separate session management.
    Try it here: counter

    Template Examples

    From bfcb047482d9585b556d7eb480216e6c0d1aa788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Bo=C3=9F?= Date: Tue, 17 Apr 2018 22:24:07 +0200 Subject: [PATCH 8/8] supply missing link to Rocket Web Server --- applications/examples/views/default/who.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/examples/views/default/who.html b/applications/examples/views/default/who.html index 48421704..4ae21094 100644 --- a/applications/examples/views/default/who.html +++ b/applications/examples/views/default/who.html @@ -148,7 +148,7 @@
    • Python created by Guido van Rossum.
    • -
    • Rocket Web Server developed by Timothy Farrell.
    • +
    • Rocket Web Server developed by Timothy Farrell.
    • CodeMirror
    • s
    • PyRTF developed by Simon Cusack and revised by Grant Edwards
    • PyRSS2Gen developed by Dalke Scientific Software