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
- many small bug fixes
+1 -1
View File
@@ -44,7 +44,7 @@ rmfiles:
rm -rf applications/examples/uploads/*
src:
### 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
#make clean
# 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:
if debug:
print "WAITING FOR REQUEST"
print("WAITING FOR REQUEST")
# receive a request
req = conn.recv()
if debug:
print "REQUEST BODY: %r\n" % req.body
print("REQUEST BODY: %r\n" % req.body)
if req.is_disconnect():
if debug:
print "DISCONNECT"
print("DISCONNECT")
continue # effectively ignore the disconnect from the client
# 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
if debug:
print "ENVIRON: %r\n" % environ
print("ENVIRON: %r\n" % environ)
# SimpleHandler needs file-like stream objects for
# requests, errors and responses
@@ -282,10 +282,10 @@ def mongrel2_handler(application, conn, debug=False):
# return the response
if debug:
print "RESPONSE: %r\n" % response
print("RESPONSE: %r\n" % response)
if errors:
if debug:
print "ERRORS: %r" % errors
print("ERRORS: %r" % errors)
data = "%s\r\n\r\n%s" % (data, errors)
conn.reply_http(
req, data, code=code, status=status, headers=headers)
@@ -355,8 +355,8 @@ def main():
dest='workers',
help='number of workers number')
(options, args) = parser.parse_args()
print 'starting %s on %s:%s...' % (
options.server, options.ip, options.port)
print('starting %s on %s:%s...' % (
options.server, options.ip, options.port))
run(options.server, options.ip, options.port,
logging=options.logging, profiler=options.profiler_dir,
options=options)
+9 -2
View File
@@ -562,7 +562,11 @@ def enable():
os.unlink(filename)
return SPAN(T('Disable'), _style='color:green')
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')
@@ -642,7 +646,10 @@ def edit():
# show settings tab and save prefernces
if 'settings' in request.vars:
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
post_vars += [(opt, 'false') for opt in preferences if opt not in request.post_vars]
if config.save(post_vars):
@@ -29,5 +29,14 @@ jQuery(function(){
}
hoverMenu(); // first page load
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>
<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).
It runs on Windows and most Unix systems, including <b>Linux</b> and <b>BSD</b>.
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.
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>
<h3>Instructions</h3>
<p>After download, unzip it and click on web2py.exe (windows) or web2py.app (osx).
To run from source, type:</p>
{{=CODE("python2.7 web2py.py", language=None, counter='>', _class='boxCode')}}
<p>With the binary packages, after download, just unzip it and then click on web2py.exe (windows) or web2py.app (osx).
If you prefer to run it from source with your own Python interpreter alreay installed, type:</p>
{{=CODE("python web2py.py", language=None, counter='>', _class='boxCode')}}
<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>
@@ -4,7 +4,7 @@
<div class="twothirds">
<div class="padded">
<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%">
<tr>
<td>
@@ -18,7 +18,7 @@
</a>
</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')}}" />
</a>
</td>
@@ -348,3 +348,13 @@ td.w2p_fc,
.icon.pen:before { content: "\f040";}
.icon.arrowright:before { content: "\f061";}
.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;
$(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;
}).next().click(function(e) {
pe(ul, e);
@@ -1,7 +1,7 @@
{{extend 'layout.html'}}
{{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">
<h1 class="display-5">/{{=request.application}}/{{=request.controller}}/{{=request.function}}</h1>
</div>
+1 -1
View File
@@ -35,7 +35,7 @@
<body>
<div class="w2p_flash alert alert-dismissable">{{=response.flash or ''}}</div>
<!-- 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>
<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>
+5 -2
View File
@@ -15,7 +15,7 @@ Note:
import re
import fnmatch
import os
import os, sys
import copy
import random
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
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 = \
r"""
+2 -2
View File
@@ -180,7 +180,7 @@ class RedisClient(object):
self.r_server.incr('web2py_cache_statistics:misses')
cache_set_key = self.cache_set_key
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_ = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
if time_expire == 0:
@@ -196,7 +196,7 @@ class RedisClient(object):
# add the key to the bucket
p.sadd(bucket_key, key)
# expire the bucket properly
p.expireat(bucket_key, ((expire_at / 60) + 1) * 60)
p.expireat(bucket_key, ((expire_at // 60) + 1) * 60)
p.execute()
return value
+14 -1
View File
@@ -1075,6 +1075,16 @@ class Session(Storage):
scookies['HttpOnly'] = True
if self._secure:
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):
request = current.request
@@ -1153,6 +1163,9 @@ class Session(Storage):
def secure(self):
self._secure = True
def samesite(self, mode='Lax'):
self._same_site = mode
def forget(self, response=None):
self._close(response)
self._forget = True
@@ -1180,7 +1193,7 @@ class Session(Storage):
def _unchanged(self, response):
if response.session_new:
internal = ['_last_timestamp', '_secure', '_start_timestamp']
internal = ['_last_timestamp', '_secure', '_start_timestamp', '_same_site']
for item in self.keys():
if item not in internal:
return False
+5 -5
View File
@@ -311,7 +311,7 @@ def write_plural_dict(filename, contents):
try:
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')
for key in sorted(contents, sort_function):
for key in sorted(contents, key=sort_function):
forms = '[' + ','.join([repr(Utf8(form))
for form in contents[key]]) + ']'
fp.write('%s: %s,\n' % (repr(Utf8(key)), forms))
@@ -325,8 +325,8 @@ def write_plural_dict(filename, contents):
fp.close()
def sort_function(x, y):
return cmp(unicode(x, 'utf-8').lower(), unicode(y, 'utf-8').lower())
def sort_function(x):
return unicode(x, 'utf-8').lower()
def write_dict(filename, contents):
@@ -936,8 +936,8 @@ class translator(object):
word = w[1:]
fun = cap_fun
if i is not None:
return fun(self.plural(word, symbols[int(i)]))
return fun(word)
return to_native(fun(self.plural(word, symbols[int(i)])))
return to_native(fun(word))
def sub_dict(m):
""" 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.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.globals import current
@@ -199,8 +199,7 @@ def serve_controller(request, response, session):
('Content-Type', contenttype('.' + request.extension)),
('Cache-Control',
'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'),
('Expires', time.strftime('%a, %d %b %Y %H:%M:%S GMT',
time.gmtime())),
('Expires', unlocalised_http_header_date(time.gmtime())),
('Pragma', 'no-cache')]
for key, value in default_headers:
response.headers.setdefault(key, value)
+52 -17
View File
@@ -804,6 +804,7 @@ class Scheduler(MetaScheduler):
Field('group_name', default='main'),
Field('status', requires=IS_IN_SET(TASK_STATUS),
default=QUEUED, writable=False),
Field('broadcast', 'boolean', default=False),
Field('function_name',
requires=IS_IN_SET(sorted(self.tasks.keys()))
if self.tasks else DEFAULT),
@@ -1155,6 +1156,15 @@ class Scheduler(MetaScheduler):
- does "housecleaning" for dead 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:
logger.debug('thread building own DAL object')
self.db_thread = DAL(
@@ -1358,23 +1368,48 @@ class Scheduler(MetaScheduler):
gname = task.group_name
ws = wkgroups.get(gname)
if ws:
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()
if task.broadcast:
for worker in ws['workers']:
new_task = db.scheduler_task.insert(
application_name = task.application_name,
task_name = task.task_name,
group_name = task.group_name,
status = ASSIGNED,
broadcast = False,
function_name = task.function_name,
args = task.args,
start_time = now,
repeats = 1,
retry_failed = task.retry_failed,
sync_output = task.sync_output,
assigned_worker_name = worker['name'])
if task.period:
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!!!!
if x > 0:
self.w_stats.empty_runs = 0
+22 -18
View File
@@ -699,18 +699,20 @@ class AutocompleteWidget(object):
if isinstance(field, Field.Virtual):
records = []
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:
if self.at_beginning:
if row[field.name].lower().startswith(kword):
count += 1
count -= 1
records.append(row)
else:
if kword in row[field.name].lower():
count += 1
count -= 1
records.append(row)
if count == 10:
if count == 0:
break
if self.limitby and self.limitby[0]:
records = records[self.limitby[0]:]
rows = Rows(self.db, records, table_rows.colnames,
compact=table_rows.compact)
elif settings and settings.global_settings.web2py_runtime_gae:
@@ -1090,7 +1092,7 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
# bootstrap 4
def formstyle_bootstrap4_stacked(form, fields):
""" bootstrap 3 format form layout
""" bootstrap 4 format form layout
Note:
Experimental!
@@ -1139,7 +1141,7 @@ def formstyle_bootstrap4_stacked(form, fields):
def formstyle_bootstrap4_inline_factory(col_label_size=3):
""" bootstrap 3 horizontal form layout
""" bootstrap 4 horizontal form layout
Note:
Experimental!
@@ -1349,7 +1351,7 @@ class SQLFORM(FORM):
if not readonly:
if not record:
# 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:
# 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)]
@@ -2021,7 +2023,7 @@ class SQLFORM(FORM):
to hold the fields.
"""
# 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.
# And if you switch from using SQLFORM to SQLFORM.factory
# your same css definitions will still apply.
@@ -2034,8 +2036,9 @@ class SQLFORM(FORM):
# Clone fields, while passing tables straight through
fields_with_clones = [f.clone() if isinstance(f, Field) else f for f in fields]
return SQLFORM(DAL(None).define_table(table_name, *fields_with_clones), **attributes)
dummy_dal = DAL(None)
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
def build_query(fields, keywords):
@@ -2312,7 +2315,7 @@ class SQLFORM(FORM):
button='button btn btn-default btn-secondary',
buttontext='buttontext button',
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',
buttondelete='icon trash icon-trash glyphicon glyphicon-trash',
buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil',
@@ -2690,13 +2693,13 @@ class SQLFORM(FORM):
dbset = dbset(SQLFORM.build_query(
sfields, keywords))
rows = dbset.select(left=left, orderby=orderby,
cacheable=True, *selectable_columns)
cacheable=True, *expcolumns)
except Exception as e:
response.flash = T('Internal Error')
rows = []
else:
rows = dbset.select(left=left, orderby=orderby,
cacheable=True, *selectable_columns)
cacheable=True, *expcolumns)
value = exportManager[export_type]
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:
cursor = request.vars.cursor or True
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:
page = safe_int(request.vars.page, 1) - 1
page = safe_int(request.vars.page or 1, 1) - 1
limitby = (paginate * page, paginate * (page + 1))
else:
limitby = None
@@ -2899,7 +2902,7 @@ class SQLFORM(FORM):
paginator = UL()
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.
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)))
if next_cursor:
d = dict(page=page + 2, cursor=next_cursor)
@@ -2918,7 +2921,7 @@ class SQLFORM(FORM):
npages, reminder = divmod(nrows, paginate)
if reminder:
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):
d = dict(page=p + 1)
@@ -3089,8 +3092,9 @@ class SQLFORM(FORM):
if formstyle == 'bootstrap':
# add space between buttons
# inputs = sum([[inp, ' '] for inp in inputs], [])[:-1]
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:
htmltable = FORM(htmltable, *inputs)
+2 -1
View File
@@ -16,6 +16,7 @@ import time
import re
import errno
from gluon.http import HTTP
from gluon.utils import unlocalised_http_header_date
from gluon.contenttype import contenttype
from gluon._compat import PY2
@@ -74,7 +75,7 @@ def stream_file_or_304_or_206(
stat_file = os.stat(static_file)
fsize = stat_file[stat.ST_SIZE]
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('Last-Modified', mtime)
headers.setdefault('Pragma', 'cache')
+2 -2
View File
@@ -18,7 +18,7 @@ import os
import cgi
import logging
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:
# have web2py
@@ -778,7 +778,7 @@ def parse_template(filename,
"""
# 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)
try:
with open(fname, 'rb') as fp:
+19
View File
@@ -231,6 +231,25 @@ class testResponse(unittest.TestCase):
cookie = str(current.response.cookies)
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):
response = Response()
response.meta[u'web2py'] = 'web2py'
+21 -1
View File
@@ -4,6 +4,7 @@
"""
Unit tests for gluon.sqlhtml
"""
import datetime
import os
import sys
import unittest
@@ -312,12 +313,31 @@ class TestSQLFORM(unittest.TestCase):
Field('field_two', 'string'))
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):
# pass
# def test_search_menu(self):
# pass
def test_grid(self):
grid_form = SQLFORM.grid(self.db.auth_user)
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_registration=False,
login_captcha=None,
login_specify_error=False,
long_expiration=3600 * 30 * 24, # one month
mailer=None,
manager_actions={},
@@ -2567,6 +2568,8 @@ class Auth(AuthAPI):
settings.formstyle, 'captcha__row')
accepted_form = False
specific_error = self.messages.invalid_user
if form.accepts(request, session if self.csrf_prevention else None,
formname='login', dbio=False,
onvalidation=onvalidation,
@@ -2582,6 +2585,7 @@ class Auth(AuthAPI):
user = table_user(**{username: entered_username})
if user:
# user in db, check if registration pending or disabled
specific_error = self.messages.invalid_password
temp_user = user
if (temp_user.registration_key or '').startswith('pending'):
response.flash = self.messages.registration_pending
@@ -2631,7 +2635,7 @@ class Auth(AuthAPI):
self.log_event(self.messages['login_failed_log'],
request.post_vars)
# 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)
redirect(
self.url(args=request.args, vars=request.get_vars),
@@ -3447,7 +3451,8 @@ class Auth(AuthAPI):
if log is DEFAULT:
log = self.messages['reset_password_log']
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':
table_user.email.requires = [
IS_EMAIL(error_message=self.messages.invalid_email),
@@ -3455,7 +3460,7 @@ class Auth(AuthAPI):
error_message=self.messages.invalid_email)]
if not self.settings.email_case_sensitive:
table_user.email.requires.insert(0, IS_LOWER())
else:
elif userfield == 'username':
table_user.username.requires = [
IS_IN_DB(self.db, table_user.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"&#x27;")
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)
"""
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):
response.session_id = None # no sessions for xmlrpc
+1 -1
View File
@@ -48,7 +48,7 @@ def main():
global REQUIRED, IGNORED
if len(sys.argv) < 2:
print USAGE
print(USAGE)
# make target folder
target = sys.argv[1]