Merge branch 'master' of github.com:web2py/web2py
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+14
-1
@@ -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
@@ -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)
|
||||||
|
|||||||
+8
-7
@@ -1351,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)]
|
||||||
@@ -2023,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.
|
||||||
@@ -2036,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):
|
||||||
@@ -2314,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',
|
||||||
@@ -2692,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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,6 +313,25 @@ 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
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user