Compare commits

...
90 Commits
Author SHA1 Message Date
mdipierro c9fd0fd71e fixed test because of layout change 2017-11-14 00:55:59 -06:00
mdipierro d59e7f35ed fixed toolbar/configuration issue 2017-11-14 00:50:44 -06:00
mdipierro 999e7b7512 fixed layout in lack of configuration 2017-11-14 00:47:40 -06:00
mdipierro 7035398681 2.16.1 2017-11-13 23:50:20 -06:00
mdipierro 2c2c9d3aa2 moved to bootstrap4 2017-11-13 23:48:54 -06:00
mdipierro 65b3e6dda9 support for bootstrap4 2017-11-13 23:29:34 -06:00
mdipierroandGitHub d424e5d317 Merge pull request #1610 from geomapdev/dal-enhancements
handle reference fields for keyed tables
2017-11-13 21:24:07 -06:00
mdipierro a1417df67c Merge branch 'master' of github.com:web2py/web2py 2017-11-13 21:15:04 -06:00
mdipierroandGitHub 551c19bcaf Merge pull request #1761 from josedesoto/issue/update_role_on_update_profile
Update groups on edit profile
2017-11-13 21:14:02 -06:00
mdipierroandGitHub 8aa07760d2 Merge pull request #1803 from leonelcamara/patch-10
Fix #1715
2017-11-13 21:13:06 -06:00
mdipierroandGitHub 8bd8db5edc Merge pull request #1804 from leonelcamara/patch-11
Update supported python versions
2017-11-13 21:12:36 -06:00
mdipierro 7c1fb6643e fllowing pydal v17.11 2017-11-13 21:12:06 -06:00
Leonel CâmaraandGitHub e44a12eaf8 Update supported python versions 2017-11-13 22:40:37 +00:00
Leonel CâmaraandGitHub 12253ab757 Fix #1715 2017-11-13 15:31:05 +00:00
mdipierro 30b3d84f24 fixed options when type is list 2017-11-10 22:31:35 -06:00
mdipierro 166e268308 improved handling of regex and options field attributes 2017-11-10 18:50:52 -06:00
mdipierro 14e58276cf form handling of f.options, f.regex, f.listable, f.rearchable 2017-11-10 18:41:41 -06:00
mdipierroandGitHub eb7222aa92 Merge pull request #1802 from leonelcamara/fixsqltable
Fix a problem with Expressions in SQLTABLE
2017-11-10 18:12:50 -06:00
mdipierroandGitHub 009a0b87f2 Merge pull request #1801 from leonelcamara/i1800
Fixes #1800
2017-11-10 18:12:16 -06:00
mdipierroandGitHub aeb0244b2c Merge pull request #1799 from vinyldarkscratch/master
Add two scripts
2017-11-10 18:12:03 -06:00
mdipierroandGitHub 57522ddeb1 Merge pull request #1797 from BigManWalter/master
Set entry animation to 'fadeIn' and provide an easy switch for 'slideDown'
2017-11-10 18:10:13 -06:00
mdipierroandGitHub 934042fc04 Merge pull request #1796 from vitkarpenko/middleware_status_code
gluon.http expects status code as integer while LazyWSGI generates str
2017-11-10 18:09:37 -06:00
Leonel Câmara 4bfa9c1686 Fix a problem with Expressions in SQLTABLE possibly fixes #1735 and fixes web2py/pydal#487 2017-11-08 22:21:51 +00:00
Leonel Câmara 925f928843 Copy all CRYPT attributes thanks @abastardi 2017-11-08 11:53:29 +00:00
mdipierro 2f5eb409b6 show computed fields in readonly mode in appadmin 2017-11-08 00:40:47 -06:00
Leonel Câmara 228d3c41b6 Fixes #1800 2017-11-07 23:34:35 +00:00
Vinyl Darkscratch 3ab91b9fe9 Add check_lang_progress script 2017-11-06 01:42:25 -08:00
Vinyl Darkscratch 5db3b21437 Add update_languages script 2017-11-06 01:42:14 -08:00
Eric Waldman 2aa8fd22a2 Set entry animation to 'fadeIn' and provide an easy switch for 'slideDown' 2017-11-03 15:17:18 -04:00
Vitaly Karpenko d5eb8d8f17 gluon.http expects status code as integer 2017-11-02 13:52:25 +03:00
mdipierroandGitHub c4e08eeeb3 Merge pull request #1793 from sherdim/master
A step to PY3  closes #1792 and may be closes #1727
2017-11-01 18:59:52 -05:00
geomapdev c1a3d5d67e sync with master branch 2017-10-31 14:47:07 -07:00
geomapdev b2841de6f3 Merge remote-tracking branch 'upstream/master' 2017-10-31 14:42:19 -07:00
Дим ЩandGitHub c7d415fefd compatible unicode 2017-10-28 19:32:48 +03:00
Дим ЩandGitHub 2e572aee9a Update custom_import.py 2017-10-28 19:29:55 +03:00
Дим ЩandGitHub dd14d1c8c9 Add reload 2017-10-28 19:28:51 +03:00
Дим ЩandGitHub afa5fac074 Add reload 2017-10-28 19:26:34 +03:00
mdipierro a5b76f1dec Merge branch 'master' of github.com:web2py/web2py 2017-10-18 22:14:27 -05:00
mdipierroandGitHub 1e586b32a4 Merge pull request #1789 from timnyborg/master
Fix setting 'ac' in request.vars
2017-10-18 22:13:16 -05:00
mdipierroandGitHub dfcffe1a9d Merge pull request #1785 from tiagobar/master
Enhances/fixes validators/is_empty() for Python 3 compatibility, fix #1764
2017-10-18 22:12:09 -05:00
Tim NyborgandGitHub 2e6f529dfd Fix setting 'ac' in request.vars 2017-10-12 17:18:06 +01:00
Tim NyborgandGitHub 0ae3ee506f Merge pull request #1 from web2py/master
rebase
2017-10-12 17:16:56 +01:00
geomapdev 6b103f7e3a updated test_validators IS_IN_DB 2017-10-10 08:42:17 -07:00
geomapdev 02c32ebace update gluon unittest for dal reference fields 2017-10-09 11:22:48 -07:00
geomapdev 603cc7092a dal.py more code cleanup 2017-10-04 10:39:42 -07:00
geomapdev 8590aae2e8 dal.py code cleanup 2017-10-04 10:34:48 -07:00
tiago.bar 511245a68c Unit test for #1764 fix 2017-09-23 20:21:14 -03:00
tiago.bar 4a347a4f78 Enhances/fixes validators/is_empty() for Python 3 compatibility, fix #1764 2017-09-22 13:04:47 -03:00
Jose de SotoandGitHub 5f4c47729b Removed a tab and replaced by spaces 2017-09-21 10:17:17 +02:00
mdipierroandGitHub 5975e4767f Merge pull request #1766 from abastardi/tsv_export
Fix grid TSV export bug
2017-09-21 00:23:17 -05:00
mdipierroandGitHub 1800304ade Merge pull request #1763 from abastardi/compiled_views
Fix bug with compiled views in compiled-only apps
2017-09-21 00:22:53 -05:00
mdipierroandGitHub 9af0d8c3c9 Merge pull request #1758 from leonelcamara/patch-9
Fix #1757
2017-09-21 00:21:10 -05:00
mdipierroandGitHub 2e2d3482e4 Merge pull request #1756 from leonelcamara/i1752
Make _compat accept ducks
2017-09-21 00:20:54 -05:00
mdipierroandGitHub 48cc63741e Merge pull request #1755 from leonelcamara/i1753
Fixes #1753
2017-09-21 00:20:26 -05:00
abastardi a605e43fa6 Fix grid TSV export bug 2017-09-14 18:49:31 -04:00
abastardi 8eda21ca86 Fix bug with compiled views in compiled-only apps
Compiled views were not being executed in apps containing only compiled
files (i.e., generated via the admin "Pack compiled" functionality),
resulting in "Invalid view" errors for all pages.
2017-09-13 11:48:25 -04:00
Jose de Soto e8cf50326d When profile is updated self._update_session_user(user) set session.user_groups to None. self.update_groups() needs to be done. 2017-09-13 11:21:03 +02:00
mdipierro 27c9250efb fixed book link 2017-09-07 12:46:36 -05:00
Leonel CâmaraandGitHub 3ecdd1c11b Fix #1757 2017-09-07 10:18:41 +01:00
Leonel Câmara 912c22d593 Don't use gluon.utf8.Utf8 with py3 there's no need for it and it breaks stuff 2017-09-06 16:22:50 +01:00
Leonel Câmara 4b38186b51 simplify condition if you ain't got decode you ain't bytes 2017-09-06 15:37:51 +01:00
Leonel Câmara 83f9016528 fix in py3 too 2017-09-06 15:33:47 +01:00
Leonel Câmara a6044068cd Fixes #1751 2017-09-06 15:31:30 +01:00
Leonel Câmara 4aefb93ab4 Fixes #1752 2017-09-06 15:28:39 +01:00
Leonel Câmara 2861dc4215 Fixes #1753 2017-09-05 15:46:31 +01:00
mdipierro 087280ec17 fixed Makefile _ssd 2017-09-01 22:47:54 -05:00
mdipierro d06a1f9dc6 R-2.15.4 2017-09-01 22:40:20 -05:00
mdipierro c06fc67064 following pydal 17.08 2017-08-31 17:31:43 -05:00
mdipierroandGitHub bd167aa94a Merge pull request #1749 from liuyigh/patch-1
Minor error in EOF instructions
2017-08-31 17:29:56 -05:00
Yi LiuandGitHub c9a71a7055 MInor error in EOF instructions
start -> stop
2017-08-29 11:26:22 -07:00
mdipierroandGitHub 02e50cadbc Merge pull request #1746 from abastardi/disable-submit
Fix submit button disable bug
2017-08-28 11:25:55 -05:00
mdipierroandGitHub 9f69ab9753 Merge pull request #1743 from ilvalle/fix_ticket_compile_views
Fix view file name in ticket traceback
2017-08-28 11:25:40 -05:00
mdipierroandGitHub 56d10a40c6 Merge pull request #1741 from leonelcamara/patch-8
execfile shim for python 3 compatibility
2017-08-28 11:25:28 -05:00
mdipierroandGitHub 76b09393e4 Merge pull request #1738 from leonelcamara/patch-7
Fixes #1737
2017-08-28 11:25:07 -05:00
abastardi e880da0d0e Fix submit button disable bug
When a form includes more than one submit button, after being disabled
all buttons are re-enabled with the value of the first button rather
than having their original values restored. This change iterates over
each button separately to disable and enable.
2017-08-23 11:43:40 -04:00
ilvalle 9694c66703 Fix view file name in ticket traceback, fix #1740, fix #1676 2017-08-19 10:07:52 +02:00
Leonel CâmaraandGitHub f22e3a7624 execfile shim for python 3 compatibility
Fixes #1739
2017-08-16 16:35:46 +01:00
Leonel CâmaraandGitHub bd7ee209ea Fixes #1737
getproxies is in urllib.request for python 3
2017-08-15 15:34:35 +01:00
mdipierroandGitHub 5e6c3dba81 Merge pull request #1732 from lkrn/master
fixes #1724
2017-08-13 13:55:09 -05:00
LAdm 135f41041d fixes #1724
call func instead of module in url_is_acceptable
2017-08-10 06:49:36 +02:00
mdipierroandGitHub 1d0f322d09 Merge pull request #1731 from leonelcamara/fix-include-files
Fix include files
2017-08-09 15:46:51 -05:00
Leonel CâmaraandGitHub 892fba9e54 fix unordered include_files result
Now only concats adjacent internal to the application stuff, this also Fixes #1673
2017-08-09 01:08:28 +01:00
Leonel CâmaraandGitHub 6e5c8b62cc Make test_include_files demand the same order 2017-08-09 01:05:37 +01:00
mdipierroandGitHub 852a9e0127 Merge pull request #1726 from BuhtigithuB/feature/has-membership-cached-true
auth.has_membership(cached=True) check membership in auth.user_groups
2017-08-08 12:19:26 -05:00
Richard Vézina 485f868cd1 auth.has_membership(cached=True) check membership in auth.user_groups 2017-08-08 11:40:07 -04:00
mdipierroandGitHub de8b2a477b Merge pull request #1722 from leonelcamara/patch-5
mobilize is back
2017-08-07 23:29:06 -05:00
Leonel Câmara 8533fa0d00 put is_mobile and is_tablet in the result of user_agent() 2017-08-08 00:50:55 +01:00
Leonel CâmaraandGitHub 0b41ed36f9 mobilize is back
Fixes #1721
2017-08-06 19:20:01 +01:00
geomapdevandGitHub 69e6e79e23 Update dal.py
updated to handle references without format
2017-05-01 08:39:31 -07:00
geomapdevandGitHub 3292f760ca handle reference fields for keyed tables 2017-04-20 10:21:16 -07:00
48 changed files with 839 additions and 407 deletions
+8 -1
View File
@@ -1,4 +1,11 @@
## 2.15.1-3 ## 2.16.1
- pydal 17.11
- bootstrap 4
- better welcome examples
- many bug fixes
## 2.15.1-4
- pydal 17.08
- dropped support for python 2.6 - dropped support for python 2.6
- dropped web shell - dropped web shell
- experimental python 3 support - experimental python 3 support
+9 -7
View File
@@ -30,11 +30,7 @@ update:
wget -O gluon/contrib/feedparser.py http://feedparser.googlecode.com/svn/trunk/feedparser/feedparser.py wget -O gluon/contrib/feedparser.py http://feedparser.googlecode.com/svn/trunk/feedparser/feedparser.py
wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: rmfiles:
### Use semantic versioning
echo 'Version 2.15.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files
make clean
### clean up baisc apps ### clean up baisc apps
rm -f routes.py rm -f routes.py
rm -rf applications/*/sessions/* rm -rf applications/*/sessions/*
@@ -46,6 +42,12 @@ src:
rm -rf applications/admin/uploads/* rm -rf applications/admin/uploads/*
rm -rf applications/welcome/uploads/* rm -rf applications/welcome/uploads/*
rm -rf applications/examples/uploads/* rm -rf applications/examples/uploads/*
src:
### Use semantic versioning
echo 'Version 2.16.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files
#make clean
# make rmfiles
### make welcome layout and appadmin the default ### make welcome layout and appadmin the default
cp applications/welcome/views/appadmin.html applications/admin/views cp applications/welcome/views/appadmin.html applications/admin/views
cp applications/welcome/views/appadmin.html applications/examples/views cp applications/welcome/views/appadmin.html applications/examples/views
@@ -54,7 +56,7 @@ src:
### build web2py_src.zip ### build web2py_src.zip
echo '' > NEWINSTALL echo '' > NEWINSTALL
mv web2py_src.zip web2py_src_old.zip | echo 'no old' mv web2py_src.zip web2py_src_old.zip | echo 'no old'
cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/fabfile.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py cd ..; zip -r --exclude=**.git** web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/fabfile.py web2py/gluon/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py
mdp: mdp:
make src make src
@@ -98,7 +100,7 @@ win:
cp -r applications/examples ../web2py_win/web2py/applications cp -r applications/examples ../web2py_win/web2py/applications
cp applications/__init__.py ../web2py_win/web2py/applications cp applications/__init__.py ../web2py_win/web2py/applications
# per https://github.com/web2py/web2py/issues/1716 # per https://github.com/web2py/web2py/issues/1716
mv ../web2py_win/web2py/_ssl.pyd ../web2py_win/web2py/_ssl.pyd.legacy mv ../web2py_win/web2py/_ssl.pyd ../web2py_win/web2py/_ssl.pyd.legacy | echo 'done'
cd ../web2py_win; zip -r web2py_win.zip web2py cd ../web2py_win; zip -r web2py_win.zip web2py
mv ../web2py_win/web2py_win.zip . mv ../web2py_win/web2py_win.zip .
run: run:
+1 -1
View File
@@ -1 +1 @@
Version 2.15.3-stable+timestamp.2017.08.07.07.32.04 Version 2.16.1-stable+timestamp.2017.11.13.23.50.07
File diff suppressed because one or more lines are too long
+41 -15
View File
@@ -12,6 +12,10 @@
$.error('web2py.js has already been loaded!'); $.error('web2py.js has already been loaded!');
} }
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
var animateIn = 'fadeIn';
// var animateIn = 'slideDown';
String.prototype.reverse = function () { String.prototype.reverse = function () {
return this.split('').reverse().join(''); return this.split('').reverse().join('');
}; };
@@ -178,7 +182,7 @@
}, },
/* manage errors in forms */ /* manage errors in forms */
manage_errors: function (target) { manage_errors: function (target) {
$('div.error', target).hide().slideDown('slow'); $('div.error', target).hide()[animateIn]('slow');
}, },
after_ajax: function (xhr) { after_ajax: function (xhr) {
/* called whenever an ajax request completes */ /* called whenever an ajax request completes */
@@ -263,13 +267,17 @@
} }
}); });
/* help preventing double form submission for normal form (not LOADed) */ /* help preventing double form submission for normal form (not LOADed) */
$(doc).on('submit', 'form', function () { $(doc).on('submit', 'form', function (e) {
var submit_button = $(this).find(web2py.formInputClickSelector); var submit_buttons = $(this).find(web2py.formInputClickSelector);
web2py.disableElement(submit_button); submit_buttons.each(function() {
web2py.disableElement($(this));
})
/* safeguard in case the form doesn't trigger a refresh, /* safeguard in case the form doesn't trigger a refresh,
see https://github.com/web2py/web2py/issues/1100 */ see https://github.com/web2py/web2py/issues/1100 */
setTimeout(function () { setTimeout(function () {
web2py.enableElement(submit_button); submit_buttons.each(function() {
web2py.enableElement($(this));
});
}, 5000); }, 5000);
}); });
doc.ajaxSuccess(function (e, xhr) { doc.ajaxSuccess(function (e, xhr) {
@@ -320,7 +328,15 @@
form.submit(function (e) { form.submit(function (e) {
web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.disableElement(form.find(web2py.formInputClickSelector));
web2py.hide_flash(); web2py.hide_flash();
web2py.ajax_page('post', url, form.serialize(), target, form);
var formData;
if (FORMDATA_IS_SUPPORTED) {
formData = new FormData(form[0]); // Allows file uploads.
} else {
formData = form.serialize(); // Fallback for older browsers.
}
web2py.ajax_page('post', url, formData, target, form);
e.preventDefault(); e.preventDefault();
}); });
form.on('click', web2py.formInputClickSelector, function (e) { form.on('click', web2py.formInputClickSelector, function (e) {
@@ -339,11 +355,18 @@
if (web2py.isUndefined(element)) element = $(document); if (web2py.isUndefined(element)) element = $(document);
/* if target is not there, fill it with something that there isn't in the page*/ /* if target is not there, fill it with something that there isn't in the page*/
if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; if (web2py.isUndefined(target) || target === '') target = 'w2p_none';
/* processData and contentType must be set to false when passing a FormData
object to jQuery.ajax. */
var isFormData = Object.prototype.toString.call(data) === '[object FormData]';
var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8';
if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */
$.ajax({ $.ajax({
'type': method, 'type': method,
'url': action, 'url': action,
'data': data, 'data': data,
'processData': !isFormData,
'contentType': contentType,
'beforeSend': function (xhr, settings) { 'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target); xhr.setRequestHeader('web2py-component-element', target);
@@ -595,7 +618,7 @@
var flash = $('.w2p_flash'); var flash = $('.w2p_flash');
web2py.hide_flash(); web2py.hide_flash();
flash.html(message).addClass(status); flash.html(message).addClass(status);
if (flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown(); if (flash.html()) flash.append('<span id="closeflash"> &times; </span>')[animateIn]();
}, },
hide_flash: function () { hide_flash: function () {
$('.w2p_flash').fadeOut(0).html(''); $('.w2p_flash').fadeOut(0).html('');
@@ -609,7 +632,7 @@
for (var k = 0; k < triggers[id].length; k++) { for (var k = 0; k < triggers[id].length; k++) {
var dep = $('#' + triggers[id][k], target); var dep = $('#' + triggers[id][k], target);
var tr = $('#' + triggers[id][k] + '__row', target); var tr = $('#' + triggers[id][k] + '__row', target);
if (t.is(dep.attr('data-show-if'))) tr.slideDown(); if (t.is(dep.attr('data-show-if'))) tr[animateIn]();
else tr.hide(); else tr.hide();
} }
}; };
@@ -699,8 +722,9 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Does not disable elements with 'data-w2p_disable' attribute
- Caches element value in 'w2p_enable_with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-w2p_disable_with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
disableFormElements: function (form) { disableFormElements: function (form) {
@@ -712,13 +736,15 @@
if (!web2py.isUndefined(disable)) { if (!web2py.isUndefined(disable)) {
return false; return false;
} }
if (web2py.isUndefined(disable_with)) { if (!element.is(':file')) { // Altering file input values is not allowed.
element.data('w2p_disable_with', element[method]()); if (web2py.isUndefined(disable_with)) {
element.data('w2p_disable_with', element[method]());
}
if (web2py.isUndefined(element.data('w2p_enable_with'))) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
} }
if (web2py.isUndefined(element.data('w2p_enable_with'))) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
}); });
}, },
@@ -3,9 +3,9 @@
- **Created by a community of professionals** and University professors in Computer Science and Software Engineering. - **Created by a community of professionals** and University professors in Computer Science and Software Engineering.
- **Always backward compatible.** We have not broken backward compatibility since version 1.0 in 2007, and we pledge not to break it in the future. - **Always backward compatible.** We have not broken backward compatibility since version 1.0 in 2007, and we pledge not to break it in the future.
- **Easy to run.** It requires no installation and no configuration. - **Easy to run.** It requires no installation and no configuration.
- **Runs on** Windows, Mac, Unix/Linux, Google App Engine, Amazon EC2, and almost any web hosting via Python 2.5/2.6/2.7/pypy, or Java with Jython. - **Runs on** Windows, Mac, Unix/Linux, Google App Engine, Amazon EC2, and almost any web hosting via Python 2.7/3.5/3.6/pypy.
- **Runs with** Apache, Lighttpd, Cherokee and almost any other web server via CGI, FastCGI, WSGI, mod_proxy, and/or mod_python. It can embed third party WSGI apps and middleware. - **Runs with** Apache, Nginx, Lighttpd, Cherokee and almost any other web server via CGI, FastCGI, WSGI, mod_proxy, and/or mod_python. It can embed third party WSGI apps and middleware.
- **Talks to** SQLite, PostgreSQL, MySQL, MSSQL, FireBird, Oracle, IBM DB2, Informix, Ingres, and Google App Engine. - **Talks to** SQLite, PostgreSQL, MySQL, MSSQL, FireBird, Sybase, Oracle, IBM DB2, Informix, Ingres, MongoDB, and Google App Engine.
- **Secure** [[It prevents the most common types of vulnerabilities http://web2py.com/examples/default/security]] including Cross Site Scripting, Injection Flaws, and Malicious File Execution. - **Secure** [[It prevents the most common types of vulnerabilities http://web2py.com/examples/default/security]] including Cross Site Scripting, Injection Flaws, and Malicious File Execution.
- **Enforces good Software Engineering practices** (Model-View-Controller design, Server-side form validation, postbacks) that make the code more readable, scalable, and maintainable. - **Enforces good Software Engineering practices** (Model-View-Controller design, Server-side form validation, postbacks) that make the code more readable, scalable, and maintainable.
- **Speaks multiple protocols** HTML/XML, RSS/ATOM, RTF, PDF, JSON, AJAX, XML-RPC, CSV, REST, WIKI, Flash/AMF, and Linked Data (RDF). - **Speaks multiple protocols** HTML/XML, RSS/ATOM, RTF, PDF, JSON, AJAX, XML-RPC, CSV, REST, WIKI, Flash/AMF, and Linked Data (RDF).
+41 -15
View File
@@ -12,6 +12,10 @@
$.error('web2py.js has already been loaded!'); $.error('web2py.js has already been loaded!');
} }
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
var animateIn = 'fadeIn';
// var animateIn = 'slideDown';
String.prototype.reverse = function () { String.prototype.reverse = function () {
return this.split('').reverse().join(''); return this.split('').reverse().join('');
}; };
@@ -178,7 +182,7 @@
}, },
/* manage errors in forms */ /* manage errors in forms */
manage_errors: function (target) { manage_errors: function (target) {
$('div.error', target).hide().slideDown('slow'); $('div.error', target).hide()[animateIn]('slow');
}, },
after_ajax: function (xhr) { after_ajax: function (xhr) {
/* called whenever an ajax request completes */ /* called whenever an ajax request completes */
@@ -263,13 +267,17 @@
} }
}); });
/* help preventing double form submission for normal form (not LOADed) */ /* help preventing double form submission for normal form (not LOADed) */
$(doc).on('submit', 'form', function () { $(doc).on('submit', 'form', function (e) {
var submit_button = $(this).find(web2py.formInputClickSelector); var submit_buttons = $(this).find(web2py.formInputClickSelector);
web2py.disableElement(submit_button); submit_buttons.each(function() {
web2py.disableElement($(this));
})
/* safeguard in case the form doesn't trigger a refresh, /* safeguard in case the form doesn't trigger a refresh,
see https://github.com/web2py/web2py/issues/1100 */ see https://github.com/web2py/web2py/issues/1100 */
setTimeout(function () { setTimeout(function () {
web2py.enableElement(submit_button); submit_buttons.each(function() {
web2py.enableElement($(this));
});
}, 5000); }, 5000);
}); });
doc.ajaxSuccess(function (e, xhr) { doc.ajaxSuccess(function (e, xhr) {
@@ -320,7 +328,15 @@
form.submit(function (e) { form.submit(function (e) {
web2py.disableElement(form.find(web2py.formInputClickSelector)); web2py.disableElement(form.find(web2py.formInputClickSelector));
web2py.hide_flash(); web2py.hide_flash();
web2py.ajax_page('post', url, form.serialize(), target, form);
var formData;
if (FORMDATA_IS_SUPPORTED) {
formData = new FormData(form[0]); // Allows file uploads.
} else {
formData = form.serialize(); // Fallback for older browsers.
}
web2py.ajax_page('post', url, formData, target, form);
e.preventDefault(); e.preventDefault();
}); });
form.on('click', web2py.formInputClickSelector, function (e) { form.on('click', web2py.formInputClickSelector, function (e) {
@@ -339,11 +355,18 @@
if (web2py.isUndefined(element)) element = $(document); if (web2py.isUndefined(element)) element = $(document);
/* if target is not there, fill it with something that there isn't in the page*/ /* if target is not there, fill it with something that there isn't in the page*/
if (web2py.isUndefined(target) || target === '') target = 'w2p_none'; if (web2py.isUndefined(target) || target === '') target = 'w2p_none';
/* processData and contentType must be set to false when passing a FormData
object to jQuery.ajax. */
var isFormData = Object.prototype.toString.call(data) === '[object FormData]';
var contentType = isFormData ? false : 'application/x-www-form-urlencoded; charset=UTF-8';
if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */ if (web2py.fire(element, 'ajax:before', null, target)) { /*test a usecase, should stop here if returns false */
$.ajax({ $.ajax({
'type': method, 'type': method,
'url': action, 'url': action,
'data': data, 'data': data,
'processData': !isFormData,
'contentType': contentType,
'beforeSend': function (xhr, settings) { 'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location); xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target); xhr.setRequestHeader('web2py-component-element', target);
@@ -595,7 +618,7 @@
var flash = $('.w2p_flash'); var flash = $('.w2p_flash');
web2py.hide_flash(); web2py.hide_flash();
flash.html(message).addClass(status); flash.html(message).addClass(status);
if (flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown(); if (flash.html()) flash.append('<span id="closeflash"> &times; </span>')[animateIn]();
}, },
hide_flash: function () { hide_flash: function () {
$('.w2p_flash').fadeOut(0).html(''); $('.w2p_flash').fadeOut(0).html('');
@@ -609,7 +632,7 @@
for (var k = 0; k < triggers[id].length; k++) { for (var k = 0; k < triggers[id].length; k++) {
var dep = $('#' + triggers[id][k], target); var dep = $('#' + triggers[id][k], target);
var tr = $('#' + triggers[id][k] + '__row', target); var tr = $('#' + triggers[id][k] + '__row', target);
if (t.is(dep.attr('data-show-if'))) tr.slideDown(); if (t.is(dep.attr('data-show-if'))) tr[animateIn]();
else tr.hide(); else tr.hide();
} }
}; };
@@ -699,8 +722,9 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Does not disable elements with 'data-w2p_disable' attribute
- Caches element value in 'w2p_enable_with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-w2p_disable_with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
disableFormElements: function (form) { disableFormElements: function (form) {
@@ -712,13 +736,15 @@
if (!web2py.isUndefined(disable)) { if (!web2py.isUndefined(disable)) {
return false; return false;
} }
if (web2py.isUndefined(disable_with)) { if (!element.is(':file')) { // Altering file input values is not allowed.
element.data('w2p_disable_with', element[method]()); if (web2py.isUndefined(disable_with)) {
element.data('w2p_disable_with', element[method]());
}
if (web2py.isUndefined(element.data('w2p_enable_with'))) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
} }
if (web2py.isUndefined(element.data('w2p_enable_with'))) {
element.data('w2p_enable_with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
}); });
}, },
@@ -48,7 +48,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<a class="btn btn180 rounded green" href="https://dl.dropbox.com/u/18065445/web2py/web2py_manual_5th.pdf">Manual</a> <a class="btn btn180 rounded green" href="http://mdipierro.github.io/web2py/web2py_manual_5th.pdf">Manual</a>
</td> </td>
<td> <td>
<a class="btn btn180 rounded" href="https://github.com/web2py/web2py/releases">Change Log</a> <a class="btn btn180 rounded" href="https://github.com/web2py/web2py/releases">Change Log</a>
+23 -26
View File
@@ -1,26 +1,35 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# this file is released under public domain and you can use without limitations
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# This is a sample controller # This is a sample controller
# - index is the default action of any application # this file is released under public domain and you can use without limitations
# - user is required for authentication and authorization
# - download is for downloading files uploaded in the db (does streaming)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# ---- example index page ----
def index(): def index():
"""
example action using the internationalization operator T and flash
rendered by views/default/index.html or views/generic.html
if you need a simple wiki simply replace the two lines below with:
return auth.wiki()
"""
response.flash = T("Hello World") response.flash = T("Hello World")
return dict(message=T('Welcome to web2py!')) return dict(message=T('Welcome to web2py!'))
# ---- API (example) -----
@auth.requires_login()
def api_get_user_email():
if not request.env.request_method == 'GET': raise HTTP(403)
return response.json({'status':'success', 'email':auth.user.email})
# ---- Smart Grid (example) -----
@auth.requires_membership('admin') # can only be accessed by members of admin groupd
def grid():
response.view = 'generic.html' # use a generic view
tablename = request.args(0)
if not tablename in db.tables: raise HTTP(403)
grid = SQLFORM.smartgrid(db[tablename], args=[tablename], deletable=False, editable=False)
return dict(grid=grid)
# ---- Embedded wiki (example) ----
def wiki():
auth.wikimenu() # add the wiki to the menu
return auth.wiki()
# ---- Action for login/register/etc (required for auth) -----
def user(): def user():
""" """
exposes: exposes:
@@ -39,7 +48,7 @@ def user():
""" """
return dict(form=auth()) return dict(form=auth())
# ---- action to server uploaded static content (required) ---
@cache.action() @cache.action()
def download(): def download():
""" """
@@ -47,15 +56,3 @@ def download():
http://..../[app]/default/download/[filename] http://..../[app]/default/download/[filename]
""" """
return response.download(request, db) return response.download(request, db)
def call():
"""
exposes services. for example:
http://..../[app]/default/call/jsonrpc
decorate with @services.jsonrpc the functions to expose
supports xml, json, xmlrpc, jsonrpc, amfrpc, rss, csv
"""
return service()
+48 -25
View File
@@ -1,12 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# -------------------------------------------------------------------------
# AppConfig configuration made easy. Look inside private/appconfig.ini
# Auth is for authenticaiton and access control
# -------------------------------------------------------------------------
from gluon.contrib.appconfig import AppConfig
from gluon.tools import Auth
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# This scaffolding model makes your app work on Google App Engine too # This scaffolding model makes your app work on Google App Engine too
# File is released under public domain and you can use without limitations # File is released under public domain and you can use without limitations
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
if request.global_settings.web2py_version < "2.14.1": if request.global_settings.web2py_version < "2.15.5":
raise HTTP(500, "Requires web2py 2.13.3 or newer") raise HTTP(500, "Requires web2py 2.15.5 or newer")
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# if SSL/HTTPS is properly configured and you want all HTTP requests to # if SSL/HTTPS is properly configured and you want all HTTP requests to
@@ -14,23 +21,18 @@ if request.global_settings.web2py_version < "2.14.1":
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# request.requires_https() # request.requires_https()
# -------------------------------------------------------------------------
# app configuration made easy. Look inside private/appconfig.ini
# -------------------------------------------------------------------------
from gluon.contrib.appconfig import AppConfig
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# once in production, remove reload=True to gain full speed # once in production, remove reload=True to gain full speed
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
myconf = AppConfig(reload=True) configuration = AppConfig(reload=True)
if not request.env.web2py_runtime_gae: if not request.env.web2py_runtime_gae:
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# if NOT running on Google App Engine use SQLite or other DB # if NOT running on Google App Engine use SQLite or other DB
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
db = DAL(myconf.get('db.uri'), db = DAL(configuration.get('db.uri'),
pool_size=myconf.get('db.pool_size'), pool_size=configuration.get('db.pool_size'),
migrate_enabled=myconf.get('db.migrate'), migrate_enabled=configuration.get('db.migrate'),
check_reserved=['all']) check_reserved=['all'])
else: else:
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@@ -52,12 +54,15 @@ else:
# by default give a view/generic.extension to all actions from localhost # by default give a view/generic.extension to all actions from localhost
# none otherwise. a pattern can be 'controller/function.extension' # none otherwise. a pattern can be 'controller/function.extension'
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
response.generic_patterns = ['*'] if request.is_local else [] response.generic_patterns = []
if request.is_local and not configuration.get('app.production'):
response.generic_patterns.append('*')
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# choose a style for forms # choose a style for forms
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
response.formstyle = myconf.get('forms.formstyle') # or 'bootstrap3_stacked' or 'bootstrap2' or other response.formstyle = 'bootstrap4_inline'
response.form_label_separator = myconf.get('forms.separator') or '' response.form_label_separator = ''
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# (optional) optimize handling of static files # (optional) optimize handling of static files
@@ -80,27 +85,24 @@ response.form_label_separator = myconf.get('forms.separator') or ''
# (more options discussed in gluon/tools.py) # (more options discussed in gluon/tools.py)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
from gluon.tools import Auth, Service, PluginManager
# host names must be a list of allowed host names (glob syntax allowed) # host names must be a list of allowed host names (glob syntax allowed)
auth = Auth(db, host_names=myconf.get('host.names')) auth = Auth(db, host_names=configuration.get('host.names'))
service = Service()
plugins = PluginManager()
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# create all tables needed by auth if not custom tables # create all tables needed by auth, maybe add a list of extra fields
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
auth.settings.extra_fields['auth_user'] = []
auth.define_tables(username=False, signature=False) auth.define_tables(username=False, signature=False)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# configure email # configure email
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
mail = auth.settings.mailer mail = auth.settings.mailer
mail.settings.server = 'logging' if request.is_local else myconf.get('smtp.server') mail.settings.server = 'logging' if request.is_local else configuration.get('smtp.server')
mail.settings.sender = myconf.get('smtp.sender') mail.settings.sender = configuration.get('smtp.sender')
mail.settings.login = myconf.get('smtp.login') mail.settings.login = configuration.get('smtp.login')
mail.settings.tls = myconf.get('smtp.tls') or False mail.settings.tls = configuration.get('smtp.tls') or False
mail.settings.ssl = myconf.get('smtp.ssl') or False mail.settings.ssl = configuration.get('smtp.ssl') or False
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# configure auth policy # configure auth policy
@@ -109,6 +111,27 @@ auth.settings.registration_requires_verification = False
auth.settings.registration_requires_approval = False auth.settings.registration_requires_approval = False
auth.settings.reset_password_requires_verification = True auth.settings.reset_password_requires_verification = True
# -------------------------------------------------------------------------
# read more at http://dev.w3.org/html5/markup/meta.name.html
# -------------------------------------------------------------------------
response.meta.author = configuration.get('app.author')
response.meta.description = configuration.get('app.description')
response.meta.keywords = configuration.get('app.keywords')
response.meta.generator = configuration.get('app.generator')
response.show_toolbar = configuration.get('app.toolbar')
# -------------------------------------------------------------------------
# your http://google.com/analytics id
# -------------------------------------------------------------------------
response.google_analytics_id = configuration.get('google.analytics_id')
# -------------------------------------------------------------------------
# maybe use the scheduler
# -------------------------------------------------------------------------
if configuration.get('scheduler.enabled'):
from gluon.scheduler import Scheduler
scheduler = Scheduler(db, heartbeat=configure.get('heartbeat'))
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Define your tables below (or better in another model file) for example # Define your tables below (or better in another model file) for example
# #
+14 -55
View File
@@ -1,29 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# this file is released under public domain and you can use without limitations # this file is released under public domain and you can use without limitations
# ----------------------------------------------------------------------------------------------------------------------
# Customize your APP title, subtitle and menus here
# ----------------------------------------------------------------------------------------------------------------------
response.logo = A(B('web', SPAN(2), 'py'), XML('&trade;&nbsp;'),
_class="navbar-brand", _href="http://www.web2py.com/",
_id="web2py-logo")
response.title = request.application.replace('_', ' ').title()
response.subtitle = ''
# ----------------------------------------------------------------------------------------------------------------------
# read more at http://dev.w3.org/html5/markup/meta.name.html
# ----------------------------------------------------------------------------------------------------------------------
response.meta.author = myconf.get('app.author')
response.meta.description = myconf.get('app.description')
response.meta.keywords = myconf.get('app.keywords')
response.meta.generator = myconf.get('app.generator')
# ----------------------------------------------------------------------------------------------------------------------
# your http://google.com/analytics id
# ----------------------------------------------------------------------------------------------------------------------
response.google_analytics_id = None
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
# this is the main application menu add/remove items as required # this is the main application menu add/remove items as required
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
@@ -32,53 +9,42 @@ response.menu = [
(T('Home'), False, URL('default', 'index'), []) (T('Home'), False, URL('default', 'index'), [])
] ]
DEVELOPMENT_MENU = True
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
# provide shortcuts for development. remove in production # provide shortcuts for development. you can remove everything below in production
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
def _(): if not configuration.get('app.production'):
# ------------------------------------------------------------------------------------------------------------------ _app = request.application
# shortcuts
# ------------------------------------------------------------------------------------------------------------------
app = request.application
ctr = request.controller
# ------------------------------------------------------------------------------------------------------------------
# useful links to internal and external resources
# ------------------------------------------------------------------------------------------------------------------
response.menu += [ response.menu += [
(T('My Sites'), False, URL('admin', 'default', 'site')), (T('My Sites'), False, URL('admin', 'default', 'site')),
(T('This App'), False, '#', [ (T('This App'), False, '#', [
(T('Design'), False, URL('admin', 'default', 'design/%s' % app)), (T('Design'), False, URL('admin', 'default', 'design/%s' % _app)),
LI(_class="divider"),
(T('Controller'), False, (T('Controller'), False,
URL( URL(
'admin', 'default', 'edit/%s/controllers/%s.py' % (app, ctr))), 'admin', 'default', 'edit/%s/controllers/%s.py' % (_app, request.controller))),
(T('View'), False, (T('View'), False,
URL( URL(
'admin', 'default', 'edit/%s/views/%s' % (app, response.view))), 'admin', 'default', 'edit/%s/views/%s' % (_app, response.view))),
(T('DB Model'), False, (T('DB Model'), False,
URL( URL(
'admin', 'default', 'edit/%s/models/db.py' % app)), 'admin', 'default', 'edit/%s/models/db.py' % _app)),
(T('Menu Model'), False, (T('Menu Model'), False,
URL( URL(
'admin', 'default', 'edit/%s/models/menu.py' % app)), 'admin', 'default', 'edit/%s/models/menu.py' % _app)),
(T('Config.ini'), False, (T('Config.ini'), False,
URL( URL(
'admin', 'default', 'edit/%s/private/appconfig.ini' % app)), 'admin', 'default', 'edit/%s/private/appconfig.ini' % _app)),
(T('Layout'), False, (T('Layout'), False,
URL( URL(
'admin', 'default', 'edit/%s/views/layout.html' % app)), 'admin', 'default', 'edit/%s/views/layout.html' % _app)),
(T('Stylesheet'), False, (T('Stylesheet'), False,
URL( URL(
'admin', 'default', 'edit/%s/static/css/web2py-bootstrap3.css' % app)), 'admin', 'default', 'edit/%s/static/css/web2py-bootstrap3.css' % _app)),
(T('Database'), False, URL(app, 'appadmin', 'index')), (T('Database'), False, URL(_app, 'appadmin', 'index')),
(T('Errors'), False, URL( (T('Errors'), False, URL(
'admin', 'default', 'errors/' + app)), 'admin', 'default', 'errors/' + _app)),
(T('About'), False, URL( (T('About'), False, URL(
'admin', 'default', 'about/' + app)), 'admin', 'default', 'about/' + _app)),
]), ]),
('web2py.com', False, '#', [ ('web2py.com', False, '#', [
(T('Download'), False, (T('Download'), False,
@@ -98,7 +64,6 @@ def _():
]), ]),
(T('Documentation'), False, '#', [ (T('Documentation'), False, '#', [
(T('Online book'), False, 'http://www.web2py.com/book'), (T('Online book'), False, 'http://www.web2py.com/book'),
LI(_class="divider"),
(T('Preface'), False, (T('Preface'), False,
'http://www.web2py.com/book/default/chapter/00'), 'http://www.web2py.com/book/default/chapter/00'),
(T('Introduction'), False, (T('Introduction'), False,
@@ -143,9 +108,3 @@ def _():
]), ]),
] ]
if DEVELOPMENT_MENU:
_()
if "auth" in locals():
auth.wikimenu()
+8 -5
View File
@@ -5,6 +5,8 @@ author = Your Name <you@example.com>
description = a cool new app description = a cool new app
keywords = web2py, python, framework keywords = web2py, python, framework
generator = Web2py Web Framework generator = Web2py Web Framework
production = false
toolbar = false
; Host configuration ; Host configuration
[host] [host]
@@ -14,7 +16,6 @@ names = localhost:*, 127.0.0.1:*, *:*, *
[db] [db]
uri = sqlite://storage.sqlite uri = sqlite://storage.sqlite
migrate = true migrate = true
; ignored for sqlite
pool_size = 10 pool_size = 10
; smtp address and credentials ; smtp address and credentials
@@ -25,7 +26,9 @@ login = username:password
tls = true tls = true
ssl = true ssl = true
; form styling [scheduler]
[forms] enabled = false
formstyle = bootstrap3_inline heartbeat = 1
separator =
[google]
analytics_id =
File diff suppressed because one or more lines are too long
@@ -108,7 +108,6 @@ select.autocomplete {
background: url(../images/background.jpg) no-repeat center center; background: url(../images/background.jpg) no-repeat center center;
} }
body { body {
padding-top: 60px;
margin-bottom: 60px; margin-bottom: 60px;
} }
header { header {
@@ -126,8 +125,7 @@ html {
bottom: 0; bottom: 0;
width: 100%; width: 100%;
height: 60px; height: 60px;
background: #333; background: #f7f7f7;
color: #aaa;
} }
header h1 { header h1 {
color: #FFF!important; color: #FFF!important;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 KiB

File diff suppressed because one or more lines are too long
-6
View File
@@ -1,6 +0,0 @@
/*! Respond.js v1.4.2: min/max-width media query polyfill
* Copyright 2014 Scott Jehl
* Licensed under MIT
* http://j.mp/respondjs */
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='&shy;<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){v(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},g=function(a){return a.replace(c.regex.minmaxwh,"").match(c.regex.other)};if(c.ajax=f,c.queue=d,c.unsupportedmq=g,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,comments:/\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,maxw:/\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,minmaxwh:/\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,other:/\([^\)]*\)/g},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var h,i,j,k=a.document,l=k.documentElement,m=[],n=[],o=[],p={},q=30,r=k.getElementsByTagName("head")[0]||l,s=k.getElementsByTagName("base")[0],t=r.getElementsByTagName("link"),u=function(){var a,b=k.createElement("div"),c=k.body,d=l.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=k.createElement("body"),c.style.background="none"),l.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&l.insertBefore(c,l.firstChild),a=b.offsetWidth,f?l.removeChild(c):c.removeChild(b),l.style.fontSize=d,e&&(c.style.fontSize=e),a=j=parseFloat(a)},v=function(b){var c="clientWidth",d=l[c],e="CSS1Compat"===k.compatMode&&d||k.body[c]||d,f={},g=t[t.length-1],p=(new Date).getTime();if(b&&h&&q>p-h)return a.clearTimeout(i),i=a.setTimeout(v,q),void 0;h=p;for(var s in m)if(m.hasOwnProperty(s)){var w=m[s],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?j||u():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?j||u():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(n[w.rules]))}for(var C in o)o.hasOwnProperty(C)&&o[C]&&o[C].parentNode===r&&r.removeChild(o[C]);o.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=k.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,r.insertBefore(E,g.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(k.createTextNode(F)),o.push(E)}},w=function(a,b,d){var e=a.replace(c.regex.comments,"").replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},i=!f&&d;b.length&&(b+="/"),i&&(f=1);for(var j=0;f>j;j++){var k,l,o,p;i?(k=d,n.push(h(a))):(k=e[j].match(c.regex.findStyles)&&RegExp.$1,n.push(RegExp.$2&&h(RegExp.$2))),o=k.split(","),p=o.length;for(var q=0;p>q;q++)l=o[q],g(l)||m.push({media:l.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:n.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}v()},x=function(){if(d.length){var b=d.shift();f(b.href,function(c){w(c,b.href,b.media),p[b.href]=!0,a.setTimeout(function(){x()},0)})}},y=function(){for(var b=0;b<t.length;b++){var c=t[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!p[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(w(c.styleSheet.rawCssText,e,f),p[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!s||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}x()};y(),c.update=y,c.getEmValue=u,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
+13 -7
View File
@@ -13,6 +13,8 @@
} }
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined'; var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
var animateIn = 'fadeIn';
// var animateIn = 'slideDown';
String.prototype.reverse = function () { String.prototype.reverse = function () {
return this.split('').reverse().join(''); return this.split('').reverse().join('');
@@ -180,7 +182,7 @@
}, },
/* manage errors in forms */ /* manage errors in forms */
manage_errors: function (target) { manage_errors: function (target) {
$('div.error', target).hide().slideDown('slow'); $('div.error', target).hide()[animateIn]('slow');
}, },
after_ajax: function (xhr) { after_ajax: function (xhr) {
/* called whenever an ajax request completes */ /* called whenever an ajax request completes */
@@ -265,13 +267,17 @@
} }
}); });
/* help preventing double form submission for normal form (not LOADed) */ /* help preventing double form submission for normal form (not LOADed) */
$(doc).on('submit', 'form', function () { $(doc).on('submit', 'form', function (e) {
var submit_button = $(this).find(web2py.formInputClickSelector); var submit_buttons = $(this).find(web2py.formInputClickSelector);
web2py.disableElement(submit_button); submit_buttons.each(function() {
web2py.disableElement($(this));
})
/* safeguard in case the form doesn't trigger a refresh, /* safeguard in case the form doesn't trigger a refresh,
see https://github.com/web2py/web2py/issues/1100 */ see https://github.com/web2py/web2py/issues/1100 */
setTimeout(function () { setTimeout(function () {
web2py.enableElement(submit_button); submit_buttons.each(function() {
web2py.enableElement($(this));
});
}, 5000); }, 5000);
}); });
doc.ajaxSuccess(function (e, xhr) { doc.ajaxSuccess(function (e, xhr) {
@@ -612,7 +618,7 @@
var flash = $('.w2p_flash'); var flash = $('.w2p_flash');
web2py.hide_flash(); web2py.hide_flash();
flash.html(message).addClass(status); flash.html(message).addClass(status);
if (flash.html()) flash.append('<span id="closeflash"> &times; </span>').slideDown(); if (flash.html()) flash.append('<span id="closeflash"> &times; </span>')[animateIn]();
}, },
hide_flash: function () { hide_flash: function () {
$('.w2p_flash').fadeOut(0).html(''); $('.w2p_flash').fadeOut(0).html('');
@@ -626,7 +632,7 @@
for (var k = 0; k < triggers[id].length; k++) { for (var k = 0; k < triggers[id].length; k++) {
var dep = $('#' + triggers[id][k], target); var dep = $('#' + triggers[id][k], target);
var tr = $('#' + triggers[id][k] + '__row', target); var tr = $('#' + triggers[id][k] + '__row', target);
if (t.is(dep.attr('data-show-if'))) tr.slideDown(); if (t.is(dep.attr('data-show-if'))) tr[animateIn]();
else tr.hide(); else tr.hide();
} }
}; };
+17 -26
View File
@@ -1,15 +1,9 @@
{{left_sidebar_enabled,right_sidebar_enabled=False,('message' in globals())}}
{{extend 'layout.html'}} {{extend 'layout.html'}}
{{block header}} {{block header}}
<header class="container-fluid background"> <center style="background-color: #333; color:white; padding:30px">
<div class="jumbotron text-center"> <h1>/{{=request.application}}/{{=request.controller}}/{{=request.function}}
{{if response.title:}} </center>
<h1>{{=response.title}}
<small>{{=response.subtitle or ''}}</small></h1>
{{pass}}
</div>
</header>
{{end}} {{end}}
{{if 'message' in globals():}} {{if 'message' in globals():}}
@@ -27,26 +21,23 @@
_href=URL('admin','default','peek',args=(request.application,'views',request.controller,'index.html')))))}}</li> _href=URL('admin','default','peek',args=(request.application,'views',request.controller,'index.html')))))}}</li>
<li>{{=T('You can modify this application and adapt it to your needs')}}</li> <li>{{=T('You can modify this application and adapt it to your needs')}}</li>
</ol> </ol>
<center style="padding:50px">
<a class="btn btn-primary" href="{{=URL('admin','default','index')}}">
<i class="glyphicon glyphicon-cog"></i>
{{=T("admin")}}
</a>
<a class="btn btn-secondary" href="{{=URL('examples','default','index')}}">{{=T("Online examples")}}</a>
<a class="btn btn-secondary" href="http://web2py.com">web2py.com</a>
<a class="btn btn-secondary" href="http://web2py.com/book">{{=T('Documentation')}}</a>
<a class="btn btn-secondary" href="{{=URL('default','api_get_user_email')}}">{{=T('API Example')}}</a>
<a class="btn btn-secondary" href="{{=URL('default','grid/auth_user')}}">{{=T('Grid Example')}}</a>
<a class="btn btn-secondary" href="{{=URL('default','wiki')}}">{{=T('Wiki Example')}}</a>
</center>
{{elif 'content' in globals():}} {{elif 'content' in globals():}}
{{=content}} {{=content}}
{{else:}} {{else:}}
{{=BEAUTIFY(response._vars)}} {{=BEAUTIFY(response._vars)}}
{{pass}} {{pass}}
{{block right_sidebar}}
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title"><a class="btn-block"
href="{{=URL('admin','default','index')}}">
<i class="glyphicon glyphicon-cog"></i>
{{=T("admin")}}
</a></h3></div>
<div class="panel-body">
{{=T("Don't know what to do?")}}
</div>
<ul class="list-group">
<li class="list-group-item">{{=A(T("Online examples"), _href=URL('examples','default','index'))}}</li>
<li class="list-group-item"><a href="http://web2py.com">web2py.com</a></li>
<li class="list-group-item"><a href="http://web2py.com/book">{{=T('Documentation')}}</a></li>
</ul>
</div>
{{end}}
-3
View File
@@ -11,6 +11,3 @@ It is used as default when a view is not provided for your controllers
{{elif len(response._vars)>1:}} {{elif len(response._vars)>1:}}
{{=BEAUTIFY(response._vars)}} {{=BEAUTIFY(response._vars)}}
{{pass}} {{pass}}
{{if request.is_local:}}
{{=response.toolbar()}}
{{pass}}
+51 -54
View File
@@ -21,84 +21,81 @@
<meta name="google-site-verification" content=""> <meta name="google-site-verification" content="">
<!-- include stylesheets --> <!-- include stylesheets -->
<link rel="stylesheet" href="{{=URL('static','css/bootstrap.min.css')}}"/> <link rel="stylesheet" href="{{=URL('static','css/bootstrap.min.css')}}"/>
<link rel="stylesheet" href="{{=URL('static','css/web2py-bootstrap3.css')}}"/> <link rel="stylesheet" href="{{=URL('static','css/web2py-bootstrap4.css')}}"/>
<link rel="shortcut icon" href="{{=URL('static','images/favicon.ico')}}" type="image/x-icon"> <link rel="shortcut icon" href="{{=URL('static','images/favicon.ico')}}" type="image/x-icon">
<link rel="apple-touch-icon" href="{{=URL('static','images/favicon.png')}}"> <link rel="apple-touch-icon" href="{{=URL('static','images/favicon.png')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables <!-- All JavaScript at the bottom, except for Modernizr which enables
HTML5 elements & feature detects --> HTML5 elements & feature detects -->
<script src="{{=URL('static','js/modernizr-2.8.3.min.js')}}"></script> <script src="{{=URL('static','js/modernizr-2.8.3.min.js')}}"></script>
<!--[if lt IE 9]>
<script src="{{=URL('static','js/respond-1.4.2.min.js')}}"></script>
<![endif]-->
<!-- Favicons --> <!-- Favicons -->
{{include 'web2py_ajax.html'}} <!-- this includes jquery.js, calendar.js/.css and web2py.js --> {{include 'web2py_ajax.html'}} <!-- this includes jquery.js, calendar.js/.css and web2py.js -->
{{block head}}{{end}} {{block head}}{{end}}
{{
# using sidebars need to know what sidebar you want to use
mc0 = 'col-md-12'
mc1 = 'col-md-9'
mc2 = 'col-md-6'
left_sidebar_enabled = globals().get('left_sidebar_enabled', False)
right_sidebar_enabled = globals().get('right_sidebar_enabled', False)
middle_column = {0: mc0, 1: mc1, 2: mc2}[
(left_sidebar_enabled and 1 or 0)+(right_sidebar_enabled and 1 or 0)]
}}
</head> </head>
<body> <body>
<!--[if lt IE 8]><p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p><![endif]-->
<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-default navbar-fixed-top" role="navigation"> <nav class="navbar navbar-toggleable-md navbar-light bg-faded">
<div class="container-fluid"> <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<div class="navbar-header"> <span class="navbar-toggler-icon"></span>
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> </button>
<span class="sr-only">Toggle navigation</span> <a class="navbar-brand" href="http://web2py.com">web2py</a>
<span class="icon-bar"></span> <div class="collapse navbar-collapse">
<span class="icon-bar"></span> <ul class="navbar-nav">
<span class="icon-bar"></span> {{for _item in response.menu or []:}}
</button> {{if len(_item)<4 or not _item[3]:}}
{{=response.logo or ''}} <li class="nav-item {{if _item[1]:}}active{{pass}}">
</div> <a class="nav-link" href="{{=_item[2]}}">{{=_item[0]}}</a>
<div class="collapse navbar-collapse"> </li>
<ul class="nav navbar-nav navbar-right"> {{else:}}
{{='auth' in globals() and auth.navbar('Welcome',mode='dropdown') or ''}} <li class="nav-item dropdown">
</ul> <a class="nav-link dropdown-toggle" href="{{=_item[2]}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{=_item[0]}}</a>
{{if response.menu:}} <div class="dropdown-menu">
{{=MENU(response.menu, _class='nav navbar-nav',li_class='dropdown',ul_class='dropdown-menu')}} {{for _subitem in _item[3]:}}
<a class="dropdown-item" href="{{=_subitem[2]}}">{{=_subitem[0]}}</a>
{{pass}}
</div>
</li>
{{pass}} {{pass}}
</div> {{pass}}
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search">
</form>
{{if 'auth' in globals():}}
<ul class="navbar-nav navbar-right">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{if auth.user:}}{{=auth.user.first_name}}{{else:}}LOGIN{{pass}}
</a>
<div class="dropdown-menu">
{{if auth.user:}}
<a class="dropdown-item" href="{{=URL('default','user/profile')}}">{{=T('Profile')}}</a>
<a class="dropdown-item" href="{{=URL('default','user/change_password')}}">{{=T('Change Password')}}</a>
<a class="dropdown-item" href="{{=URL('default','user/logout')}}">{{=T('Logout')}}</a>
{{else:}}
<a class="dropdown-item" href="{{=URL('default','user/login')}}">{{=T('Login')}}</a>
<a class="dropdown-item" href="{{=URL('default','user/register')}}">{{=T('Sign up')}}</a>
<a class="dropdown-item" href="{{=URL('default','user/request_password')}}">{{=T('Lost Password')}}</a>
{{pass}}
</div>
</li>
</ul>
{{pass}}
</div> </div>
</nav> </nav>
<!-- Masthead ===================================== --> <!-- Masthead ===================================== -->
{{block header}} {{block header}}
{{end}} {{end}}
<!-- Main ========================================= --> <!-- Main ========================================= -->
<!-- Begin page content --> <!-- Begin page content -->
<div class="container-fluid main-container"> <div class="container-fluid main-container">
{{if left_sidebar_enabled:}} <div class="col-md-12">
<div class="col-md-3 left-sidebar">
{{block left_sidebar}}
<h3>Left Sidebar</h3>
<p></p>
{{end}}
</div>
{{pass}}
<div class="{{=middle_column}}">
{{block center}} {{block center}}
{{include}} {{include}}
{{end}} {{end}}
{{=response.toolbar() if response.show_toolbar else ''}}
</div> </div>
{{if right_sidebar_enabled:}}
<div class="col-md-3">
{{block right_sidebar}}
<h3>Right Sidebar</h3>
<p></p>
{{end}}
</div>
{{pass}}
</div> </div>
{{block footer}} <!-- this is default footer --> {{block footer}} <!-- this is default footer -->
@@ -114,7 +111,7 @@
{{end}} {{end}}
<!-- The javascript =============================== --> <!-- The javascript =============================== -->
<script src="{{=URL('static','js/bootstrap.min.js')}}"></script> <script src="{{=URL('static','js/bootstrap.min.js')}}"></script>
<script src="{{=URL('static','js/web2py-bootstrap3.js')}}"></script> <script src="{{=URL('static','js/web2py-bootstrap4.js')}}"></script>
{{block page_js}}{{end page_js}} {{block page_js}}{{end page_js}}
{{if response.google_analytics_id:}} {{if response.google_analytics_id:}}
<!-- Analytics ==================================== --> <!-- Analytics ==================================== -->
+5 -3
View File
@@ -35,6 +35,7 @@ if PY2:
from gluon.contrib import ipaddress from gluon.contrib import ipaddress
BytesIO = StringIO BytesIO = StringIO
reduce = reduce reduce = reduce
reload = reload
hashlib_md5 = hashlib.md5 hashlib_md5 = hashlib.md5
iterkeys = lambda d: d.iterkeys() iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues() itervalues = lambda d: d.itervalues()
@@ -63,7 +64,7 @@ if PY2:
return None return None
if isinstance(obj, (bytes, bytearray, buffer)): if isinstance(obj, (bytes, bytearray, buffer)):
return bytes(obj) return bytes(obj)
if isinstance(obj, unicode): if hasattr(obj, 'encode'):
return obj.encode(charset, errors) return obj.encode(charset, errors)
raise TypeError('Expected bytes') raise TypeError('Expected bytes')
@@ -77,6 +78,7 @@ else:
import pickle import pickle
from io import StringIO, BytesIO from io import StringIO, BytesIO
import copyreg import copyreg
from importlib import reload
from functools import reduce from functools import reduce
from html.parser import HTMLParser from html.parser import HTMLParser
from http import cookies as Cookie from http import cookies as Cookie
@@ -122,7 +124,7 @@ else:
return None return None
if isinstance(obj, (bytes, bytearray, memoryview)): if isinstance(obj, (bytes, bytearray, memoryview)):
return bytes(obj) return bytes(obj)
if isinstance(obj, str): if hasattr(obj, 'encode'):
return obj.encode(charset, errors) return obj.encode(charset, errors)
raise TypeError('Expected bytes') raise TypeError('Expected bytes')
@@ -151,7 +153,7 @@ def with_metaclass(meta, *bases):
def to_unicode(obj, charset='utf-8', errors='strict'): def to_unicode(obj, charset='utf-8', errors='strict'):
if obj is None: if obj is None:
return None return None
if not isinstance(obj, bytes): if not hasattr(obj, 'decode'):
return text_type(obj) return text_type(obj)
return obj.decode(charset, errors) return obj.decode(charset, errors)
+29 -12
View File
@@ -561,23 +561,39 @@ class AuthAPI(object):
del self.user_groups[group_id] del self.user_groups[group_id]
return ret return ret
def has_membership(self, group_id=None, user_id=None, role=None): def has_membership(self, group_id=None, user_id=None, role=None, cached=False):
""" """
Checks if user is member of group_id or role Checks if user is member of group_id or role
NOTE: To avoid database query at each page load that use auth.has_membership, someone can use cached=True.
If cached is set to True has_membership() check group_id or role only against auth.user_groups variable
which is populated properly only at login time. This means that if an user membership change during a
given session the user has to log off and log in again in order to auth.user_groups to be properly
recreated and reflecting the user membership modification. There is one exception to this log off and
log in process which is in case that the user change his own membership, in this case auth.user_groups
can be properly update for the actual connected user because web2py has access to the proper session
user_groups variable. To make use of this exception someone has to place an "auth.update_groups()"
instruction in his app code to force auth.user_groups to be updated. As mention this will only work if it
the user itself that change it membership not if another user, let say an administrator, change someone
else's membership.
""" """
group_id = group_id or self.id_group(role)
try:
group_id = int(group_id)
except:
group_id = self.id_group(group_id) # interpret group_id as a role
if not user_id and self.user: if not user_id and self.user:
user_id = self.user.id user_id = self.user.id
membership = self.table_membership() if cached:
if group_id and user_id and self.db((membership.user_id == user_id) & id_role = group_id or role
(membership.group_id == group_id)).select(): r = (user_id and id_role in self.user_groups.values()) or (user_id and id_role in self.user_groups)
r = True
else: else:
r = False group_id = group_id or self.id_group(role)
try:
group_id = int(group_id)
except:
group_id = self.id_group(group_id) # interpret group_id as a role
membership = self.table_membership()
if group_id and user_id and self.db((membership.user_id == user_id) &
(membership.group_id == group_id)).select():
r = True
else:
r = False
self.log_event(self.messages['has_membership_log'], self.log_event(self.messages['has_membership_log'],
dict(user_id=user_id, group_id=group_id, check=r)) dict(user_id=user_id, group_id=group_id, check=r))
return r return r
@@ -972,7 +988,8 @@ class AuthAPI(object):
requires = [requires] requires = [requires]
requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) requires = list(filter(lambda t: isinstance(t, CRYPT), requires))
if requires: if requires:
requires[0].min_length = 0 requires[0] = CRYPT(**requires[0].__dict__) # Copy the existing CRYPT attributes
requires[0].min_length = 0 # But do not enforce minimum length for the old password
old_password = kwargs.get('old_password', '') old_password = kwargs.get('old_password', '')
new_password = kwargs.get('new_password', '') new_password = kwargs.get('new_password', '')
+18 -16
View File
@@ -18,7 +18,7 @@ import fnmatch
import os import os
import copy import copy
import random import random
from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, basestring, reduce, xrange, long, reload
from gluon.storage import Storage, List from gluon.storage import Storage, List
from gluon.template import parse_template from gluon.template import parse_template
from gluon.restricted import restricted, compile2 from gluon.restricted import restricted, compile2
@@ -205,7 +205,7 @@ def LOAD(c=None, f='index', args=None, vars=None,
other_response = Response() other_response = Response()
other_request.env.path_info = '/' + \ other_request.env.path_info = '/' + \
'/'.join([request.application, c, f] + '/'.join([request.application, c, f] +
map(str, other_request.args)) [str(a) for a in other_request.args])
other_request.env.query_string = \ other_request.env.query_string = \
vars and URL(vars=vars).split('?')[1] or '' vars and URL(vars=vars).split('?')[1] or ''
other_request.env.http_web2py_component_location = \ other_request.env.http_web2py_component_location = \
@@ -288,7 +288,7 @@ class LoadFactory(object):
other_response = globals.Response() other_response = globals.Response()
other_request.env.path_info = '/' + \ other_request.env.path_info = '/' + \
'/'.join([request.application, c, f] + '/'.join([request.application, c, f] +
map(str, other_request.args)) [str(a) for a in other_request.args])
other_request.env.query_string = \ other_request.env.query_string = \
vars and html.URL(vars=vars).split('?')[1] or '' vars and html.URL(vars=vars).split('?')[1] or ''
other_request.env.http_web2py_component_location = \ other_request.env.http_web2py_component_location = \
@@ -676,8 +676,9 @@ def run_view_in(environment):
badv = 'invalid view (%s)' % view badv = 'invalid view (%s)' % view
patterns = response.get('generic_patterns') patterns = response.get('generic_patterns')
layer = None layer = None
scode = None
if patterns: if patterns:
regex = re_compile('|'.join(map(fnmatch.translate, patterns))) regex = re_compile('|'.join(fnmatch.translate(p) for p in patterns))
short_action = '%(controller)s/%(function)s.%(extension)s' % request short_action = '%(controller)s/%(function)s.%(extension)s' % request
allow_generic = regex.search(short_action) allow_generic = regex.search(short_action)
else: else:
@@ -708,22 +709,23 @@ def run_view_in(environment):
ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled)) ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled))
layer = compiled layer = compiled
break break
if not os.path.exists(filename) and allow_generic:
view = 'generic.' + request.extension
filename = pjoin(folder, 'views', view)
if not os.path.exists(filename):
raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv)
# if the view is not compiled # if the view is not compiled
if not layer: if not layer:
# Compile the template if not os.path.exists(filename) and allow_generic:
ccode = parse_template(view, view = 'generic.' + request.extension
filename = pjoin(folder, 'views', view)
if not os.path.exists(filename):
raise HTTP(404,
rewrite.THREAD_LOCAL.routes.error_message % badv,
web2py_error=badv)
# Parse template
scode = parse_template(view,
pjoin(folder, 'views'), pjoin(folder, 'views'),
context=environment) context=environment)
layer = filename # Compile template
restricted(ccode, environment, layer=layer) ccode = compile2(scode, filename)
layer = filename
restricted(ccode, environment, layer=layer, scode=scode)
# parse_template saves everything in response body # parse_template saves everything in response body
return environment['response'].body.getvalue() return environment['response'].body.getvalue()
+2
View File
@@ -137,6 +137,8 @@ def populate_generator(table, default=True, compute=False, contents={}):
continue continue
elif field.type == 'upload': elif field.type == 'upload':
continue continue
elif field.compute is not None:
continue
elif default and not field.default in (None, ''): elif default and not field.default in (None, ''):
record[fieldname] = field.default record[fieldname] = field.default
elif compute and field.compute: elif compute and field.compute:
+20
View File
@@ -673,3 +673,23 @@ def simple_detect(agent):
if os_version: if os_version:
os = " ".join((os, os_version)) os = " ".join((os, os_version))
return os, browser return os, browser
class mobilize(object):
"""
Decorator for controller functions so they use different views for mobile devices.
WARNING: If you update httpagentparser make sure to leave mobilize for
backwards compatibility.
"""
def __init__(self, func):
self.func = func
def __call__(self):
from gluon import current
user_agent = current.request.user_agent()
if user_agent.is_mobile:
items = current.response.view.split('.')
items.insert(-1, 'mobile')
current.response.view = '.'.join(items)
return self.func()
+1 -1
View File
@@ -8,7 +8,7 @@
Support for smart import syntax for web2py applications Support for smart import syntax for web2py applications
------------------------------------------------------- -------------------------------------------------------
""" """
from gluon._compat import builtin, unicodeT, PY2, to_native from gluon._compat import builtin, unicodeT, PY2, to_native, reload
import os import os
import sys import sys
import threading import threading
+55 -31
View File
@@ -32,7 +32,11 @@ def _default_validators(db, field):
field_type, field_length = field.type, field.length field_type, field_length = field.type, field.length
requires = [] requires = []
if field_type in (('string', 'text', 'password')): if isinstance(field.options, list) and field.requires:
requires = validators.IS_IN_SET(field.options, multiple=field_type.startswith('list:'))
elif field.regex and not field.requires:
requires.append(validators.IS_REGEX(regex))
elif field_type in (('string', 'text', 'password')):
requires.append(validators.IS_LENGTH(field_length)) requires.append(validators.IS_LENGTH(field_length))
elif field_type == 'json': elif field_type == 'json':
requires.append(validators.IS_EMPTY_OR(validators.IS_JSON())) requires.append(validators.IS_EMPTY_OR(validators.IS_JSON()))
@@ -50,42 +54,62 @@ def _default_validators(db, field):
requires.append(validators.IS_TIME()) requires.append(validators.IS_TIME())
elif field_type == 'datetime': elif field_type == 'datetime':
requires.append(validators.IS_DATETIME()) requires.append(validators.IS_DATETIME())
elif db and field_type.startswith('reference') and \ elif db and field_type.startswith('reference'):
field_type.find('.') < 0 and \ if field_type.find('.') < 0 and field_type[10:] in db.tables:
field_type[10:] in db.tables: referenced = db[field_type[10:]]
referenced = db[field_type[10:]] if hasattr(referenced, '_format') and referenced._format:
if hasattr(referenced, '_format') and referenced._format: requires = validators.IS_IN_DB(db, referenced._id,referenced._format)
requires = validators.IS_IN_DB(db, referenced._id, else:
referenced._format) requires = validators.IS_IN_DB(db, referenced._id)
if field.unique: elif field_type.find('.') > 0 and field_type[10:].split('.')[0] in db.tables:
requires._and = validators.IS_NOT_IN_DB(db, field) table_field = field_type[10:].split('.')
if field.tablename == field_type[10:]: table_name=table_field[0]
return validators.IS_EMPTY_OR(requires) field_name=table_field[1]
return requires referenced = db[table_name]
elif db and field_type.startswith('list:reference') and \ if hasattr(referenced, '_format') and referenced._format:
field_type.find('.') < 0 and \ requires = validators.IS_IN_DB(db, referenced[field_name],referenced._format)
field_type[15:] in db.tables: else:
referenced = db[field_type[15:]] requires = validators.IS_IN_DB(db, referenced[field_name])
if hasattr(referenced, '_format') and referenced._format: if field.unique:
requires = validators.IS_IN_DB(db, referenced._id, requires._and = validators.IS_NOT_IN_DB(db, field)
referenced._format, multiple=True) if not field.notnull:
else: requires = validators.IS_EMPTY_OR(requires)
requires = validators.IS_IN_DB(db, referenced._id, return requires
multiple=True) elif db and field_type.startswith('list:reference'):
if field_type.find('.') < 0 and field_type[15:] in db.tables:
referenced = db[field_type[15:]]
if hasattr(referenced, '_format') and referenced._format:
requires = validators.IS_IN_DB(db, referenced._id,
referenced._format, multiple=True)
else:
requires = validators.IS_IN_DB(db, referenced._id,
multiple=True)
elif field_type.find('.') > 0 and field_type[15:].split('.')[0] in db.tables:
table_field = field_type[15:].split('.')
table_name=table_field[0]
field_name=table_field[1]
referenced = db[table_name]
if hasattr(referenced, '_format') and referenced._format:
requires = validators.IS_IN_DB(db, referenced[field_name],
referenced._format, multiple=True)
else:
requires = validators.IS_IN_DB(db, referenced[field_name],
multiple=True)
if field.unique: if field.unique:
requires._and = validators.IS_NOT_IN_DB(db, field) requires._and = validators.IS_NOT_IN_DB(db, field)
if not field.notnull: if not field.notnull:
requires = validators.IS_EMPTY_OR(requires) requires = validators.IS_EMPTY_OR(requires)
return requires return requires
# does not get here for reference and list:reference # does not get here for reference and list:reference
if field.unique: if isinstance(requires, list):
requires.insert(0, validators.IS_NOT_IN_DB(db, field)) if field.unique:
excluded_fields = ['string', 'upload', 'text', 'password', 'boolean'] requires.insert(0, validators.IS_NOT_IN_DB(db, field))
if (field.notnull or field.unique) and field_type not in excluded_fields: excluded_fields = ['string', 'upload', 'text', 'password', 'boolean']
requires.insert(0, validators.IS_NOT_EMPTY()) if (field.notnull or field.unique) and field_type not in excluded_fields:
elif not field.notnull and not field.unique and requires: requires.insert(0, validators.IS_NOT_EMPTY())
requires[0] = \ elif not field.notnull and not field.unique and requires:
validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None) null = null='' if field.type in ('string', 'text', 'password') else None
requires[0] = validators.IS_EMPTY_OR(requires[0], null=null)
return requires return requires
DAL.serializers = {'json': custom_json, 'xml': xml} DAL.serializers = {'json': custom_json, 'xml': xml}
+72 -33
View File
@@ -220,7 +220,12 @@ class Request(Storage):
if is_json: if is_json:
try: try:
json_vars = json_parser.load(body) # In Python 3 versions prior to 3.6 load doesn't accept bytes and
# bytearray, so we read the body convert to native and use loads
# 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()))
except: except:
# incoherent request bodies can still be parsed "ad-hoc" # incoherent request bodies can still be parsed "ad-hoc"
json_vars = {} json_vars = {}
@@ -331,11 +336,16 @@ class Request(Storage):
user_agent = session._user_agent user_agent = session._user_agent
if user_agent: if user_agent:
return user_agent return user_agent
user_agent = user_agent_parser.detect(self.env.http_user_agent) http_user_agent = self.env.http_user_agent or ''
user_agent = user_agent_parser.detect(http_user_agent)
for key, value in user_agent.items(): for key, value in user_agent.items():
if isinstance(value, dict): if isinstance(value, dict):
user_agent[key] = Storage(value) user_agent[key] = Storage(value)
user_agent = session._user_agent = Storage(user_agent) 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
return user_agent return user_agent
def requires_https(self): def requires_https(self):
@@ -468,45 +478,67 @@ class Response(Storage):
response.cache_includes = (cache_method, time_expire). response.cache_includes = (cache_method, time_expire).
Example: (cache.disk, 60) # caches to disk for 1 minute. Example: (cache.disk, 60) # caches to disk for 1 minute.
""" """
app = current.request.application
# We start by building a files list in which adjacent files internal to
# the application are placed in a list inside the files list.
#
# 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
# internal files may be interleaved with external ones.
files = [] files = []
ext_files = [] # For the adjacent list we're going to use storage List to both distinguish
has_js = has_css = False # from the regular list and so we can add attributes
internal = List()
internal.has_js = False
internal.has_css = False
done = set() # to remove duplicates
for item in self.files: for item in self.files:
if isinstance(item, (list, tuple)): if not isinstance(item, list):
ext_files.append(item) if item in done:
continue
done.add(item)
if isinstance(item, (list, tuple)) or not item.startswith('/' + app): # also consider items in other web2py applications to be external
if internal:
files.append(internal)
internal = List()
internal.has_js = False
internal.has_css = False
files.append(item)
continue continue
if extensions and not item.rpartition('.')[2] in extensions: if extensions and not item.rpartition('.')[2] in extensions:
continue continue
if item in files: internal.append(item)
continue
if item.endswith('.js'): if item.endswith('.js'):
has_js = True internal.has_js = True
if item.endswith('.css'): if item.endswith('.css'):
has_css = True internal.has_css = True
files.append(item) if internal:
files.append(internal)
if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)): # We're done we can now minify
# cache for 5 minutes by default if have_minify:
key = hashlib_md5(repr(files)).hexdigest() for i, f in enumerate(files):
cache = self.cache_includes or (current.cache.ram, 60 * 5) if isinstance(f, List) and ((self.optimize_css and f.has_css) or (self.optimize_js and f.has_js)):
# cache for 5 minutes by default
key = hashlib_md5(repr(f)).hexdigest()
cache = self.cache_includes or (current.cache.ram, 60 * 5)
def call_minify(files=f):
return List(minify.minify(files,
URL('static', 'temp'),
current.request.folder,
self.optimize_css,
self.optimize_js))
if cache:
cache_model, time_expire = cache
files[i] = cache_model('response.files.minified/' + key,
call_minify,
time_expire)
else:
files[i] = call_minify()
def call_minify(files=files): def static_map(s, item):
return minify.minify(files,
URL('static', 'temp'),
current.request.folder,
self.optimize_css,
self.optimize_js)
if cache:
cache_model, time_expire = cache
files = cache_model('response.files.minified/' + key,
call_minify,
time_expire)
else:
files = call_minify()
files.extend(ext_files)
s = []
for item in files:
if isinstance(item, str): if isinstance(item, str):
f = item.lower().split('?')[0] f = item.lower().split('?')[0]
ext = f.rpartition('.')[2] ext = f.rpartition('.')[2]
@@ -526,6 +558,13 @@ class Response(Storage):
if tmpl: if tmpl:
s.append(tmpl % item[1]) s.append(tmpl % item[1])
s = []
for item in files:
if isinstance(item, List):
for f in item:
static_map(s, f)
else:
static_map(s, item)
self.write(''.join(s), escape=False) self.write(''.join(s), escape=False)
def stream(self, def stream(self,
+9 -4
View File
@@ -18,10 +18,11 @@ import pkgutil
import logging import logging
from cgi import escape from cgi import escape
from threading import RLock from threading import RLock
from gluon.utf8 import Utf8
from gluon.utils import local_html_escape from gluon.utils import local_html_escape
from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin
from pydal.contrib.portalocker import read_locked, LockedFile from pydal.contrib.portalocker import read_locked, LockedFile
from gluon.fileutils import listdir from gluon.fileutils import listdir
@@ -49,8 +50,10 @@ DEFAULT_CONSTRUCT_PLURAL_FORM = lambda word, plural_id: word
if PY2: if PY2:
NUMBERS = (int, long, float) NUMBERS = (int, long, float)
from gluon.utf8 import Utf8
else: else:
NUMBERS = (int, float) NUMBERS = (int, float)
Utf8 = str
# pattern to find T(blah blah blah) expressions # pattern to find T(blah blah blah) expressions
PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\ PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\
@@ -107,15 +110,17 @@ def markmin(s):
def upper_fun(s): def upper_fun(s):
return unicode(s, 'utf-8').upper().encode('utf-8') return to_unicode(s).upper()
def title_fun(s): def title_fun(s):
return unicode(s, 'utf-8').title().encode('utf-8') return to_unicode(s).title()
def cap_fun(s): def cap_fun(s):
return unicode(s, 'utf-8').capitalize().encode('utf-8') return to_unicode(s).capitalize()
ttab_in = maketrans("\\%{}", '\x1c\x1d\x1e\x1f') ttab_in = maketrans("\\%{}", '\x1c\x1d\x1e\x1f')
ttab_out = maketrans('\x1c\x1d\x1e\x1f', "\\%{}") ttab_out = maketrans('\x1c\x1d\x1e\x1f', "\\%{}")
+1 -1
View File
@@ -231,7 +231,7 @@ class LazyWSGI(object):
to call third party WSGI applications to call third party WSGI applications
""" """
self.response.status = str(status).split(' ', 1)[0] self.response.status = int(str(status).split(' ', 1)[0])
self.response.headers = dict(headers) self.response.headers = dict(headers)
return lambda *args, **kargs: \ return lambda *args, **kargs: \
self.response.write(escape=False, *args, **kargs) self.response.write(escape=False, *args, **kargs)
+4 -2
View File
@@ -205,7 +205,7 @@ def compile2(code, layer):
return compile(code, layer, 'exec') return compile(code, layer, 'exec')
def restricted(ccode, environment=None, layer='Unknown'): def restricted(ccode, environment=None, layer='Unknown', scode=None):
""" """
Runs code in environment and returns the output. If an exception occurs Runs code in environment and returns the output. If an exception occurs
in code it raises a RestrictedError containing the traceback. Layer is in code it raises a RestrictedError containing the traceback. Layer is
@@ -230,7 +230,9 @@ def restricted(ccode, environment=None, layer='Unknown'):
sys.excepthook(etype, evalue, tb) sys.excepthook(etype, evalue, tb)
del tb del tb
output = "%s %s" % (etype, evalue) output = "%s %s" % (etype, evalue)
raise RestrictedError(layer, ccode, output, environment) # Save source code in ticket when available
scode = scode if scode else ccode
raise RestrictedError(layer, scode, output, environment)
def snapshot(info=None, context=5, code=None, environment=None): def snapshot(info=None, context=5, code=None, environment=None):
+1 -1
View File
@@ -145,7 +145,7 @@ class XssCleaner(HTMLParser):
if url.startswith('#'): if url.startswith('#'):
return True return True
else: else:
parsed = urlparse(url) parsed = urlparse.urlparse(url)
return ((parsed[0] in self.allowed_schemes and '.' in parsed[1]) or return ((parsed[0] in self.allowed_schemes and '.' in parsed[1]) or
(parsed[0] in self.allowed_schemes and '@' in parsed[2]) or (parsed[0] in self.allowed_schemes and '@' in parsed[2]) or
(parsed[0] == '' and parsed[2].startswith('/'))) (parsed[0] == '' and parsed[2].startswith('/')))
+7 -1
View File
@@ -31,10 +31,16 @@ from gluon.globals import Request, Response, Session
from gluon.storage import Storage, List from gluon.storage import Storage, List
from gluon.admin import w2p_unpack from gluon.admin import w2p_unpack
from pydal.base import BaseAdapter from pydal.base import BaseAdapter
from gluon._compat import iteritems, ClassType from gluon._compat import iteritems, ClassType, PY2
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
if not PY2:
def execfile(filename, global_vars=None, local_vars=None):
with open(filename) as f:
code = compile(f.read(), filename, 'exec')
exec(code, global_vars, local_vars)
def enable_autocomplete_and_history(adir, env): def enable_autocomplete_and_history(adir, env):
try: try:
+114 -9
View File
@@ -17,6 +17,7 @@ Holds:
import datetime import datetime
import urllib import urllib
import re import re
import copy
import os import os
from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native, to_unicode from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native, to_unicode
@@ -682,7 +683,7 @@ class AutocompleteWidget(object):
else: else:
self.is_reference = False self.is_reference = False
if hasattr(request, 'application'): if hasattr(request, 'application'):
urlvars = request.vars urlvars = copy.copy(request.vars)
urlvars[default_var] = 1 urlvars[default_var] = 1
self.url = URL(args=request.args, vars=urlvars, self.url = URL(args=request.args, vars=urlvars,
user_signature=user_signature, hash_vars=hash_vars) user_signature=user_signature, hash_vars=hash_vars)
@@ -1087,6 +1088,106 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
return parent return parent
return _inner return _inner
# bootstrap 4
def formstyle_bootstrap4_stacked(form, fields):
""" bootstrap 3 format form layout
Note:
Experimental!
"""
parent = CAT()
for id, label, controls, help in fields:
# wrappers
_help = SPAN(help, _class='help-block')
# embed _help into _controls
_controls = CAT(controls, _help)
if isinstance(controls, INPUT):
if controls['_type'] == 'submit':
controls.add_class('btn btn-primary')
if controls['_type'] == 'button':
controls.add_class('btn btn-secondary')
elif controls['_type'] == 'file':
controls.add_class('input-file')
elif controls['_type'] in ('text', 'password'):
controls.add_class('form-control')
elif controls['_type'] == 'checkbox':
label['_for'] = None
label.insert(0, controls)
label.insert(0, ' ')
_controls = DIV(label, _help, _class="checkbox")
label = ''
elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control')
elif isinstance(controls, SPAN):
_controls = P(controls.components)
elif isinstance(controls, UL):
for e in controls.elements("input"):
e.add_class('form-control')
elif isinstance(controls, CAT) and isinstance(controls[0], INPUT):
controls[0].add_class('form-control')
if isinstance(label, LABEL):
label['_class'] = add_class(label.get('_class'), 'form-control-label')
parent.append(DIV(label, _controls, _class='form-group', _id=id))
return parent
def formstyle_bootstrap4_inline_factory(col_label_size=3):
""" bootstrap 3 horizontal form layout
Note:
Experimental!
"""
def _inner(form, fields):
form.add_class('form-horizontal')
label_col_class = "col-sm-%d" % col_label_size
col_class = "col-sm-%d" % (12 - col_label_size)
offset_class = "col-sm-offset-%d" % col_label_size
parent = CAT()
for id, label, controls, help in fields:
# wrappers
_help = SPAN(help, _class='help-block')
# embed _help into _controls
_controls = DIV(controls, _help, _class="%s" % (col_class))
if isinstance(controls, INPUT):
if controls['_type'] == 'submit':
controls.add_class('btn btn-primary')
_controls = DIV(controls, _class="%s %s" % (col_class, offset_class))
if controls['_type'] == 'button':
controls.add_class('btn btn-secondary')
elif controls['_type'] == 'file':
controls.add_class('input-file')
elif controls['_type'] in ('text', 'password'):
controls.add_class('form-control')
elif controls['_type'] == 'checkbox':
label['_for'] = None
label.insert(0, controls)
label.insert(1, ' ')
_controls = DIV(DIV(label, _help, _class="checkbox"),
_class="%s %s" % (offset_class, col_class))
label = ''
elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control')
elif isinstance(controls, SPAN):
_controls = P(controls.components,
_class="form-control-static %s" % col_class)
elif isinstance(controls, UL):
for e in controls.elements("input"):
e.add_class('form-control')
elif isinstance(controls, CAT) and isinstance(controls[0], INPUT):
controls[0].add_class('form-control')
if isinstance(label, LABEL):
label['_class'] = add_class(label.get('_class'), 'form-control-label %s' % label_col_class)
parent.append(DIV(label, _controls, _class='form-group', _id=id))
return parent
return _inner
class SQLFORM(FORM): class SQLFORM(FORM):
@@ -1175,6 +1276,8 @@ class SQLFORM(FORM):
bootstrap=formstyle_bootstrap, bootstrap=formstyle_bootstrap,
bootstrap3_stacked=formstyle_bootstrap3_stacked, bootstrap3_stacked=formstyle_bootstrap3_stacked,
bootstrap3_inline=formstyle_bootstrap3_inline_factory(3), bootstrap3_inline=formstyle_bootstrap3_inline_factory(3),
bootstrap4_stacked=formstyle_bootstrap4_stacked,
bootstrap4_inline=formstyle_bootstrap4_inline_factory(3),
inline=formstyle_inline, inline=formstyle_inline,
) )
@@ -1244,7 +1347,7 @@ class SQLFORM(FORM):
if fields is None: if fields is None:
fields = [f.name for f in table if fields = [f.name for f in table if
(ignore_rw or f.writable or f.readable) and (ignore_rw or f.writable or f.readable) and
(readonly or not f.compute)] not (f.compute and not record)]
self.fields = fields self.fields = fields
# make sure we have an id # make sure we have an id
@@ -1327,7 +1430,7 @@ class SQLFORM(FORM):
label = LABEL(label, label and sep, _for=field_id, label = LABEL(label, label and sep, _for=field_id,
_id=field_id + SQLFORM.ID_LABEL_SUFFIX) _id=field_id + SQLFORM.ID_LABEL_SUFFIX)
cond = readonly or \ cond = readonly or field.compute or \
(not ignore_rw and not field.writable and field.readable) (not ignore_rw and not field.writable and field.readable)
if cond: if cond:
@@ -2347,12 +2450,12 @@ class SQLFORM(FORM):
for k, f in iteritems(table): for k, f in iteritems(table):
if isinstance(f, Field.Virtual): if isinstance(f, Field.Virtual):
f.tablename = table._tablename f.tablename = table._tablename
columns = [f for f in fields if f.tablename in tablenames] columns = [f for f in fields if f.tablename in tablenames and f.listable]
else: else:
fields = [] fields = []
columns = [] columns = []
filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs) filter1 = lambda f: isinstance(f, Field) and (f.type!='blob' or showblobs)
filter2 = lambda f: isinstance(f, Field) and f.readable filter2 = lambda f: isinstance(f, Field) and f.readable and f.listable
for table in tables: for table in tables:
fields += filter(filter1, table) fields += filter(filter1, table)
columns += filter(filter2, table) columns += filter(filter2, table)
@@ -2570,8 +2673,8 @@ class SQLFORM(FORM):
try: try:
# the query should be constructed using searchable # the query should be constructed using searchable
# fields but not virtual fields # fields but not virtual fields
sfields = reduce(lambda a, b: a + b, is_searchable = lambda f: f.readable and not isinstance(f, Field.Virtual) and f.searchable
[[f for f in t if f.readable and not isinstance(f, Field.Virtual)] for t in tables]) sfields = reduce(lambda a, b: a + b, [filter(is_searchable, t) for t in tables])
# use custom_query using searchable # use custom_query using searchable
if callable(searchable): if callable(searchable):
dbset = dbset(searchable(sfields, keywords)) dbset = dbset(searchable(sfields, keywords))
@@ -3327,7 +3430,7 @@ class SQLTABLE(TABLE):
return return
REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD
fieldmap = dict(zip(sqlrows.colnames, sqlrows.fields)) fieldmap = dict(zip(sqlrows.colnames, sqlrows.fields))
tablemap = dict(((f.tablename, f.table) for f in fieldmap.values())) tablemap = dict(((f.tablename, f.table) if isinstance(f, Field) else (f._table._tablename, f._table) for f in fieldmap.values()))
for table in tablemap.values(): for table in tablemap.values():
pref = table._tablename + '.' pref = table._tablename + '.'
fieldmap.update(((pref+f.name, f) for f in table._virtual_fields)) fieldmap.update(((pref+f.name, f) for f in table._virtual_fields))
@@ -3570,7 +3673,9 @@ class ExportClass(object):
if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col): if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col):
row.append(record._extra[col]) row.append(record._extra[col])
else: else:
(t, f) = col.split('.') # The grid code modifies rows.colnames, adding double quotes
# around the table and field names -- so they must be removed here.
(t, f) = [name.strip('"') for name in col.split('.')]
field = self.rows.db[t][f] field = self.rows.db[t][f]
if isinstance(record.get(t, None), (Row, dict)): if isinstance(record.get(t, None), (Row, dict)):
value = record[t][f] value = record[t][f]
+3
View File
@@ -138,6 +138,9 @@ class TestAuthAPI(unittest.TestCase):
self.assertTrue('new_password2' in result['errors']) self.assertTrue('new_password2' in result['errors'])
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234') result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.assertTrue('old_password' in result['errors']) self.assertTrue('old_password' in result['errors'])
# Test the default 4 min_length is enforced on change password
result = self.auth.change_password(old_password='1234', new_password='123', new_password2='123')
self.assertTrue('new_password' in result['errors'])
def test_verify_key(self): def test_verify_key(self):
self.auth.settings.registration_requires_verification = True self.auth.settings.registration_requires_verification = True
+2 -2
View File
@@ -158,10 +158,10 @@ class testResponse(unittest.TestCase):
response.files.append(URL('a', 'static', 'css/file.ts')) response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response) content = return_includes(response)
self.assertEqual(content, self.assertEqual(content,
'<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>' +
'<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' + '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>' +
'<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' + '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />' +
'<script src="/a/static/css/file.ts" type="text/typescript"></script>' + '<script src="/a/static/css/file.ts" type="text/typescript"></script>'
'<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
) )
response = Response() response = Response()
+4 -4
View File
@@ -355,10 +355,10 @@ class TestSQLTABLE(unittest.TestCase):
self.db.commit() self.db.commit()
# def test_SQLTABLE(self): def test_SQLTABLE(self):
# rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL) rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL)
# sqltable = SQLTABLE(rows) sqltable = SQLTABLE(rows)
# self.assertEqual(sqltable.xml(), '<table><thead><tr><th>auth_user.id</th><th>auth_user.first_name</th><th>auth_user.last_name</th><th>auth_user.email</th><th>auth_user.username</th><th>auth_user.password</th><th>auth_user.registration_key</th><th>auth_user.reset_password_key</th><th>auth_user.registration_id</th></tr></thead><tbody><tr class="w2p_odd odd"><td>1</td><td>Bart</td><td>Simpson</td><td>user1@test.com</td><td>user1</td><td>password_123</td><td>None</td><td></td><td>None</td></tr></tbody></table>') self.assertEqual(sqltable.xml(), b'<table><thead><tr><th>auth_user.id</th><th>auth_user.first_name</th><th>auth_user.last_name</th><th>auth_user.email</th><th>auth_user.username</th><th>auth_user.password</th><th>auth_user.registration_key</th><th>auth_user.reset_password_key</th><th>auth_user.registration_id</th></tr></thead><tbody><tr class="w2p_odd odd"><td>1</td><td>Bart</td><td>Simpson</td><td>user1@test.com</td><td>user1</td><td>password_123</td><td>None</td><td></td><td>None</td></tr></tbody></table>')
# class TestExportClass(unittest.TestCase): # class TestExportClass(unittest.TestCase):
+62
View File
@@ -278,8 +278,68 @@ class TestValidators(unittest.TestCase):
self.assertEqual(rtn, ('jerry', 'oops')) self.assertEqual(rtn, ('jerry', 'oops'))
rtn = IS_IN_DB(db, 'person.id', '%(name)s', auto_add=True)('jerry') rtn = IS_IN_DB(db, 'person.id', '%(name)s', auto_add=True)('jerry')
self.assertEqual(rtn, (3, None)) self.assertEqual(rtn, (3, None))
# Test it works with reference table
db.define_table('ref_table',
Field('name'),
Field('person_id', 'reference person')
)
ret = db.ref_table.validate_and_insert(name='test reference table')
self.assertFalse(list(ret.errors))
ret = db.ref_table.validate_and_insert(name='test reference table', person_id=george_id)
self.assertFalse(list(ret.errors))
rtn = IS_IN_DB(db, 'ref_table.person_id', '%(name)s')(george_id)
self.assertEqual(rtn, (george_id, None))
# Test it works with reference table.field and keyed table
db.define_table('person_keyed',
Field('name'),
primarykey=['name'])
db.person_keyed.insert(name='george')
db.person_keyed.insert(name='costanza')
rtn = IS_IN_DB(db, 'person_keyed.name')('george')
self.assertEqual(rtn, ('george', None))
db.define_table('ref_table_field',
Field('name'),
Field('person_name', 'reference person_keyed.name')
)
ret = db.ref_table_field.validate_and_insert(name='test reference table.field')
self.assertFalse(list(ret.errors))
ret = db.ref_table_field.validate_and_insert(name='test reference table.field', person_name='george')
self.assertFalse(list(ret.errors))
vldtr = IS_IN_DB(db, 'ref_table_field.person_name', '%(name)s')
vldtr.options()
rtn = vldtr('george')
self.assertEqual(rtn, ('george', None))
# Test it works with list:reference table
db.define_table('list_ref_table',
Field('name'),
Field('person_list', 'list:reference person'))
ret = db.list_ref_table.validate_and_insert(name='test list:reference table')
self.assertFalse(list(ret.errors))
ret = db.list_ref_table.validate_and_insert(name='test list:reference table', person_list=[george_id,costanza_id])
self.assertFalse(list(ret.errors))
vldtr = IS_IN_DB(db, 'list_ref_table.person_list')
vldtr.options()
rtn = vldtr([george_id,costanza_id])
self.assertEqual(rtn, ([george_id,costanza_id], None))
# Test it works with list:reference table.field and keyed table
#db.define_table('list_ref_table_field',
# Field('name'),
# Field('person_list', 'list:reference person_keyed.name'))
#ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field')
#self.assertFalse(list(ret.errors))
#ret = db.list_ref_table_field.validate_and_insert(name='test list:reference table.field', person_list=['george','costanza'])
#self.assertFalse(list(ret.errors))
#vldtr = IS_IN_DB(db, 'list_ref_table_field.person_list')
#vldtr.options()
#rtn = vldtr(['george','costanza'])
#self.assertEqual(rtn, (['george','costanza'], None))
db.person.drop() db.person.drop()
db.category.drop() db.category.drop()
db.person_keyed.drop()
db.ref_table.drop()
db.ref_table_field.drop()
db.list_ref_table.drop()
#db.list_ref_table_field.drop()
def test_IS_NOT_IN_DB(self): def test_IS_NOT_IN_DB(self):
from gluon.dal import DAL, Field from gluon.dal import DAL, Field
@@ -444,6 +504,8 @@ class TestValidators(unittest.TestCase):
self.assertEqual(rtn, (None, 'Enter a value')) self.assertEqual(rtn, (None, 'Enter a value'))
rtn = IS_NOT_EMPTY()('') rtn = IS_NOT_EMPTY()('')
self.assertEqual(rtn, ('', 'Enter a value')) self.assertEqual(rtn, ('', 'Enter a value'))
rtn = IS_NOT_EMPTY()(b'')
self.assertEqual(rtn, (b'', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' ') rtn = IS_NOT_EMPTY()(' ')
self.assertEqual(rtn, (' ', 'Enter a value')) self.assertEqual(rtn, (' ', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' \n\t') rtn = IS_NOT_EMPTY()(' \n\t')
+2 -2
View File
@@ -102,12 +102,12 @@ class TestWeb(LiveTest):
password='test', password='test',
_formname='login') _formname='login')
client.post('user/login', data=data) client.post('user/login', data=data)
self.assertTrue('Welcome Homer' in client.text) self.assertTrue('Homer' in client.text)
# check registration and login were successful # check registration and login were successful
client.get('index') client.get('index')
self.assertTrue('Welcome Homer' in client.text) self.assertTrue('Homer' in client.text)
client = WebClient('http://127.0.0.1:8000/admin/default/') client = WebClient('http://127.0.0.1:8000/admin/default/')
client.post('index', data=dict(password='testpass')) client.post('index', data=dict(password='testpass'))
+3 -2
View File
@@ -3693,7 +3693,8 @@ class Auth(AuthAPI):
requires = [requires] requires = [requires]
requires = list(filter(lambda t: isinstance(t, CRYPT), requires)) requires = list(filter(lambda t: isinstance(t, CRYPT), requires))
if requires: if requires:
requires[0].min_length = 0 requires[0] = CRYPT(**requires[0].__dict__) # Copy the existing CRYPT attributes
requires[0].min_length = 0 # But do not enforce minimum length for the old password
form = SQLFORM.factory( form = SQLFORM.factory(
Field('old_password', 'password', requires=requires, Field('old_password', 'password', requires=requires,
label=self.messages.old_password), label=self.messages.old_password),
@@ -3777,9 +3778,9 @@ class Auth(AuthAPI):
if any(f.compute for f in extra_fields): if any(f.compute for f in extra_fields):
user = table_user[self.user.id] user = table_user[self.user.id]
self._update_session_user(user) self._update_session_user(user)
self.update_groups()
else: else:
self.user.update(table_user._filter_fields(form.vars)) self.user.update(table_user._filter_fields(form.vars))
session.flash = self.messages.profile_updated session.flash = self.messages.profile_updated
self.log_event(log, self.user) self.log_event(log, self.user)
callback(onaccept, form) callback(onaccept, form)
+1 -1
View File
@@ -996,7 +996,7 @@ def is_empty(value, empty_regex=None):
value = value.strip() value = value.strip()
if empty_regex is not None and empty_regex.match(value): if empty_regex is not None and empty_regex.match(value):
value = '' value = ''
if value is None or value == '' or value == []: if value is None or value == '' or value == b'' or value == []:
return (_value, True) return (_value, True)
return (_value, False) return (_value, False)
+4 -1
View File
@@ -1076,7 +1076,10 @@ def start_schedulers(options):
return return
# Work around OS X problem: http://bugs.python.org/issue9405 # Work around OS X problem: http://bugs.python.org/issue9405
import urllib if PY2:
import urllib
else:
import urllib.request as urllib
urllib.getproxies() urllib.getproxies()
for app in apps: for app in apps:
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Written by Vinyl Darkscratch, www.queengoob.org
import sys
import os
# TODO: add comments
# This script can be run with no arguments (which sets the language folder to the current working directory, and default language to English), one argument (which sets the default language), or two arguments (language folder path and default language).
# When run, this script will compare the original language's strings to their assigned values and determine if they match, just like web2py's interface. While some phrases may be the exact same between both languages, this should provide a good idea of how far along translations are.
def check_lang_progress(cwd, default_lang):
for x in os.listdir(cwd):
if x == default_lang or x.startswith("plural-"): continue
data = eval(open(os.path.join(cwd, x)).read())
total = 0
translated = 0
for key in data:
total += 1
if key.replace('@markmin\x01', '') != data[key]: translated += 1
print "Translations for %s (%s): %d/%d Translated (%d Untranslated)" %(data['!langname!'], data['!langcode!'], translated, total, total-translated)
if __name__ == "__main__":
cwd = os.getcwd()
default_lang = 'en'
if len(sys.argv) > 2:
cwd = sys.argv[1]
default_lang = sys.argv[2]
elif len(sys.argv) > 1:
default_lang = sys.argv[1]
check_lang_progress(cwd, default_lang)
+1 -1
View File
@@ -222,7 +222,7 @@ echo <<EOF
you can stop uwsgi and nginx with you can stop uwsgi and nginx with
sudo /etc/init.d/nginx stop sudo /etc/init.d/nginx stop
sudo systemctl start emperor.uwsgi.service sudo systemctl stop emperor.uwsgi.service
and start it with and start it with
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Written by Vinyl Darkscratch, www.queengoob.org
# TODO: add comments
import os
import ast
import sys
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
from gluon.cfs import getcfs
from gluon.utf8 import Utf8
from gluon._compat import copyreg, PY2, maketrans, iterkeys, unicodeT, to_unicode, to_bytes, iteritems, to_native, pjoin
from gluon.languages import findT
# This script can be run with no arguments (which sets the language folder to the current working directory, and default language to English), one argument (which sets the default language), or two arguments (language folder path and default language).
# When run, it will update the default language, as well as strip all of the strings found in the non-default languages but not in the default language, and add the strings found in the default language to the non-default languages it is not, making sure translators don't do additional work that will never be used.
def read_dict_aux(filename):
lang_text = open(filename, 'r').read().replace(b'\r\n', b'\n')
try:
return safe_eval(to_native(lang_text)) or {}
except Exception:
e = sys.exc_info()[1]
status = 'Syntax error in %s (%s)' % (filename, e)
return {'__corrupted__': status}
def read_dict(filename):
return getcfs('lang:' + filename, filename, lambda: read_dict_aux(filename))
def safe_eval(text):
if text.strip():
try:
return ast.literal_eval(text)
except ImportError:
return eval(text, {}, {})
return None
def sort_function(x, y):
return cmp(unicode(x, 'utf-8').lower(), unicode(y, 'utf-8').lower())
def write_file(file, contents):
file.write('# -*- coding: utf-8 -*-\n{\n')
for key in sorted(contents, sort_function):
file.write('%s: %s,\n' % (repr(Utf8(key)),
repr(Utf8(contents[key]))))
file.write('}\n')
file.close()
def update_languages(cwd, default_lang):
defaultfp = os.path.join(cwd, '%s.py' %default_lang)
findT(cwd, default_lang)
default = read_dict(defaultfp)
for x in os.listdir(cwd):
if x == default_lang or x.startswith("plural-"): continue
i18n = read_dict(os.path.join(cwd, x))
if i18n:
nd = default
for k_d1 in i18n:
if k_d1 in default:
nd[k_d1] = i18n[k_d1]
write_file(open(x, 'w'), nd)
print x
if __name__ == "__main__":
cwd = os.getcwd()
default_lang = 'en'
if len(sys.argv) > 2:
cwd = sys.argv[1]
default_lang = sys.argv[2]
elif len(sys.argv) > 1:
default_lang = sys.argv[1]
update_languages(cwd, default_lang)