Merge branch 'master' into fix_issues_for_python3

This commit is contained in:
Koen van Zuijlen
2018-10-29 13:47:06 +01:00
30 changed files with 312 additions and 83 deletions
+35
View File
@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1 -1
View File
@@ -1,4 +1,4 @@
## 2.17.1 ## 2.17.1-2
- pydal 18.08 - pydal 18.08
- many small bug fixes - many small bug fixes
+1 -1
View File
@@ -44,7 +44,7 @@ rmfiles:
rm -rf applications/examples/uploads/* rm -rf applications/examples/uploads/*
src: src:
### Use semantic versioning ### Use semantic versioning
echo 'Version 2.17.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.17.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files ### rm -f all junk files
#make clean #make clean
# make rmfiles # make rmfiles
+1 -1
View File
@@ -1 +1 @@
Version 2.17.1-stable+timestamp.2018.08.05.17.57.00 Version 2.17.2-stable+timestamp.2018.10.06.11.34.06
+8 -8
View File
@@ -212,16 +212,16 @@ def mongrel2_handler(application, conn, debug=False):
while True: while True:
if debug: if debug:
print "WAITING FOR REQUEST" print("WAITING FOR REQUEST")
# receive a request # receive a request
req = conn.recv() req = conn.recv()
if debug: if debug:
print "REQUEST BODY: %r\n" % req.body print("REQUEST BODY: %r\n" % req.body)
if req.is_disconnect(): if req.is_disconnect():
if debug: if debug:
print "DISCONNECT" print("DISCONNECT")
continue # effectively ignore the disconnect from the client continue # effectively ignore the disconnect from the client
# Set a couple of environment attributes a.k.a. header attributes # Set a couple of environment attributes a.k.a. header attributes
@@ -247,7 +247,7 @@ def mongrel2_handler(application, conn, debug=False):
environ['wsgi.input'] = req.body environ['wsgi.input'] = req.body
if debug: if debug:
print "ENVIRON: %r\n" % environ print("ENVIRON: %r\n" % environ)
# SimpleHandler needs file-like stream objects for # SimpleHandler needs file-like stream objects for
# requests, errors and responses # requests, errors and responses
@@ -282,10 +282,10 @@ def mongrel2_handler(application, conn, debug=False):
# return the response # return the response
if debug: if debug:
print "RESPONSE: %r\n" % response print("RESPONSE: %r\n" % response)
if errors: if errors:
if debug: if debug:
print "ERRORS: %r" % errors print("ERRORS: %r" % errors)
data = "%s\r\n\r\n%s" % (data, errors) data = "%s\r\n\r\n%s" % (data, errors)
conn.reply_http( conn.reply_http(
req, data, code=code, status=status, headers=headers) req, data, code=code, status=status, headers=headers)
@@ -355,8 +355,8 @@ def main():
dest='workers', dest='workers',
help='number of workers number') help='number of workers number')
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
print 'starting %s on %s:%s...' % ( print('starting %s on %s:%s...' % (
options.server, options.ip, options.port) options.server, options.ip, options.port))
run(options.server, options.ip, options.port, run(options.server, options.ip, options.port,
logging=options.logging, profiler=options.profiler_dir, logging=options.logging, profiler=options.profiler_dir,
options=options) options=options)
+9 -2
View File
@@ -562,7 +562,11 @@ def enable():
os.unlink(filename) os.unlink(filename)
return SPAN(T('Disable'), _style='color:green') return SPAN(T('Disable'), _style='color:green')
else: else:
safe_open(filename, 'wb').write('disabled: True\ntime-disabled: %s' % request.now) if PY2:
safe_open(filename, 'wb').write('disabled: True\ntime-disabled: %s' % request.now)
else:
str_ = 'disabled: True\ntime-disabled: %s' % request.now
safe_open(filename, 'wb').write(str_.encode('utf-8'))
return SPAN(T('Enable'), _style='color:red') return SPAN(T('Enable'), _style='color:red')
@@ -642,7 +646,10 @@ def edit():
# show settings tab and save prefernces # show settings tab and save prefernces
if 'settings' in request.vars: if 'settings' in request.vars:
if request.post_vars: # save new preferences if request.post_vars: # save new preferences
post_vars = request.post_vars.items() if PY2:
post_vars = request.post_vars.items()
else:
post_vars = list(request.post_vars.items())
# Since unchecked checkbox are not serialized, we must set them as false by hand to store the correct preference in the settings # Since unchecked checkbox are not serialized, we must set them as false by hand to store the correct preference in the settings
post_vars += [(opt, 'false') for opt in preferences if opt not in request.post_vars] post_vars += [(opt, 'false') for opt in preferences if opt not in request.post_vars]
if config.save(post_vars): if config.save(post_vars):
@@ -29,5 +29,14 @@ jQuery(function(){
} }
hoverMenu(); // first page load hoverMenu(); // first page load
jQuery(window).resize(hoverMenu); // on resize event jQuery(window).resize(hoverMenu); // on resize event
jQuery('ul.nav li.dropdown a').click(function(){window.location=jQuery(this).attr('href');}); jQuery('ul.nav li.dropdown a').click(function(){
if(jQuery(this).attr("target")){
window.open(
jQuery(this).attr('href'),
jQuery(this).attr('target') // <- This is what makes it open in a new window.
);
} else {
window.location=jQuery(this).attr('href');
}
});
}); });
@@ -62,16 +62,16 @@
</center> </center>
<p style="text-align:left;"> <p style="text-align:left;">
The source code version works on all supported platforms, including Linux, but it requires Python 2.6, or 2.7 (recommended). The source code version works on Windows and most Unix systems, including <b>Linux</b>, <b>BSD</b> and <b>Mac</b> . It requires Python 2.6 (no more supported), Python 2.7 (stable) or Python 3.5+ (recommended for new projects) already installed on your system.
It runs on Windows and most Unix systems, including <b>Linux</b> and <b>BSD</b>. There are also binary packages for Windows and Mac OS X. They include the Python 2.7 interpreter so you do not need to have it pre-installed.
</p> </p>
<h3>Instructions</h3> <h3>Instructions</h3>
<p>After download, unzip it and click on web2py.exe (windows) or web2py.app (osx). <p>With the binary packages, after download, just unzip it and then click on web2py.exe (windows) or web2py.app (osx).
To run from source, type:</p> If you prefer to run it from source with your own Python interpreter alreay installed, type:</p>
{{=CODE("python2.7 web2py.py", language=None, counter='>', _class='boxCode')}} {{=CODE("python web2py.py", language=None, counter='>', _class='boxCode')}}
<p>or for more info type:</p> <p>or for more info type:</p>
{{=CODE("python2.7 web2py.py -h", language=None, counter='>', _class='boxCode')}} {{=CODE("python web2py.py -h", language=None, counter='>', _class='boxCode')}}
<h3>Caveats</h3> <h3>Caveats</h3>
@@ -4,7 +4,7 @@
<div class="twothirds"> <div class="twothirds">
<div class="padded"> <div class="padded">
<h3><img src="{{=URL('static/images', 'web2py_logo.png')}}"> Web Framework</h3> <h3><img src="{{=URL('static/images', 'web2py_logo.png')}}"> Web Framework</h3>
<p>Free open source full-stack framework for rapid development of fast, scalable, <a href="http://www.web2py.com/book/default/chapter/01#Security" target="_blank">secure</a> and portable database-driven web-based applications. Written and programmable in <a href="http://www.python.org" target="_blank">Python</a>.</p> <p>Free open source full-stack framework for rapid development of fast, scalable, <a href="http://www.web2py.com/book/default/chapter/01#Security" target="_blank">secure</a> and portable database-driven web-based applications. Written and programmable in <a href="http://www.python.org" target="_blank">Python</a> (version 3 and 2.7).</p>
<table width="100%"> <table width="100%">
<tr> <tr>
<td> <td>
@@ -18,7 +18,7 @@
</a> </a>
</td> </td>
<td> <td>
<a class="noeffect" href="http://link.packtpub.com/SUlnrN"> <a class="noeffect" href="https://www.packtpub.com/web-development/web2py-application-development-cookbook">
<img src="{{=URL('static','images/book-recipes.png')}}" /> <img src="{{=URL('static','images/book-recipes.png')}}" />
</a> </a>
</td> </td>
@@ -348,3 +348,13 @@ td.w2p_fc,
.icon.pen:before { content: "\f040";} .icon.pen:before { content: "\f040";}
.icon.arrowright:before { content: "\f061";} .icon.arrowright:before { content: "\f061";}
.icon.magnifier:before { content: "\f002";} .icon.magnifier:before { content: "\f002";}
.web2py_table_selectable_actions {
padding-top: 10px;
float: right;
}
.web2py_table_selectable_actions input {
padding: 5px 7px;
margin-right: 10px;
}
@@ -52,7 +52,7 @@
}); });
} }
var ul = this; var ul = this;
$(ul).find(":text").addClass('form-control').wrap("<div class='input-group'></div>").after('<div class="input-group-addon"><i class="glyphicon glyphicon-plus"></i></div><div class="input-group-addon"><i class="glyphicon glyphicon-minus"></i></div>').keypress(function(e) { $(ul).find(":text").addClass('form-control').wrap("<div class='input-group'></div>").after('<div class="input-group-append"><i class="fa fa-plus-circle"></i></div>&nbsp;<div class="input-group-append"><i class="fa fa-minus-circle"></i></div>').keypress(function(e) {
return (e.which == 13) ? pe(ul, e) : true; return (e.which == 13) ? pe(ul, e) : true;
}).next().click(function(e) { }).next().click(function(e) {
pe(ul, e); pe(ul, e);
@@ -1,7 +1,7 @@
{{extend 'layout.html'}} {{extend 'layout.html'}}
{{block header}} {{block header}}
<div class="jumbotron jumbotron-fluid" style="background-color: #333; color:white; padding:30px;word-wrap:break-word;"> <div class="jumbotron jumbotron-fluid background" style="background-color: #333; color:white; padding:30px;word-wrap:break-word;">
<div class="container center"> <div class="container center">
<h1 class="display-5">/{{=request.application}}/{{=request.controller}}/{{=request.function}}</h1> <h1 class="display-5">/{{=request.application}}/{{=request.controller}}/{{=request.function}}</h1>
</div> </div>
+1 -1
View File
@@ -35,7 +35,7 @@
<body> <body>
<div class="w2p_flash alert alert-dismissable">{{=response.flash or ''}}</div> <div class="w2p_flash alert alert-dismissable">{{=response.flash or ''}}</div>
<!-- Navbar ======================================= --> <!-- Navbar ======================================= -->
<nav class="navbar navbar-light navbar-expand-md bg-faded justify-content-center"> <nav class="navbar navbar-light navbar-expand-md bg-faded bg-dark navbar-dark justify-content-center">
<a href="http://web2py.com" class="navbar-brand d-flex w-50 mr-auto">web2py</a> <a href="http://web2py.com" class="navbar-brand d-flex w-50 mr-auto">web2py</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
+5 -2
View File
@@ -15,7 +15,7 @@ Note:
import re import re
import fnmatch import fnmatch
import os import os, sys
import copy import copy
import random import random
from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long, reload from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long, reload
@@ -52,7 +52,10 @@ is_gae = settings.global_settings.web2py_runtime_gae
is_jython = settings.global_settings.is_jython is_jython = settings.global_settings.is_jython
pjoin = os.path.join pjoin = os.path.join
marshal_header_size = 8 if PY2 else 12 if PY2:
marshal_header_size = 8
else:
marshal_header_size = 16 if sys.version_info[1] >= 7 else 12
TEST_CODE = \ TEST_CODE = \
r""" r"""
+2 -2
View File
@@ -180,7 +180,7 @@ class RedisClient(object):
self.r_server.incr('web2py_cache_statistics:misses') self.r_server.incr('web2py_cache_statistics:misses')
cache_set_key = self.cache_set_key cache_set_key = self.cache_set_key
expire_at = int(time.time() + time_expire) + 120 expire_at = int(time.time() + time_expire) + 120
bucket_key = "%s:%s" % (cache_set_key, expire_at / 60) bucket_key = "%s:%s" % (cache_set_key, expire_at // 60)
value = f() value = f()
value_ = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) value_ = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
if time_expire == 0: if time_expire == 0:
@@ -196,7 +196,7 @@ class RedisClient(object):
# add the key to the bucket # add the key to the bucket
p.sadd(bucket_key, key) p.sadd(bucket_key, key)
# expire the bucket properly # expire the bucket properly
p.expireat(bucket_key, ((expire_at / 60) + 1) * 60) p.expireat(bucket_key, ((expire_at // 60) + 1) * 60)
p.execute() p.execute()
return value return value
+14 -1
View File
@@ -1075,6 +1075,16 @@ class Session(Storage):
scookies['HttpOnly'] = True scookies['HttpOnly'] = True
if self._secure: if self._secure:
scookies['secure'] = True scookies['secure'] = True
if self._same_site is None:
# Using SameSite Lax Mode is the default
# You actually have to call session.samesite(False) if you really
# dont want the extra protection provided by the SameSite header
self._same_site = 'Lax'
if self._same_site:
if 'samesite' not in Cookie.Morsel._reserved:
# Python version 3.7 and lower needs this
Cookie.Morsel._reserved['samesite'] = 'SameSite'
scookies['samesite'] = self._same_site
def clear_session_cookies(self): def clear_session_cookies(self):
request = current.request request = current.request
@@ -1153,6 +1163,9 @@ class Session(Storage):
def secure(self): def secure(self):
self._secure = True self._secure = True
def samesite(self, mode='Lax'):
self._same_site = mode
def forget(self, response=None): def forget(self, response=None):
self._close(response) self._close(response)
self._forget = True self._forget = True
@@ -1180,7 +1193,7 @@ class Session(Storage):
def _unchanged(self, response): def _unchanged(self, response):
if response.session_new: if response.session_new:
internal = ['_last_timestamp', '_secure', '_start_timestamp'] internal = ['_last_timestamp', '_secure', '_start_timestamp', '_same_site']
for item in self.keys(): for item in self.keys():
if item not in internal: if item not in internal:
return False return False
+5 -5
View File
@@ -311,7 +311,7 @@ def write_plural_dict(filename, contents):
try: try:
fp = LockedFile(filename, 'w') fp = LockedFile(filename, 'w')
fp.write('#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n{\n# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...],\n') fp.write('#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n{\n# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...],\n')
for key in sorted(contents, sort_function): for key in sorted(contents, key=sort_function):
forms = '[' + ','.join([repr(Utf8(form)) forms = '[' + ','.join([repr(Utf8(form))
for form in contents[key]]) + ']' for form in contents[key]]) + ']'
fp.write('%s: %s,\n' % (repr(Utf8(key)), forms)) fp.write('%s: %s,\n' % (repr(Utf8(key)), forms))
@@ -325,8 +325,8 @@ def write_plural_dict(filename, contents):
fp.close() fp.close()
def sort_function(x, y): def sort_function(x):
return cmp(unicode(x, 'utf-8').lower(), unicode(y, 'utf-8').lower()) return unicode(x, 'utf-8').lower()
def write_dict(filename, contents): def write_dict(filename, contents):
@@ -936,8 +936,8 @@ class translator(object):
word = w[1:] word = w[1:]
fun = cap_fun fun = cap_fun
if i is not None: if i is not None:
return fun(self.plural(word, symbols[int(i)])) return to_native(fun(self.plural(word, symbols[int(i)])))
return fun(word) return to_native(fun(word))
def sub_dict(m): def sub_dict(m):
""" word(key or num) """ word(key or num)
+2 -3
View File
@@ -31,7 +31,7 @@ from gluon._compat import Cookie, urllib2
from gluon.fileutils import abspath, write_file from gluon.fileutils import abspath, write_file
from gluon.settings import global_settings from gluon.settings import global_settings
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid, unlocalised_http_header_date
from gluon.admin import add_path_first, create_missing_folders, create_missing_app_folders from gluon.admin import add_path_first, create_missing_folders, create_missing_app_folders
from gluon.globals import current from gluon.globals import current
@@ -199,8 +199,7 @@ def serve_controller(request, response, session):
('Content-Type', contenttype('.' + request.extension)), ('Content-Type', contenttype('.' + request.extension)),
('Cache-Control', ('Cache-Control',
'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'), 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'),
('Expires', time.strftime('%a, %d %b %Y %H:%M:%S GMT', ('Expires', unlocalised_http_header_date(time.gmtime())),
time.gmtime())),
('Pragma', 'no-cache')] ('Pragma', 'no-cache')]
for key, value in default_headers: for key, value in default_headers:
response.headers.setdefault(key, value) response.headers.setdefault(key, value)
+52 -17
View File
@@ -804,6 +804,7 @@ class Scheduler(MetaScheduler):
Field('group_name', default='main'), Field('group_name', default='main'),
Field('status', requires=IS_IN_SET(TASK_STATUS), Field('status', requires=IS_IN_SET(TASK_STATUS),
default=QUEUED, writable=False), default=QUEUED, writable=False),
Field('broadcast', 'boolean', default=False),
Field('function_name', Field('function_name',
requires=IS_IN_SET(sorted(self.tasks.keys())) requires=IS_IN_SET(sorted(self.tasks.keys()))
if self.tasks else DEFAULT), if self.tasks else DEFAULT),
@@ -1155,6 +1156,15 @@ class Scheduler(MetaScheduler):
- does "housecleaning" for dead workers - does "housecleaning" for dead workers
- triggers tasks assignment to workers - triggers tasks assignment to workers
""" """
if self.db_thread:
# BKR 20180612 check if connection still works
try:
query = self.db_thread.scheduler_worker.worker_name == self.worker_name
self.db_thread(query).count()
except self.db_thread._adapter.connection.OperationalError:
# if not -> throw away self.db_thread and force reconnect
self.db_thread = None
if not self.db_thread: if not self.db_thread:
logger.debug('thread building own DAL object') logger.debug('thread building own DAL object')
self.db_thread = DAL( self.db_thread = DAL(
@@ -1358,23 +1368,48 @@ class Scheduler(MetaScheduler):
gname = task.group_name gname = task.group_name
ws = wkgroups.get(gname) ws = wkgroups.get(gname)
if ws: if ws:
counter = 0 if task.broadcast:
myw = 0 for worker in ws['workers']:
for i, w in enumerate(ws['workers']): new_task = db.scheduler_task.insert(
if w['c'] < counter: application_name = task.application_name,
myw = i task_name = task.task_name,
counter = w['c'] group_name = task.group_name,
assigned_wn = wkgroups[gname]['workers'][myw]['name'] status = ASSIGNED,
d = dict( broadcast = False,
status=ASSIGNED, function_name = task.function_name,
assigned_worker_name=assigned_wn args = task.args,
) start_time = now,
db( repeats = 1,
(st.id == task.id) & retry_failed = task.retry_failed,
(st.status.belongs((QUEUED, ASSIGNED))) sync_output = task.sync_output,
).update(**d) assigned_worker_name = worker['name'])
wkgroups[gname]['workers'][myw]['c'] += 1 if task.period:
db.commit() next_run_time = now+datetime.timedelta(seconds=task.period)
else:
# must be cronline
raise NotImplementedError
db(st.id == task.id).update(times_run=task.times_run+1,
next_run_time=next_run_time,
last_run_time=now)
db.commit()
else:
counter = 0
myw = 0
for i, w in enumerate(ws['workers']):
if w['c'] < counter:
myw = i
counter = w['c']
assigned_wn = wkgroups[gname]['workers'][myw]['name']
d = dict(
status=ASSIGNED,
assigned_worker_name=assigned_wn
)
db(
(st.id == task.id) &
(st.status.belongs((QUEUED, ASSIGNED)))
).update(**d)
wkgroups[gname]['workers'][myw]['c'] += 1
db.commit()
# I didn't report tasks but I'm working nonetheless!!!! # I didn't report tasks but I'm working nonetheless!!!!
if x > 0: if x > 0:
self.w_stats.empty_runs = 0 self.w_stats.empty_runs = 0
+22 -18
View File
@@ -699,18 +699,20 @@ class AutocompleteWidget(object):
if isinstance(field, Field.Virtual): if isinstance(field, Field.Virtual):
records = [] records = []
table_rows = self.db(self.db[field.tablename]).select(orderby=self.orderby) table_rows = self.db(self.db[field.tablename]).select(orderby=self.orderby)
count = 0 count = self.limitby[1] if self.limitby else -1
for row in table_rows: for row in table_rows:
if self.at_beginning: if self.at_beginning:
if row[field.name].lower().startswith(kword): if row[field.name].lower().startswith(kword):
count += 1 count -= 1
records.append(row) records.append(row)
else: else:
if kword in row[field.name].lower(): if kword in row[field.name].lower():
count += 1 count -= 1
records.append(row) records.append(row)
if count == 10: if count == 0:
break break
if self.limitby and self.limitby[0]:
records = records[self.limitby[0]:]
rows = Rows(self.db, records, table_rows.colnames, rows = Rows(self.db, records, table_rows.colnames,
compact=table_rows.compact) compact=table_rows.compact)
elif settings and settings.global_settings.web2py_runtime_gae: elif settings and settings.global_settings.web2py_runtime_gae:
@@ -1090,7 +1092,7 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
# bootstrap 4 # bootstrap 4
def formstyle_bootstrap4_stacked(form, fields): def formstyle_bootstrap4_stacked(form, fields):
""" bootstrap 3 format form layout """ bootstrap 4 format form layout
Note: Note:
Experimental! Experimental!
@@ -1139,7 +1141,7 @@ def formstyle_bootstrap4_stacked(form, fields):
def formstyle_bootstrap4_inline_factory(col_label_size=3): def formstyle_bootstrap4_inline_factory(col_label_size=3):
""" bootstrap 3 horizontal form layout """ bootstrap 4 horizontal form layout
Note: Note:
Experimental! Experimental!
@@ -1349,7 +1351,7 @@ class SQLFORM(FORM):
if not readonly: if not readonly:
if not record: if not record:
# create form should only show writable fields # create form should only show writable fields
fields = [f.name for f in table if (ignore_rw or f.writable) and not f.compute] fields = [f.name for f in table if (ignore_rw or f.writable or (f.readable and f.default)) and not f.compute]
else: else:
# update form should also show readable fields and computed fields (but in reaodnly mode) # update form should also show readable fields and computed fields (but in reaodnly mode)
fields = [f.name for f in table if (ignore_rw or f.writable or f.readable)] fields = [f.name for f in table if (ignore_rw or f.writable or f.readable)]
@@ -2021,7 +2023,7 @@ class SQLFORM(FORM):
to hold the fields. to hold the fields.
""" """
# this is here to avoid circular references # this is here to avoid circular references
from gluon.dal import DAL from gluon.dal import DAL, _default_validators
# Define a table name, this way it can be logical to our CSS. # Define a table name, this way it can be logical to our CSS.
# And if you switch from using SQLFORM to SQLFORM.factory # And if you switch from using SQLFORM to SQLFORM.factory
# your same css definitions will still apply. # your same css definitions will still apply.
@@ -2034,8 +2036,9 @@ class SQLFORM(FORM):
# Clone fields, while passing tables straight through # Clone fields, while passing tables straight through
fields_with_clones = [f.clone() if isinstance(f, Field) else f for f in fields] fields_with_clones = [f.clone() if isinstance(f, Field) else f for f in fields]
dummy_dal = DAL(None)
return SQLFORM(DAL(None).define_table(table_name, *fields_with_clones), **attributes) dummy_dal.validators_method = lambda f: _default_validators(dummy_dal, f) # See https://github.com/web2py/web2py/issues/2007
return SQLFORM(dummy_dal.define_table(table_name, *fields_with_clones), **attributes)
@staticmethod @staticmethod
def build_query(fields, keywords): def build_query(fields, keywords):
@@ -2312,7 +2315,7 @@ class SQLFORM(FORM):
button='button btn btn-default btn-secondary', button='button btn btn-default btn-secondary',
buttontext='buttontext button', buttontext='buttontext button',
buttonadd='icon plus icon-plus glyphicon glyphicon-plus', buttonadd='icon plus icon-plus glyphicon glyphicon-plus',
buttonback='icon leftarrow icon-arrow-left glyphicon glyphicon-arrow-left', buttonback='icon arrowleft icon-arrow-left glyphicon glyphicon-arrow-left',
buttonexport='icon downarrow icon-download glyphicon glyphicon-download', buttonexport='icon downarrow icon-download glyphicon glyphicon-download',
buttondelete='icon trash icon-trash glyphicon glyphicon-trash', buttondelete='icon trash icon-trash glyphicon glyphicon-trash',
buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil', buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil',
@@ -2690,13 +2693,13 @@ class SQLFORM(FORM):
dbset = dbset(SQLFORM.build_query( dbset = dbset(SQLFORM.build_query(
sfields, keywords)) sfields, keywords))
rows = dbset.select(left=left, orderby=orderby, rows = dbset.select(left=left, orderby=orderby,
cacheable=True, *selectable_columns) cacheable=True, *expcolumns)
except Exception as e: except Exception as e:
response.flash = T('Internal Error') response.flash = T('Internal Error')
rows = [] rows = []
else: else:
rows = dbset.select(left=left, orderby=orderby, rows = dbset.select(left=left, orderby=orderby,
cacheable=True, *selectable_columns) cacheable=True, *expcolumns)
value = exportManager[export_type] value = exportManager[export_type]
clazz = value[0] if hasattr(value, '__getitem__') else value clazz = value[0] if hasattr(value, '__getitem__') else value
@@ -2855,9 +2858,9 @@ class SQLFORM(FORM):
if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
cursor = request.vars.cursor or True cursor = request.vars.cursor or True
limitby = (0, paginate) limitby = (0, paginate)
page = safe_int(request.vars.page, 1) - 1 page = safe_int(request.vars.page or 1, 1) - 1
elif paginate and paginate < nrows: elif paginate and paginate < nrows:
page = safe_int(request.vars.page, 1) - 1 page = safe_int(request.vars.page or 1, 1) - 1
limitby = (paginate * page, paginate * (page + 1)) limitby = (paginate * page, paginate * (page + 1))
else: else:
limitby = None limitby = None
@@ -2899,7 +2902,7 @@ class SQLFORM(FORM):
paginator = UL() paginator = UL()
if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor: 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. # this means we may have a large table with an unknown number of rows.
page = safe_int(request.vars.page, 1) - 1 page = safe_int(request.vars.page or 1, 1) - 1
paginator.append(LI('page %s' % (page + 1))) paginator.append(LI('page %s' % (page + 1)))
if next_cursor: if next_cursor:
d = dict(page=page + 2, cursor=next_cursor) d = dict(page=page + 2, cursor=next_cursor)
@@ -2918,7 +2921,7 @@ class SQLFORM(FORM):
npages, reminder = divmod(nrows, paginate) npages, reminder = divmod(nrows, paginate)
if reminder: if reminder:
npages += 1 npages += 1
page = safe_int(request.vars.page, 1) - 1 page = safe_int(request.vars.page or 1, 1) - 1
def self_link(name, p): def self_link(name, p):
d = dict(page=p + 1) d = dict(page=p + 1)
@@ -3089,8 +3092,9 @@ class SQLFORM(FORM):
if formstyle == 'bootstrap': if formstyle == 'bootstrap':
# add space between buttons # add space between buttons
# inputs = sum([[inp, ' '] for inp in inputs], [])[:-1]
htmltable = FORM(htmltable, DIV(_class='form-actions', *inputs)) htmltable = FORM(htmltable, DIV(_class='form-actions', *inputs))
elif 'bootstrap' in formstyle : # Same for bootstrap 3 & 4
htmltable = FORM(htmltable, DIV(_class='form-group web2py_table_selectable_actions', *inputs))
else: else:
htmltable = FORM(htmltable, *inputs) htmltable = FORM(htmltable, *inputs)
+2 -1
View File
@@ -16,6 +16,7 @@ import time
import re import re
import errno import errno
from gluon.http import HTTP from gluon.http import HTTP
from gluon.utils import unlocalised_http_header_date
from gluon.contenttype import contenttype from gluon.contenttype import contenttype
from gluon._compat import PY2 from gluon._compat import PY2
@@ -74,7 +75,7 @@ def stream_file_or_304_or_206(
stat_file = os.stat(static_file) stat_file = os.stat(static_file)
fsize = stat_file[stat.ST_SIZE] fsize = stat_file[stat.ST_SIZE]
modified = stat_file[stat.ST_MTIME] modified = stat_file[stat.ST_MTIME]
mtime = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(modified)) mtime = unlocalised_http_header_date(time.gmtime(modified))
headers.setdefault('Content-Type', contenttype(static_file)) headers.setdefault('Content-Type', contenttype(static_file))
headers.setdefault('Last-Modified', mtime) headers.setdefault('Last-Modified', mtime)
headers.setdefault('Pragma', 'cache') headers.setdefault('Pragma', 'cache')
+2 -2
View File
@@ -18,7 +18,7 @@ import os
import cgi import cgi
import logging import logging
from re import compile, sub, escape, DOTALL from re import compile, sub, escape, DOTALL
from gluon._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native from gluon._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native, basestring
try: try:
# have web2py # have web2py
@@ -778,7 +778,7 @@ def parse_template(filename,
""" """
# First, if we have a str try to open the file # First, if we have a str try to open the file
if isinstance(filename, str): if isinstance(filename, basestring):
fname = os.path.join(path, filename) fname = os.path.join(path, filename)
try: try:
with open(fname, 'rb') as fp: with open(fname, 'rb') as fp:
+19
View File
@@ -231,6 +231,25 @@ class testResponse(unittest.TestCase):
cookie = str(current.response.cookies) cookie = str(current.response.cookies)
self.assertTrue('httponly' not in cookie.lower()) self.assertTrue('httponly' not in cookie.lower())
def test_cookies_samesite(self):
# Test Lax is the default mode
current = setup_clean_session()
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('samesite=lax' in cookie.lower())
# Test you can disable samesite
current = setup_clean_session()
current.session.samesite(False)
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('samesite' not in cookie.lower())
# Test you can change mode
current = setup_clean_session()
current.session.samesite('Strict')
current.session._fixup_before_save()
cookie = str(current.response.cookies)
self.assertTrue('samesite=strict' in cookie.lower())
def test_include_meta(self): def test_include_meta(self):
response = Response() response = Response()
response.meta[u'web2py'] = 'web2py' response.meta[u'web2py'] = 'web2py'
+21 -1
View File
@@ -4,6 +4,7 @@
""" """
Unit tests for gluon.sqlhtml Unit tests for gluon.sqlhtml
""" """
import datetime
import os import os
import sys import sys
import unittest import unittest
@@ -312,12 +313,31 @@ class TestSQLFORM(unittest.TestCase):
Field('field_two', 'string')) Field('field_two', 'string'))
self.assertEqual(factory_form.xml()[:5], b'<form') self.assertEqual(factory_form.xml()[:5], b'<form')
def test_factory_applies_default_validators(self):
from gluon import current
factory_form = SQLFORM.factory(
Field('a_date', type='date'),
)
# Fake user input
current.request.post_vars.update({
'_formname': 'no_table/create',
'a_date': '2018-09-14',
'_formkey': '123',
})
# Fake the formkey
current.session['_formkey[no_table/create]'] = ['123']
self.assertTrue(factory_form.process().accepted)
self.assertIsInstance(factory_form.vars.a_date, datetime.date)
# def test_build_query(self): # def test_build_query(self):
# pass # pass
# def test_search_menu(self): # def test_search_menu(self):
# pass # pass
def test_grid(self): def test_grid(self):
grid_form = SQLFORM.grid(self.db.auth_user) grid_form = SQLFORM.grid(self.db.auth_user)
self.assertEqual(grid_form.xml()[:4], b'<div') self.assertEqual(grid_form.xml()[:4], b'<div')
+8 -3
View File
@@ -1378,6 +1378,7 @@ class Auth(AuthAPI):
login_after_password_change=True, login_after_password_change=True,
login_after_registration=False, login_after_registration=False,
login_captcha=None, login_captcha=None,
login_specify_error=False,
long_expiration=3600 * 30 * 24, # one month long_expiration=3600 * 30 * 24, # one month
mailer=None, mailer=None,
manager_actions={}, manager_actions={},
@@ -2567,6 +2568,8 @@ class Auth(AuthAPI):
settings.formstyle, 'captcha__row') settings.formstyle, 'captcha__row')
accepted_form = False accepted_form = False
specific_error = self.messages.invalid_user
if form.accepts(request, session if self.csrf_prevention else None, if form.accepts(request, session if self.csrf_prevention else None,
formname='login', dbio=False, formname='login', dbio=False,
onvalidation=onvalidation, onvalidation=onvalidation,
@@ -2582,6 +2585,7 @@ class Auth(AuthAPI):
user = table_user(**{username: entered_username}) user = table_user(**{username: entered_username})
if user: if user:
# user in db, check if registration pending or disabled # user in db, check if registration pending or disabled
specific_error = self.messages.invalid_password
temp_user = user temp_user = user
if (temp_user.registration_key or '').startswith('pending'): if (temp_user.registration_key or '').startswith('pending'):
response.flash = self.messages.registration_pending response.flash = self.messages.registration_pending
@@ -2631,7 +2635,7 @@ class Auth(AuthAPI):
self.log_event(self.messages['login_failed_log'], self.log_event(self.messages['login_failed_log'],
request.post_vars) request.post_vars)
# invalid login # invalid login
session.flash = self.messages.invalid_login session.flash = specific_error if self.settings.login_specify_error else self.messages.invalid_login
callback(onfail, None) callback(onfail, None)
redirect( redirect(
self.url(args=request.args, vars=request.get_vars), self.url(args=request.args, vars=request.get_vars),
@@ -3447,7 +3451,8 @@ class Auth(AuthAPI):
if log is DEFAULT: if log is DEFAULT:
log = self.messages['reset_password_log'] log = self.messages['reset_password_log']
userfield = self.settings.login_userfield or 'username' \ userfield = self.settings.login_userfield or 'username' \
if 'username' in table_user.fields else 'email' if self.settings.login_userfield or 'username' \
in table_user.fields else 'email'
if userfield == 'email': if userfield == 'email':
table_user.email.requires = [ table_user.email.requires = [
IS_EMAIL(error_message=self.messages.invalid_email), IS_EMAIL(error_message=self.messages.invalid_email),
@@ -3455,7 +3460,7 @@ class Auth(AuthAPI):
error_message=self.messages.invalid_email)] error_message=self.messages.invalid_email)]
if not self.settings.email_case_sensitive: if not self.settings.email_case_sensitive:
table_user.email.requires.insert(0, IS_LOWER()) table_user.email.requires.insert(0, IS_LOWER())
else: elif userfield == 'username':
table_user.username.requires = [ table_user.username.requires = [
IS_IN_DB(self.db, table_user.username, IS_IN_DB(self.db, table_user.username,
error_message=self.messages.invalid_username)] error_message=self.messages.invalid_username)]
+48
View File
@@ -462,3 +462,51 @@ def local_html_escape(data, quote=False):
data = data.replace(b'"', b"&quot;") data = data.replace(b'"', b"&quot;")
data = data.replace(b'\'', b"&#x27;") data = data.replace(b'\'', b"&#x27;")
return data return data
def unlocalised_http_header_date(data):
"""
Converts input datetime to format defined by RFC 7231, section 7.1.1.1
Previously, %a and %b formats were used for weekday and month names, but
those are not locale-safe. uWSGI requires latin1-encodable headers and
for example in cs_CS locale, fourth day in week is not encodable in latin1,
as it's "Čt".
Example output: Sun, 06 Nov 1994 08:49:37 GMT
"""
short_weekday = {
"0": "Sun",
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat",
}.get(time.strftime("%w", data))
day_of_month = time.strftime("%d", data)
short_month = {
"01": "Jan",
"02": "Feb",
"03": "Mar",
"04": "Apr",
"05": "May",
"06": "Jun",
"07": "Jul",
"08": "Aug",
"09": "Sep",
"10": "Oct",
"11": "Nov",
"12": "Dec",
}.get(time.strftime("%m", data))
year_and_time = time.strftime("%Y %H:%M:%S GMT")
return "{}, {} {} {}".format(
short_weekday,
day_of_month,
short_month,
year_and_time)
+5 -1
View File
@@ -7,8 +7,12 @@
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
""" """
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher from gluon._compat import PY2
if PY2:
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
else:
from xmlrpc.server import SimpleXMLRPCDispatcher
def handler(request, response, methods): def handler(request, response, methods):
response.session_id = None # no sessions for xmlrpc response.session_id = None # no sessions for xmlrpc
+1 -1
View File
@@ -48,7 +48,7 @@ def main():
global REQUIRED, IGNORED global REQUIRED, IGNORED
if len(sys.argv) < 2: if len(sys.argv) < 2:
print USAGE print(USAGE)
# make target folder # make target folder
target = sys.argv[1] target = sys.argv[1]