Compare commits

...

122 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
mdipierro 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
mdipierro 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
mdipierro 8aa07760d2 Merge pull request #1803 from leonelcamara/patch-10
Fix #1715
2017-11-13 21:13:06 -06:00
mdipierro 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âmara e44a12eaf8 Update supported python versions 2017-11-13 22:40:37 +00:00
Leonel Câmara 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
mdipierro eb7222aa92 Merge pull request #1802 from leonelcamara/fixsqltable
Fix a problem with Expressions in SQLTABLE
2017-11-10 18:12:50 -06:00
mdipierro 009a0b87f2 Merge pull request #1801 from leonelcamara/i1800
Fixes #1800
2017-11-10 18:12:16 -06:00
mdipierro aeb0244b2c Merge pull request #1799 from vinyldarkscratch/master
Add two scripts
2017-11-10 18:12:03 -06:00
mdipierro 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
mdipierro 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
mdipierro 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
Дим Щ c7d415fefd compatible unicode 2017-10-28 19:32:48 +03:00
Дим Щ 2e572aee9a Update custom_import.py 2017-10-28 19:29:55 +03:00
Дим Щ dd14d1c8c9 Add reload 2017-10-28 19:28:51 +03:00
Дим Щ 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
mdipierro 1e586b32a4 Merge pull request #1789 from timnyborg/master
Fix setting 'ac' in request.vars
2017-10-18 22:13:16 -05:00
mdipierro 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 Nyborg 2e6f529dfd Fix setting 'ac' in request.vars 2017-10-12 17:18:06 +01:00
Tim Nyborg 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 Soto 5f4c47729b Removed a tab and replaced by spaces 2017-09-21 10:17:17 +02:00
mdipierro 5975e4767f Merge pull request #1766 from abastardi/tsv_export
Fix grid TSV export bug
2017-09-21 00:23:17 -05:00
mdipierro 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
mdipierro 9af0d8c3c9 Merge pull request #1758 from leonelcamara/patch-9
Fix #1757
2017-09-21 00:21:10 -05:00
mdipierro 2e2d3482e4 Merge pull request #1756 from leonelcamara/i1752
Make _compat accept ducks
2017-09-21 00:20:54 -05:00
mdipierro 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âmara 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
mdipierro bd167aa94a Merge pull request #1749 from liuyigh/patch-1
Minor error in EOF instructions
2017-08-31 17:29:56 -05:00
Yi Liu c9a71a7055 MInor error in EOF instructions
start -> stop
2017-08-29 11:26:22 -07:00
mdipierro 02e50cadbc Merge pull request #1746 from abastardi/disable-submit
Fix submit button disable bug
2017-08-28 11:25:55 -05:00
mdipierro 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
mdipierro 56d10a40c6 Merge pull request #1741 from leonelcamara/patch-8
execfile shim for python 3 compatibility
2017-08-28 11:25:28 -05:00
mdipierro 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âmara f22e3a7624 execfile shim for python 3 compatibility
Fixes #1739
2017-08-16 16:35:46 +01:00
Leonel Câmara bd7ee209ea Fixes #1737
getproxies is in urllib.request for python 3
2017-08-15 15:34:35 +01:00
mdipierro 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
mdipierro 1d0f322d09 Merge pull request #1731 from leonelcamara/fix-include-files
Fix include files
2017-08-09 15:46:51 -05:00
Leonel Câmara 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âmara 6e5c8b62cc Make test_include_files demand the same order 2017-08-09 01:05:37 +01:00
mdipierro 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
mdipierro 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
mdipierro 7e96ecafd7 R-2.15.3 2017-08-07 07:41:11 -05:00
mdipierro 86ea728f4f R-2.15.3 2017-08-07 07:32:58 -05:00
mdipierro a29947f298 websocket_messaging in Python 3 #1696, thanks hirolau 2017-08-07 07:27:28 -05:00
mdipierro dc29f33365 addressed __ssl.pyd issue #1716, thanks Jhleite 2017-08-07 07:18:42 -05:00
mdipierro 834460f0cc Merge pull request #1723 from leonelcamara/patch-6
Small unicode compatibility py3 fixes
2017-08-07 07:09:30 -05:00
mdipierro 5353e17c66 Merge pull request #1720 from leonelcamara/patch-4
Fix BEAUTIFY trying to display uploaded file contents
2017-08-07 07:08:58 -05:00
mdipierro d2022ca500 Merge pull request #1719 from leonelcamara/patch-3
Fix response.download with nonasccii filenames
2017-08-07 07:08:33 -05:00
mdipierro c560f607af Merge pull request #1710 from willimoa/issue/1704
Fix Issue 1709
2017-08-07 07:08:10 -05:00
Leonel Câmara 3eea1f68f4 Make sure you return bytes when you str(body) 2017-08-07 00:20:29 +01:00
Leonel Câmara 10b6b93cb2 minor py3 compatibility change 2017-08-07 00:17:43 +01:00
Leonel Câmara 0b41ed36f9 mobilize is back
Fixes #1721
2017-08-06 19:20:01 +01:00
Leonel Câmara 90e20a4f39 Fix BEAUTIFY trying to display uploaded file contents
Fixes #1717
2017-08-06 17:01:27 +01:00
Leonel Câmara a744835f21 Fix response.download with nonasccii filenames
Fixes #1718
2017-08-05 13:27:44 +01:00
mdipierro ebc614bf91 fixed has_ssl, thanks leonel 2017-08-04 10:21:43 -05:00
mdipierro b7270df9c3 fixed issue topic/web2py/UnN6AyOh2Lg thanks Paul 2017-08-04 09:58:05 -05:00
Andrew Willimott 4a47bb8e83 d3_graph_model fixed, no longer hardcoded for a “db” database 2017-08-02 21:47:26 +12:00
mdipierro 213c4ee7d1 fixed use of whitespaces 2017-08-01 10:26:33 -05:00
mdipierro 7088b74d42 Merge pull request #1705 from josedesoto/enhancement/1557
Enhancement/1557
2017-08-01 09:46:55 -05:00
mdipierro 752b5a8cdb Merge pull request #1704 from josedesoto/enhancement/response_json_sort_paramenter
Allow response.json to be sortable or not
2017-08-01 09:46:11 -05:00
mdipierro 56e6d682d6 Merge pull request #1702 from leonelcamara/i1699
Fix autocomplete called after user_signature chk
2017-08-01 09:46:00 -05:00
mdipierro c1881fa205 Merge pull request #1701 from jkotyz/issue/1700
Fixes 1700 - utc and local time inconsistency in AuthJWT
2017-08-01 09:45:46 -05:00
mdipierro cc2cae5de4 Merge pull request #1692 from timnyborg/master
Fix #1691: Passing tables to SQLFORM.factory() fails
2017-08-01 09:45:21 -05:00
mdipierro cedd74c1ad Merge pull request #1690 from josedesoto/issue/1689
del_membership prevent update self user if user_id has a value
2017-08-01 09:44:41 -05:00
Jose de Soto d5167f2ed6 change_password_url parameter for alternate login methods 2017-07-31 19:00:24 +02:00
Jose de Soto 1014d3e86e new parameter to auto create or not users with alternate login methods 2017-07-31 18:33:15 +02:00
Jose de Soto f3bba29287 Allow response.json to be sortable or not 2017-07-31 17:46:46 +02:00
mdipierro cd2ec98a26 grid.dbset 2017-07-29 01:31:25 -05:00
mdipierro 0e613f2d7f allow DAL(..., adapter_args=dict(migrator=InDBMigrator)) 2017-07-28 16:05:35 -05:00
Leonel Câmara 561828fa60 Fix autocomplete called after user_signature chk
Fixes #1699 which was caused by the autocomplete widget only being called after a user_signature check and the signature not being there.  

The default values for user_signature and hash_vars make this work with the grid when it has the user_signature=True option without any problem when that isn't the case. Users can disable it if they want or change it according to what they're verifying in their controllers.   
  
The autocomplete widget will still fail with the default kwargs in a situation where it is called in a controller function that first verifies the URL signature with hash_vars=True.  
  
For users having that problem, there's no way around this without inserting a security flaw, so they must sadly change their application code to give the autocomplete widget the needed user_signature and hash_vars argument.
2017-07-28 02:20:54 +01:00
Jan Kotyz 19efbfecfa Fixes 1700 2017-07-27 11:27:41 +02:00
Tim Nyborg d43604c3ff List comprehension rather than loop
I've now learned this is quicker
2017-07-24 10:15:12 +01:00
Tim Nyborg ec21f72ce3 Fixes #1691
Clone fields, but leave tables untouched in factory()
2017-07-21 10:20:08 +01:00
Jose de Soto aa0313c59b del_membership prevent update self user if user_id has a value 2017-07-21 10:41:58 +02:00
geomapdev 69e6e79e23 Update dal.py
updated to handle references without format
2017-05-01 08:39:31 -07:00
geomapdev 3292f760ca handle reference fields for keyed tables 2017-04-20 10:21:16 -07:00
58 changed files with 1017 additions and 539 deletions
+8 -1
View File
@@ -1,4 +1,11 @@
## 2.15.0b1
## 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 web shell
- experimental python 3 support
+10 -6
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/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py
echo "remember that pymysql was tweaked"
src:
### Use semantic versioning
echo 'Version 2.15.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files
make clean
rmfiles:
### clean up baisc apps
rm -f routes.py
rm -rf applications/*/sessions/*
@@ -46,6 +42,12 @@ src:
rm -rf applications/admin/uploads/*
rm -rf applications/welcome/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
cp applications/welcome/views/appadmin.html applications/admin/views
cp applications/welcome/views/appadmin.html applications/examples/views
@@ -54,7 +56,7 @@ src:
### build web2py_src.zip
echo '' > NEWINSTALL
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:
make src
@@ -97,6 +99,8 @@ win:
cp -r applications/welcome ../web2py_win/web2py/applications
cp -r applications/examples ../web2py_win/web2py/applications
cp applications/__init__.py ../web2py_win/web2py/applications
# per https://github.com/web2py/web2py/issues/1716
mv ../web2py_win/web2py/_ssl.pyd ../web2py_win/web2py/_ssl.pyd.legacy | echo 'done'
cd ../web2py_win; zip -r web2py_win.zip web2py
mv ../web2py_win/web2py_win.zip .
run:
+1 -1
View File
@@ -1 +1 @@
Version 2.15.2-stable+timestamp.2017.07.19.01.21.31
Version 2.16.1-stable+timestamp.2017.11.13.23.50.07
+24 -25
View File
@@ -657,37 +657,36 @@ def d3_graph_model():
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for database in databases:
db = eval_in_global_env(database)
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
File diff suppressed because one or more lines are too long
+42 -16
View File
@@ -12,6 +12,10 @@
$.error('web2py.js has already been loaded!');
}
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
var animateIn = 'fadeIn';
// var animateIn = 'slideDown';
String.prototype.reverse = function () {
return this.split('').reverse().join('');
};
@@ -178,7 +182,7 @@
},
/* manage errors in forms */
manage_errors: function (target) {
$('div.error', target).hide().slideDown('slow');
$('div.error', target).hide()[animateIn]('slow');
},
after_ajax: function (xhr) {
/* called whenever an ajax request completes */
@@ -263,13 +267,17 @@
}
});
/* help preventing double form submission for normal form (not LOADed) */
$(doc).on('submit', 'form', function () {
var submit_button = $(this).find(web2py.formInputClickSelector);
web2py.disableElement(submit_button);
$(doc).on('submit', 'form', function (e) {
var submit_buttons = $(this).find(web2py.formInputClickSelector);
submit_buttons.each(function() {
web2py.disableElement($(this));
})
/* safeguard in case the form doesn't trigger a refresh,
see https://github.com/web2py/web2py/issues/1100 */
setTimeout(function () {
web2py.enableElement(submit_button);
submit_buttons.each(function() {
web2py.enableElement($(this));
});
}, 5000);
});
doc.ajaxSuccess(function (e, xhr) {
@@ -320,7 +328,15 @@
form.submit(function (e) {
web2py.disableElement(form.find(web2py.formInputClickSelector));
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();
});
form.on('click', web2py.formInputClickSelector, function (e) {
@@ -339,11 +355,18 @@
if (web2py.isUndefined(element)) element = $(document);
/* if target is not there, fill it with something that there isn't in the page*/
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 */
$.ajax({
'type': method,
'url': action,
'data': data,
'processData': !isFormData,
'contentType': contentType,
'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target);
@@ -595,7 +618,7 @@
var flash = $('.w2p_flash');
web2py.hide_flash();
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 () {
$('.w2p_flash').fadeOut(0).html('');
@@ -609,7 +632,7 @@
for (var k = 0; k < triggers[id].length; k++) {
var dep = $('#' + triggers[id][k], 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();
}
};
@@ -699,8 +722,9 @@
});
},
/* Disables form elements:
- Does not disable elements with 'data-w2p_disable' attribute
- 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
*/
disableFormElements: function (form) {
@@ -712,13 +736,15 @@
if (!web2py.isUndefined(disable)) {
return false;
}
if (web2py.isUndefined(disable_with)) {
element.data('w2p_disable_with', element[method]());
if (!element.is(':file')) { // Altering file input values is not allowed.
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);
});
},
@@ -799,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/
/* compatibility code - end*/
/* compatibility code - end*/
+24 -25
View File
@@ -657,37 +657,36 @@ def d3_graph_model():
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for database in databases:
db = eval_in_global_env(database)
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
@@ -3,9 +3,9 @@
- **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.
- **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 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.
- **Talks to** SQLite, PostgreSQL, MySQL, MSSQL, FireBird, Oracle, IBM DB2, Informix, Ingres, and Google App Engine.
- **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, 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, 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.
- **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).
+42 -16
View File
@@ -12,6 +12,10 @@
$.error('web2py.js has already been loaded!');
}
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
var animateIn = 'fadeIn';
// var animateIn = 'slideDown';
String.prototype.reverse = function () {
return this.split('').reverse().join('');
};
@@ -178,7 +182,7 @@
},
/* manage errors in forms */
manage_errors: function (target) {
$('div.error', target).hide().slideDown('slow');
$('div.error', target).hide()[animateIn]('slow');
},
after_ajax: function (xhr) {
/* called whenever an ajax request completes */
@@ -263,13 +267,17 @@
}
});
/* help preventing double form submission for normal form (not LOADed) */
$(doc).on('submit', 'form', function () {
var submit_button = $(this).find(web2py.formInputClickSelector);
web2py.disableElement(submit_button);
$(doc).on('submit', 'form', function (e) {
var submit_buttons = $(this).find(web2py.formInputClickSelector);
submit_buttons.each(function() {
web2py.disableElement($(this));
})
/* safeguard in case the form doesn't trigger a refresh,
see https://github.com/web2py/web2py/issues/1100 */
setTimeout(function () {
web2py.enableElement(submit_button);
submit_buttons.each(function() {
web2py.enableElement($(this));
});
}, 5000);
});
doc.ajaxSuccess(function (e, xhr) {
@@ -320,7 +328,15 @@
form.submit(function (e) {
web2py.disableElement(form.find(web2py.formInputClickSelector));
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();
});
form.on('click', web2py.formInputClickSelector, function (e) {
@@ -339,11 +355,18 @@
if (web2py.isUndefined(element)) element = $(document);
/* if target is not there, fill it with something that there isn't in the page*/
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 */
$.ajax({
'type': method,
'url': action,
'data': data,
'processData': !isFormData,
'contentType': contentType,
'beforeSend': function (xhr, settings) {
xhr.setRequestHeader('web2py-component-location', document.location);
xhr.setRequestHeader('web2py-component-element', target);
@@ -595,7 +618,7 @@
var flash = $('.w2p_flash');
web2py.hide_flash();
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 () {
$('.w2p_flash').fadeOut(0).html('');
@@ -609,7 +632,7 @@
for (var k = 0; k < triggers[id].length; k++) {
var dep = $('#' + triggers[id][k], 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();
}
};
@@ -699,8 +722,9 @@
});
},
/* Disables form elements:
- Does not disable elements with 'data-w2p_disable' attribute
- 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
*/
disableFormElements: function (form) {
@@ -712,13 +736,15 @@
if (!web2py.isUndefined(disable)) {
return false;
}
if (web2py.isUndefined(disable_with)) {
element.data('w2p_disable_with', element[method]());
if (!element.is(':file')) { // Altering file input values is not allowed.
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);
});
},
@@ -799,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/
/* compatibility code - end*/
/* compatibility code - end*/
@@ -48,7 +48,7 @@
</tr>
<tr>
<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>
<a class="btn btn180 rounded" href="https://github.com/web2py/web2py/releases">Change Log</a>
+24 -25
View File
@@ -657,37 +657,36 @@ def d3_graph_model():
Create a list of table dicts, called "nodes"
"""
data = {}
nodes = []
links = []
subgraphs = dict()
for database in databases:
db = eval_in_global_env(database)
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
for tablename in db.tables:
fields = []
for field in db[tablename]:
f_type = field.type
if not isinstance(f_type,str):
disp = ' '
elif f_type == 'string':
disp = field.length
elif f_type == 'id':
disp = "PK"
elif f_type.startswith('reference') or \
f_type.startswith('list:reference'):
disp = "FK"
else:
disp = ' '
fields.append(dict(name= field.name, type=field.type, disp = disp))
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
if isinstance(f_type,str) and (
f_type.startswith('reference') or
f_type.startswith('list:reference')):
referenced_table = f_type.split()[1].split('.')[0]
links.append(dict(source=tablename, target = referenced_table))
links.append(dict(source=tablename, target = referenced_table))
nodes.append(dict(name=tablename, type="table", fields = fields))
nodes.append(dict(name=tablename, type="table", fields = fields))
# d3 v4 allows individual modules to be specified. The complete d3 library is included below.
response.files.append(URL('admin','static','js/d3.min.js'))
+23 -26
View File
@@ -1,26 +1,35 @@
# -*- coding: utf-8 -*-
# this file is released under public domain and you can use without limitations
# -------------------------------------------------------------------------
# This is a sample controller
# - index is the default action of any application
# - user is required for authentication and authorization
# - download is for downloading files uploaded in the db (does streaming)
# this file is released under public domain and you can use without limitations
# -------------------------------------------------------------------------
# ---- example index page ----
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")
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():
"""
exposes:
@@ -39,7 +48,7 @@ def user():
"""
return dict(form=auth())
# ---- action to server uploaded static content (required) ---
@cache.action()
def download():
"""
@@ -47,15 +56,3 @@ def download():
http://..../[app]/default/download/[filename]
"""
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 -*-
# -------------------------------------------------------------------------
# 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
# File is released under public domain and you can use without limitations
# -------------------------------------------------------------------------
if request.global_settings.web2py_version < "2.14.1":
raise HTTP(500, "Requires web2py 2.13.3 or newer")
if request.global_settings.web2py_version < "2.15.5":
raise HTTP(500, "Requires web2py 2.15.5 or newer")
# -------------------------------------------------------------------------
# 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()
# -------------------------------------------------------------------------
# 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
# -------------------------------------------------------------------------
myconf = AppConfig(reload=True)
configuration = AppConfig(reload=True)
if not request.env.web2py_runtime_gae:
# ---------------------------------------------------------------------
# if NOT running on Google App Engine use SQLite or other DB
# ---------------------------------------------------------------------
db = DAL(myconf.get('db.uri'),
pool_size=myconf.get('db.pool_size'),
migrate_enabled=myconf.get('db.migrate'),
db = DAL(configuration.get('db.uri'),
pool_size=configuration.get('db.pool_size'),
migrate_enabled=configuration.get('db.migrate'),
check_reserved=['all'])
else:
# ---------------------------------------------------------------------
@@ -52,12 +54,15 @@ else:
# by default give a view/generic.extension to all actions from localhost
# 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
# -------------------------------------------------------------------------
response.formstyle = myconf.get('forms.formstyle') # or 'bootstrap3_stacked' or 'bootstrap2' or other
response.form_label_separator = myconf.get('forms.separator') or ''
response.formstyle = 'bootstrap4_inline'
response.form_label_separator = ''
# -------------------------------------------------------------------------
# (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)
# -------------------------------------------------------------------------
from gluon.tools import Auth, Service, PluginManager
# host names must be a list of allowed host names (glob syntax allowed)
auth = Auth(db, host_names=myconf.get('host.names'))
service = Service()
plugins = PluginManager()
auth = Auth(db, host_names=configuration.get('host.names'))
# -------------------------------------------------------------------------
# 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)
# -------------------------------------------------------------------------
# configure email
# -------------------------------------------------------------------------
mail = auth.settings.mailer
mail.settings.server = 'logging' if request.is_local else myconf.get('smtp.server')
mail.settings.sender = myconf.get('smtp.sender')
mail.settings.login = myconf.get('smtp.login')
mail.settings.tls = myconf.get('smtp.tls') or False
mail.settings.ssl = myconf.get('smtp.ssl') or False
mail.settings.server = 'logging' if request.is_local else configuration.get('smtp.server')
mail.settings.sender = configuration.get('smtp.sender')
mail.settings.login = configuration.get('smtp.login')
mail.settings.tls = configuration.get('smtp.tls') or False
mail.settings.ssl = configuration.get('smtp.ssl') or False
# -------------------------------------------------------------------------
# configure auth policy
@@ -109,6 +111,27 @@ auth.settings.registration_requires_verification = False
auth.settings.registration_requires_approval = False
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
#
+14 -55
View File
@@ -1,29 +1,6 @@
# -*- coding: utf-8 -*-
# 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
# ----------------------------------------------------------------------------------------------------------------------
@@ -32,53 +9,42 @@ response.menu = [
(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 _():
# ------------------------------------------------------------------------------------------------------------------
# shortcuts
# ------------------------------------------------------------------------------------------------------------------
app = request.application
ctr = request.controller
# ------------------------------------------------------------------------------------------------------------------
# useful links to internal and external resources
# ------------------------------------------------------------------------------------------------------------------
if not configuration.get('app.production'):
_app = request.application
response.menu += [
(T('My Sites'), False, URL('admin', 'default', 'site')),
(T('This App'), False, '#', [
(T('Design'), False, URL('admin', 'default', 'design/%s' % app)),
LI(_class="divider"),
(T('Design'), False, URL('admin', 'default', 'design/%s' % _app)),
(T('Controller'), False,
URL(
'admin', 'default', 'edit/%s/controllers/%s.py' % (app, ctr))),
'admin', 'default', 'edit/%s/controllers/%s.py' % (_app, request.controller))),
(T('View'), False,
URL(
'admin', 'default', 'edit/%s/views/%s' % (app, response.view))),
'admin', 'default', 'edit/%s/views/%s' % (_app, response.view))),
(T('DB Model'), False,
URL(
'admin', 'default', 'edit/%s/models/db.py' % app)),
'admin', 'default', 'edit/%s/models/db.py' % _app)),
(T('Menu Model'), False,
URL(
'admin', 'default', 'edit/%s/models/menu.py' % app)),
'admin', 'default', 'edit/%s/models/menu.py' % _app)),
(T('Config.ini'), False,
URL(
'admin', 'default', 'edit/%s/private/appconfig.ini' % app)),
'admin', 'default', 'edit/%s/private/appconfig.ini' % _app)),
(T('Layout'), False,
URL(
'admin', 'default', 'edit/%s/views/layout.html' % app)),
'admin', 'default', 'edit/%s/views/layout.html' % _app)),
(T('Stylesheet'), False,
URL(
'admin', 'default', 'edit/%s/static/css/web2py-bootstrap3.css' % app)),
(T('Database'), False, URL(app, 'appadmin', 'index')),
'admin', 'default', 'edit/%s/static/css/web2py-bootstrap3.css' % _app)),
(T('Database'), False, URL(_app, 'appadmin', 'index')),
(T('Errors'), False, URL(
'admin', 'default', 'errors/' + app)),
'admin', 'default', 'errors/' + _app)),
(T('About'), False, URL(
'admin', 'default', 'about/' + app)),
'admin', 'default', 'about/' + _app)),
]),
('web2py.com', False, '#', [
(T('Download'), False,
@@ -98,7 +64,6 @@ def _():
]),
(T('Documentation'), False, '#', [
(T('Online book'), False, 'http://www.web2py.com/book'),
LI(_class="divider"),
(T('Preface'), False,
'http://www.web2py.com/book/default/chapter/00'),
(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
keywords = web2py, python, framework
generator = Web2py Web Framework
production = false
toolbar = false
; Host configuration
[host]
@@ -14,7 +16,6 @@ names = localhost:*, 127.0.0.1:*, *:*, *
[db]
uri = sqlite://storage.sqlite
migrate = true
; ignored for sqlite
pool_size = 10
; smtp address and credentials
@@ -25,7 +26,9 @@ login = username:password
tls = true
ssl = true
; form styling
[forms]
formstyle = bootstrap3_inline
separator =
[scheduler]
enabled = false
heartbeat = 1
[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;
}
body {
padding-top: 60px;
margin-bottom: 60px;
}
header {
@@ -126,8 +125,7 @@ html {
bottom: 0;
width: 100%;
height: 60px;
background: #333;
color: #aaa;
background: #f7f7f7;
}
header h1 {
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);
+14 -8
View File
@@ -13,6 +13,8 @@
}
var FORMDATA_IS_SUPPORTED = typeof(FormData) !== 'undefined';
var animateIn = 'fadeIn';
// var animateIn = 'slideDown';
String.prototype.reverse = function () {
return this.split('').reverse().join('');
@@ -180,7 +182,7 @@
},
/* manage errors in forms */
manage_errors: function (target) {
$('div.error', target).hide().slideDown('slow');
$('div.error', target).hide()[animateIn]('slow');
},
after_ajax: function (xhr) {
/* called whenever an ajax request completes */
@@ -265,13 +267,17 @@
}
});
/* help preventing double form submission for normal form (not LOADed) */
$(doc).on('submit', 'form', function () {
var submit_button = $(this).find(web2py.formInputClickSelector);
web2py.disableElement(submit_button);
$(doc).on('submit', 'form', function (e) {
var submit_buttons = $(this).find(web2py.formInputClickSelector);
submit_buttons.each(function() {
web2py.disableElement($(this));
})
/* safeguard in case the form doesn't trigger a refresh,
see https://github.com/web2py/web2py/issues/1100 */
setTimeout(function () {
web2py.enableElement(submit_button);
submit_buttons.each(function() {
web2py.enableElement($(this));
});
}, 5000);
});
doc.ajaxSuccess(function (e, xhr) {
@@ -612,7 +618,7 @@
var flash = $('.w2p_flash');
web2py.hide_flash();
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 () {
$('.w2p_flash').fadeOut(0).html('');
@@ -626,7 +632,7 @@
for (var k = 0; k < triggers[id].length; k++) {
var dep = $('#' + triggers[id][k], 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();
}
};
@@ -819,4 +825,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/
/* compatibility code - end*/
/* compatibility code - end*/
+17 -26
View File
@@ -1,15 +1,9 @@
{{left_sidebar_enabled,right_sidebar_enabled=False,('message' in globals())}}
{{extend 'layout.html'}}
{{block header}}
<header class="container-fluid background">
<div class="jumbotron text-center">
{{if response.title:}}
<h1>{{=response.title}}
<small>{{=response.subtitle or ''}}</small></h1>
{{pass}}
</div>
</header>
<center style="background-color: #333; color:white; padding:30px">
<h1>/{{=request.application}}/{{=request.controller}}/{{=request.function}}
</center>
{{end}}
{{if 'message' in globals():}}
@@ -27,26 +21,23 @@
_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>
</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():}}
{{=content}}
{{else:}}
{{=BEAUTIFY(response._vars)}}
{{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:}}
{{=BEAUTIFY(response._vars)}}
{{pass}}
{{if request.is_local:}}
{{=response.toolbar()}}
{{pass}}
+51 -54
View File
@@ -21,84 +21,81 @@
<meta name="google-site-verification" content="">
<!-- include stylesheets -->
<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="apple-touch-icon" href="{{=URL('static','images/favicon.png')}}">
<!-- All JavaScript at the bottom, except for Modernizr which enables
HTML5 elements & feature detects -->
<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 -->
{{include 'web2py_ajax.html'}} <!-- this includes jquery.js, calendar.js/.css and web2py.js -->
{{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>
<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>
<!-- Navbar ======================================= -->
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{{=response.logo or ''}}
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
{{='auth' in globals() and auth.navbar('Welcome',mode='dropdown') or ''}}
</ul>
{{if response.menu:}}
{{=MENU(response.menu, _class='nav navbar-nav',li_class='dropdown',ul_class='dropdown-menu')}}
<nav class="navbar navbar-toggleable-md navbar-light bg-faded">
<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">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="http://web2py.com">web2py</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav">
{{for _item in response.menu or []:}}
{{if len(_item)<4 or not _item[3]:}}
<li class="nav-item {{if _item[1]:}}active{{pass}}">
<a class="nav-link" href="{{=_item[2]}}">{{=_item[0]}}</a>
</li>
{{else:}}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="{{=_item[2]}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{=_item[0]}}</a>
<div class="dropdown-menu">
{{for _subitem in _item[3]:}}
<a class="dropdown-item" href="{{=_subitem[2]}}">{{=_subitem[0]}}</a>
{{pass}}
</div>
</li>
{{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>
</nav>
<!-- Masthead ===================================== -->
{{block header}}
{{end}}
<!-- Main ========================================= -->
<!-- Begin page content -->
<div class="container-fluid main-container">
{{if left_sidebar_enabled:}}
<div class="col-md-3 left-sidebar">
{{block left_sidebar}}
<h3>Left Sidebar</h3>
<p></p>
{{end}}
</div>
{{pass}}
<div class="{{=middle_column}}">
<div class="col-md-12">
{{block center}}
{{include}}
{{end}}
{{=response.toolbar() if response.show_toolbar else ''}}
</div>
{{if right_sidebar_enabled:}}
<div class="col-md-3">
{{block right_sidebar}}
<h3>Right Sidebar</h3>
<p></p>
{{end}}
</div>
{{pass}}
</div>
{{block footer}} <!-- this is default footer -->
@@ -114,7 +111,7 @@
{{end}}
<!-- The javascript =============================== -->
<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}}
{{if response.google_analytics_id:}}
<!-- Analytics ==================================== -->
+5 -3
View File
@@ -35,6 +35,7 @@ if PY2:
from gluon.contrib import ipaddress
BytesIO = StringIO
reduce = reduce
reload = reload
hashlib_md5 = hashlib.md5
iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues()
@@ -63,7 +64,7 @@ if PY2:
return None
if isinstance(obj, (bytes, bytearray, buffer)):
return bytes(obj)
if isinstance(obj, unicode):
if hasattr(obj, 'encode'):
return obj.encode(charset, errors)
raise TypeError('Expected bytes')
@@ -77,6 +78,7 @@ else:
import pickle
from io import StringIO, BytesIO
import copyreg
from importlib import reload
from functools import reduce
from html.parser import HTMLParser
from http import cookies as Cookie
@@ -122,7 +124,7 @@ else:
return None
if isinstance(obj, (bytes, bytearray, memoryview)):
return bytes(obj)
if isinstance(obj, str):
if hasattr(obj, 'encode'):
return obj.encode(charset, errors)
raise TypeError('Expected bytes')
@@ -151,7 +153,7 @@ def with_metaclass(meta, *bases):
def to_unicode(obj, charset='utf-8', errors='strict'):
if obj is None:
return None
if not isinstance(obj, bytes):
if not hasattr(obj, 'decode'):
return text_type(obj)
return obj.decode(charset, errors)
+35 -18
View File
@@ -20,10 +20,10 @@ DEFAULT = lambda: None
class AuthAPI(object):
"""
AuthAPI is a barebones Auth implementation which does not have a concept of
HTML forms or redirects, emailing or even an URL, you are responsible for
HTML forms or redirects, emailing or even an URL, you are responsible for
all that if you use it.
The main Auth functions such as login, logout, register, profile are designed
in a Dict In -> Dict Out logic so, for instance, if you set
in a Dict In -> Dict Out logic so, for instance, if you set
registration_requires_verification you are responsible for sending the key to
the user and even rolling back the transaction if you can't do it.
@@ -245,13 +245,13 @@ class AuthAPI(object):
migrate = db._migrate
if fake_migrate is None:
fake_migrate = db._fake_migrate
settings = self.settings
if username is None:
username = settings.use_username
else:
settings.use_username = username
if not self.signature:
self.define_signature()
if signature is True:
@@ -557,27 +557,43 @@ class AuthAPI(object):
self.log_event(self.messages['del_membership_log'],
dict(user_id=user_id, group_id=group_id))
ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete()
if group_id in self.user_groups:
if group_id in self.user_groups and user_id == self.user_id:
del self.user_groups[group_id]
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
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:
user_id = self.user.id
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
if cached:
id_role = group_id or role
r = (user_id and id_role in self.user_groups.values()) or (user_id and id_role in self.user_groups)
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'],
dict(user_id=user_id, group_id=group_id, check=r))
return r
@@ -972,7 +988,8 @@ class AuthAPI(object):
requires = [requires]
requires = list(filter(lambda t: isinstance(t, CRYPT), 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', '')
new_password = kwargs.get('new_password', '')
@@ -1012,7 +1029,7 @@ class AuthAPI(object):
):
"""
Verify a given registration_key actually exists in the user table.
Resets the key to empty string '' or 'pending' if
Resets the key to empty string '' or 'pending' if
setttings.registration_requires_approval is true.
Keyword Args:
+18 -16
View File
@@ -18,7 +18,7 @@ import fnmatch
import os
import copy
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.template import parse_template
from gluon.restricted import restricted, compile2
@@ -205,7 +205,7 @@ def LOAD(c=None, f='index', args=None, vars=None,
other_response = Response()
other_request.env.path_info = '/' + \
'/'.join([request.application, c, f] +
map(str, other_request.args))
[str(a) for a in other_request.args])
other_request.env.query_string = \
vars and URL(vars=vars).split('?')[1] or ''
other_request.env.http_web2py_component_location = \
@@ -288,7 +288,7 @@ class LoadFactory(object):
other_response = globals.Response()
other_request.env.path_info = '/' + \
'/'.join([request.application, c, f] +
map(str, other_request.args))
[str(a) for a in other_request.args])
other_request.env.query_string = \
vars and html.URL(vars=vars).split('?')[1] or ''
other_request.env.http_web2py_component_location = \
@@ -676,8 +676,9 @@ def run_view_in(environment):
badv = 'invalid view (%s)' % view
patterns = response.get('generic_patterns')
layer = None
scode = None
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
allow_generic = regex.search(short_action)
else:
@@ -708,22 +709,23 @@ def run_view_in(environment):
ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled))
layer = compiled
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 not layer:
# Compile the template
ccode = parse_template(view,
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)
# Parse template
scode = parse_template(view,
pjoin(folder, 'views'),
context=environment)
layer = filename
restricted(ccode, environment, layer=layer)
# Compile template
ccode = compile2(scode, filename)
layer = filename
restricted(ccode, environment, layer=layer, scode=scode)
# parse_template saves everything in response body
return environment['response'].body.getvalue()
+12 -1
View File
@@ -49,7 +49,8 @@ class CasAuth(object):
email=lambda v: v.get('email', None),
user_id=lambda v: v['user']),
casversion=1,
casusername='cas:user'
casusername='cas:user',
change_password_url=None
):
self.urlbase = urlbase
self.cas_login_url = "%s/%s" % (self.urlbase, actions[0])
@@ -64,6 +65,9 @@ class CasAuth(object):
#vars=current.request.vars,
scheme=True)
# URL to let users change their password in the IDP system
self.cas_change_password_url = change_password_url
def login_url(self, next="/"):
current.session.token = self._CAS_login()
return next
@@ -74,6 +78,10 @@ class CasAuth(object):
self._CAS_logout()
return next
def change_password_url(self, next="/"):
self._CAS_change_password()
return next
def get_user(self):
user = current.session.token
if user:
@@ -135,3 +143,6 @@ class CasAuth(object):
redirects to the CAS logout page
"""
redirect("%s?service=%s" % (self.cas_logout_url, self.cas_my_url))
def _CAS_change_password(self):
redirect(self.cas_change_password_url)
+23 -1
View File
@@ -145,10 +145,16 @@ class Saml2Auth(object):
username=lambda v:v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
email=lambda v:v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
user_id=lambda v:v['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'][0],
)):
), logout_url=None, change_password_url=None):
self.config_file = config_file
self.maps = maps
# URL for redirecting users to when they sign out
self.saml_logout_url = logout_url
# URL to let users change their password in the IDP system
self.saml_change_password_url = change_password_url
def login_url(self, next="/"):
d = saml2_handler(current.session, current.request)
if 'url' in d:
@@ -170,6 +176,12 @@ class Saml2Auth(object):
def logout_url(self, next="/"):
current.session.saml2_info = None
current.session.auth = None
self._SAML_logout()
return next
def change_password_url(self, next="/"):
self._SAML_change_password()
return next
def get_user(self):
@@ -180,3 +192,13 @@ class Saml2Auth(object):
d[key] = self.maps[key](user)
return d
return None
def _SAML_logout(self):
"""
exposed SAML.logout()
redirects to the SAML logout page
"""
redirect(self.saml_logout_url)
def _SAML_change_password(self):
redirect(self.saml_change_password_url)
+2
View File
@@ -137,6 +137,8 @@ def populate_generator(table, default=True, compute=False, contents={}):
continue
elif field.type == 'upload':
continue
elif field.compute is not None:
continue
elif default and not field.default in (None, ''):
record[fieldname] = field.default
elif compute and field.compute:
+20
View File
@@ -673,3 +673,23 @@ def simple_detect(agent):
if os_version:
os = " ".join((os, os_version))
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()
+3 -25
View File
@@ -94,32 +94,10 @@ import optparse
import time
import sys
import gluon.utils
if (sys.version_info[0] == 2):
from urllib import urlencode, urlopen
def to_bytes(obj, charset='utf-8', errors='strict'):
if obj is None:
return None
if isinstance(obj, (bytes, bytearray, buffer)):
return bytes(obj)
if isinstance(obj, unicode):
return obj.encode(charset, errors)
raise TypeError('Expected bytes')
else:
from urllib.request import urlopen
from urllib.parse import urlencode
def to_bytes(obj, charset='utf-8', errors='strict'):
if obj is None:
return None
if isinstance(obj, (bytes, bytearray, memoryview)):
return bytes(obj)
if isinstance(obj, str):
return obj.encode(charset, errors)
raise TypeError('Expected bytes')
from gluon._compat import to_native, to_bytes, urlencode, urlopen
listeners, names, tokens = {}, {}, {}
def websocket_send(url, message, hmac_key=None, group='default'):
sig = hmac_key and hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() or ''
params = urlencode(
@@ -138,8 +116,8 @@ class PostHandler(tornado.web.RequestHandler):
if hmac_key and not 'signature' in self.request.arguments:
self.send_error(401)
if 'message' in self.request.arguments:
message = self.request.arguments['message'][0]
group = self.request.arguments.get('group', ['default'])[0]
message = self.request.arguments['message'][0].decode(encoding='UTF-8')
group = self.request.arguments.get('group', ['default'])[0].decode(encoding='UTF-8')
print('%s:MESSAGE to %s:%s' % (time.time(), group, message))
if hmac_key:
signature = self.request.arguments['signature'][0]
+1 -1
View File
@@ -8,7 +8,7 @@
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 sys
import threading
+56 -31
View File
@@ -14,6 +14,7 @@ from pydal import DAL as DAL
from pydal import Field
from pydal.objects import Row, Rows, Table, Query, Set, Expression
from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon
from pydal.migrator import Migrator, InDBMigrator
from gluon.serializers import custom_json, xml
from gluon.utils import web2py_uuid
from gluon import sqlhtml
@@ -31,7 +32,11 @@ def _default_validators(db, field):
field_type, field_length = field.type, field.length
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))
elif field_type == 'json':
requires.append(validators.IS_EMPTY_OR(validators.IS_JSON()))
@@ -49,42 +54,62 @@ def _default_validators(db, field):
requires.append(validators.IS_TIME())
elif field_type == 'datetime':
requires.append(validators.IS_DATETIME())
elif db and field_type.startswith('reference') and \
field_type.find('.') < 0 and \
field_type[10:] in db.tables:
referenced = db[field_type[10:]]
if hasattr(referenced, '_format') and referenced._format:
requires = validators.IS_IN_DB(db, referenced._id,
referenced._format)
if field.unique:
requires._and = validators.IS_NOT_IN_DB(db, field)
if field.tablename == field_type[10:]:
return validators.IS_EMPTY_OR(requires)
return requires
elif db and field_type.startswith('list:reference') and \
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 db and field_type.startswith('reference'):
if field_type.find('.') < 0 and field_type[10:] in db.tables:
referenced = db[field_type[10:]]
if hasattr(referenced, '_format') and referenced._format:
requires = validators.IS_IN_DB(db, referenced._id,referenced._format)
else:
requires = validators.IS_IN_DB(db, referenced._id)
elif field_type.find('.') > 0 and field_type[10:].split('.')[0] in db.tables:
table_field = field_type[10:].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)
else:
requires = validators.IS_IN_DB(db, referenced[field_name])
if field.unique:
requires._and = validators.IS_NOT_IN_DB(db, field)
if not field.notnull:
requires = validators.IS_EMPTY_OR(requires)
return requires
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:
requires._and = validators.IS_NOT_IN_DB(db, field)
if not field.notnull:
requires = validators.IS_EMPTY_OR(requires)
return requires
# does not get here for reference and list:reference
if field.unique:
requires.insert(0, validators.IS_NOT_IN_DB(db, field))
excluded_fields = ['string', 'upload', 'text', 'password', 'boolean']
if (field.notnull or field.unique) and field_type not in excluded_fields:
requires.insert(0, validators.IS_NOT_EMPTY())
elif not field.notnull and not field.unique and requires:
requires[0] = \
validators.IS_EMPTY_OR(requires[0], null='' if field.type in ('string', 'text', 'password') else None)
if isinstance(requires, list):
if field.unique:
requires.insert(0, validators.IS_NOT_IN_DB(db, field))
excluded_fields = ['string', 'upload', 'text', 'password', 'boolean']
if (field.notnull or field.unique) and field_type not in excluded_fields:
requires.insert(0, validators.IS_NOT_EMPTY())
elif not field.notnull and not field.unique and requires:
null = null='' if field.type in ('string', 'text', 'password') else None
requires[0] = validators.IS_EMPTY_OR(requires[0], null=null)
return requires
DAL.serializers = {'json': custom_json, 'xml': xml}
+79 -35
View File
@@ -14,7 +14,7 @@ Contains the classes for the global used variables:
"""
from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \
unicodeT, long, hashlib_md5
unicodeT, long, hashlib_md5, urllib_quote
from gluon.storage import Storage, List
from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
from gluon.contenttype import contenttype
@@ -220,7 +220,12 @@ class Request(Storage):
if is_json:
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:
# incoherent request bodies can still be parsed "ad-hoc"
json_vars = {}
@@ -331,11 +336,16 @@ class Request(Storage):
user_agent = session._user_agent
if 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():
if isinstance(value, dict):
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
def requires_https(self):
@@ -468,45 +478,67 @@ class Response(Storage):
response.cache_includes = (cache_method, time_expire).
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 = []
ext_files = []
has_js = has_css = False
# For the adjacent list we're going to use storage List to both distinguish
# from the regular list and so we can add attributes
internal = List()
internal.has_js = False
internal.has_css = False
done = set() # to remove duplicates
for item in self.files:
if isinstance(item, (list, tuple)):
ext_files.append(item)
if not isinstance(item, list):
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
if extensions and not item.rpartition('.')[2] in extensions:
continue
if item in files:
continue
internal.append(item)
if item.endswith('.js'):
has_js = True
internal.has_js = True
if item.endswith('.css'):
has_css = True
files.append(item)
internal.has_css = True
if internal:
files.append(internal)
if have_minify and ((self.optimize_css and has_css) or (self.optimize_js and has_js)):
# cache for 5 minutes by default
key = hashlib_md5(repr(files)).hexdigest()
cache = self.cache_includes or (current.cache.ram, 60 * 5)
def call_minify(files=files):
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:
# We're done we can now minify
if have_minify:
for i, f in enumerate(files):
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 static_map(s, item):
if isinstance(item, str):
f = item.lower().split('?')[0]
ext = f.rpartition('.')[2]
@@ -526,6 +558,13 @@ class Response(Storage):
if tmpl:
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)
def stream(self,
@@ -641,6 +680,11 @@ class Response(Storage):
if download_filename is None:
download_filename = filename
if attachment:
# Browsers still don't have a simple uniform way to have non ascii
# characters in the filename so for now we are percent encoding it
if isinstance(download_filename, unicodeT):
download_filename = download_filename.encode('utf-8')
download_filename = urllib_quote(download_filename)
headers['Content-Disposition'] = \
'attachment; filename="%s"' % download_filename.replace('"', '\"')
return self.stream(stream, chunk_size=chunk_size, request=request)
+1 -1
View File
@@ -2431,7 +2431,7 @@ class BEAUTIFY(DIV):
if level == 0:
return
for c in self.components:
if hasattr(c, 'value') and not callable(c.value):
if hasattr(c, 'value') and not callable(c.value) and not isinstance(c, cgi.FieldStorage):
if c.value:
components.append(c.value)
if hasattr(c, 'xml') and callable(c.xml):
+8 -3
View File
@@ -11,7 +11,7 @@ HTTP statuses helpers
"""
import re
from gluon._compat import iteritems, unicodeT
from gluon._compat import iteritems, unicodeT, to_bytes
__all__ = ['HTTP', 'redirect']
@@ -111,6 +111,8 @@ class HTTP(Exception):
if not body:
body = status
if isinstance(body, (str, bytes, bytearray)):
if isinstance(body, unicodeT):
body = to_bytes(body) # This must be done before len
headers['Content-Length'] = len(body)
rheaders = []
for k, v in iteritems(headers):
@@ -123,12 +125,15 @@ class HTTP(Exception):
return ['']
elif isinstance(body, (str, bytes, bytearray)):
if isinstance(body, unicodeT):
body = body.encode('utf-8')
body = to_bytes(body)
return [body]
elif hasattr(body, '__iter__'):
return body
else:
return [str(body)]
body = str(body)
if isinstance(body, unicodeT):
body = to_bytes(body)
return [body]
@property
def message(self):
+9 -4
View File
@@ -18,10 +18,11 @@ import pkgutil
import logging
from cgi import escape
from threading import RLock
from gluon.utf8 import Utf8
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 pydal.contrib.portalocker import read_locked, LockedFile
from gluon.fileutils import listdir
@@ -49,8 +50,10 @@ DEFAULT_CONSTRUCT_PLURAL_FORM = lambda word, plural_id: word
if PY2:
NUMBERS = (int, long, float)
from gluon.utf8 import Utf8
else:
NUMBERS = (int, float)
Utf8 = str
# pattern to find T(blah blah blah) expressions
PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\
@@ -107,15 +110,17 @@ def markmin(s):
def upper_fun(s):
return unicode(s, 'utf-8').upper().encode('utf-8')
return to_unicode(s).upper()
def title_fun(s):
return unicode(s, 'utf-8').title().encode('utf-8')
return to_unicode(s).title()
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_out = maketrans('\x1c\x1d\x1e\x1f', "\\%{}")
+2 -2
View File
@@ -231,7 +231,7 @@ class LazyWSGI(object):
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)
return lambda *args, **kargs: \
self.response.write(escape=False, *args, **kargs)
@@ -745,7 +745,7 @@ class HttpServer(object):
sock_list = [ip, port]
if not ssl_certificate or not ssl_private_key:
logger.info('SSL is off')
elif not rocket.ssl:
elif not rocket.has_ssl:
logger.warning('Python "ssl" module unavailable. SSL is OFF')
elif not exists(ssl_certificate):
logger.warning('unable to open SSL certificate. SSL is OFF')
+1 -1
View File
@@ -225,7 +225,7 @@ def parsecronline(line):
params = line.strip().split(None, 6)
if len(params) < 7:
return None
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3,
daysofweek = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3,
'thu': 4, 'fri': 5, 'sat': 6}
for (s, id) in zip(params[:5], ['min', 'hr', 'dom', 'mon', 'dow']):
if not s in [None, '*']:
+8 -6
View File
@@ -10,7 +10,7 @@ Restricted environment to execute application's code
"""
import sys
from gluon._compat import pickle, ClassType
from gluon._compat import pickle, ClassType, unicodeT, to_bytes
import traceback
import types
import os
@@ -192,10 +192,10 @@ class RestrictedError(Exception):
# safely show an useful message to the user
try:
output = self.output
if isinstance(output, unicode):
output = output.encode("utf8")
elif not isinstance(output, str):
if not isinstance(output, str, bytes, bytearray):
output = str(output)
if isinstance(output, unicodeT):
output = to_bytes(output)
except:
output = ""
return output
@@ -205,7 +205,7 @@ def compile2(code, layer):
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
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)
del tb
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):
+1 -1
View File
@@ -145,7 +145,7 @@ class XssCleaner(HTMLParser):
if url.startswith('#'):
return True
else:
parsed = urlparse(url)
parsed = urlparse.urlparse(url)
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] == '' and parsed[2].startswith('/')))
+2 -2
View File
@@ -119,8 +119,8 @@ def xml(value, encoding='UTF-8', key='document', quote=True):
return ('<?xml version="1.0" encoding="%s"?>' % encoding) + str(xml_rec(value, key, quote))
def json(value, default=custom_json, indent=None):
value = json_parser.dumps(value, default=default, sort_keys=True, indent=indent)
def json(value, default=custom_json, indent=None, sort_keys=False):
value = json_parser.dumps(value, default=default, sort_keys=sort_keys, indent=indent)
# replace JavaScript incompatible spacing
# http://timelessrepo.com/json-isnt-a-javascript-subset
# PY3 FIXME
+7 -1
View File
@@ -31,10 +31,16 @@ from gluon.globals import Request, Response, Session
from gluon.storage import Storage, List
from gluon.admin import w2p_unpack
from pydal.base import BaseAdapter
from gluon._compat import iteritems, ClassType
from gluon._compat import iteritems, ClassType, PY2
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):
try:
+130 -17
View File
@@ -17,6 +17,7 @@ Holds:
import datetime
import urllib
import re
import copy
import os
from gluon._compat import StringIO, unichr, urllib_quote, iteritems, basestring, long, unicodeT, to_native, to_unicode
@@ -658,7 +659,8 @@ class AutocompleteWidget(object):
orderby=None, limitby=(0, 10), distinct=False,
keyword='_autocomplete_%(tablename)s_%(fieldname)s',
min_length=2, help_fields=None, help_string=None,
at_beginning=True, default_var='ac'):
at_beginning=True, default_var='ac', user_signature=True,
hash_vars=False):
self.help_fields = help_fields or []
self.help_string = help_string
@@ -681,9 +683,10 @@ class AutocompleteWidget(object):
else:
self.is_reference = False
if hasattr(request, 'application'):
urlvars = request.vars
urlvars = copy.copy(request.vars)
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)
self.run_callback = True
else:
self.url = request
@@ -1085,6 +1088,106 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
return parent
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):
@@ -1173,6 +1276,8 @@ class SQLFORM(FORM):
bootstrap=formstyle_bootstrap,
bootstrap3_stacked=formstyle_bootstrap3_stacked,
bootstrap3_inline=formstyle_bootstrap3_inline_factory(3),
bootstrap4_stacked=formstyle_bootstrap4_stacked,
bootstrap4_inline=formstyle_bootstrap4_inline_factory(3),
inline=formstyle_inline,
)
@@ -1241,8 +1346,8 @@ class SQLFORM(FORM):
# will only use writable or readable fields, unless forced to ignore
if fields is None:
fields = [f.name for f in table if
(ignore_rw or f.writable or f.readable) and
(readonly or not f.compute)]
(ignore_rw or f.writable or f.readable) and
not (f.compute and not record)]
self.fields = fields
# make sure we have an id
@@ -1325,7 +1430,7 @@ class SQLFORM(FORM):
label = LABEL(label, label and sep, _for=field_id,
_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)
if cond:
@@ -1919,8 +2024,10 @@ class SQLFORM(FORM):
if 'table_name' in attributes:
del attributes['table_name']
return SQLFORM(DAL(None).define_table(table_name, *[field.clone() for field in fields]),
**attributes)
# Clone fields, while passing tables straight through
fields_with_clones = [f.clone() if isinstance(f, Field) else f for f in fields]
return SQLFORM(DAL(None).define_table(table_name, *fields_with_clones), **attributes)
@staticmethod
def build_query(fields, keywords):
@@ -1935,11 +2042,13 @@ class SQLFORM(FORM):
if settings.global_settings.web2py_runtime_gae:
return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields])
else:
if not (sfields and key and key.split()):
return fields[0].table
return reduce(lambda a,b:a&b,[
reduce(lambda a,b: a|b, [
field.contains(k) for field in sfields]
) for k in key.split()])
# from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4
# needs testing...
#words = key.split(' ') if key else []
@@ -2159,6 +2268,7 @@ class SQLFORM(FORM):
represent_none=None,
showblobs=False):
dbset = None
formstyle = formstyle or current.response.formstyle
if isinstance(query, Set):
query = query.query
@@ -2340,12 +2450,12 @@ class SQLFORM(FORM):
for k, f in iteritems(table):
if isinstance(f, Field.Virtual):
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:
fields = []
columns = []
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:
fields += filter(filter1, table)
columns += filter(filter2, table)
@@ -2563,8 +2673,8 @@ class SQLFORM(FORM):
try:
# the query should be constructed using searchable
# fields but not virtual fields
sfields = reduce(lambda a, b: a + b,
[[f for f in t if f.readable and not isinstance(f, Field.Virtual)] for t in tables])
is_searchable = lambda f: f.readable and not isinstance(f, Field.Virtual) and f.searchable
sfields = reduce(lambda a, b: a + b, [filter(is_searchable, t) for t in tables])
# use custom_query using searchable
if callable(searchable):
dbset = dbset(searchable(sfields, keywords))
@@ -3037,6 +3147,7 @@ class SQLFORM(FORM):
res.view_form = view_form
res.search_form = search_form
res.rows = rows
res.dbset = dbset
return res
@staticmethod
@@ -3155,8 +3266,8 @@ class SQLFORM(FORM):
# if isinstance(linked_tables, dict):
# linked_tables = linked_tables.get(table._tablename, [])
if linked_tables is None or referee in linked_tables:
field.represent = (lambda id, r=None, referee=referee, rep=field.represent:
A(callable(rep) and rep(id) or id,
field.represent = (lambda id, r=None, referee=referee, rep=field.represent:
A(callable(rep) and rep(id) or id,
cid=request.cid, _href=url(args=['view', referee, id])))
except (KeyError, ValueError, TypeError):
redirect(URL(args=table._tablename))
@@ -3319,7 +3430,7 @@ class SQLTABLE(TABLE):
return
REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD
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():
pref = table._tablename + '.'
fieldmap.update(((pref+f.name, f) for f in table._virtual_fields))
@@ -3562,7 +3673,9 @@ class ExportClass(object):
if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col):
row.append(record._extra[col])
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]
if isinstance(record.get(t, None), (Row, dict)):
value = record[t][f]
+3
View File
@@ -138,6 +138,9 @@ class TestAuthAPI(unittest.TestCase):
self.assertTrue('new_password2' in result['errors'])
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
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):
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'))
content = return_includes(response)
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>' +
'<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="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>'
'<script src="/a/static/css/file.ts" type="text/typescript"></script>'
)
response = Response()
+4 -4
View File
@@ -355,10 +355,10 @@ class TestSQLTABLE(unittest.TestCase):
self.db.commit()
# def test_SQLTABLE(self):
# rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL)
# 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>')
def test_SQLTABLE(self):
rows = self.db(self.db.auth_user.id > 0).select(self.db.auth_user.ALL)
sqltable = SQLTABLE(rows)
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):
+62
View File
@@ -278,8 +278,68 @@ class TestValidators(unittest.TestCase):
self.assertEqual(rtn, ('jerry', 'oops'))
rtn = IS_IN_DB(db, 'person.id', '%(name)s', auto_add=True)('jerry')
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.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):
from gluon.dal import DAL, Field
@@ -444,6 +504,8 @@ class TestValidators(unittest.TestCase):
self.assertEqual(rtn, (None, 'Enter a value'))
rtn = IS_NOT_EMPTY()('')
self.assertEqual(rtn, ('', 'Enter a value'))
rtn = IS_NOT_EMPTY()(b'')
self.assertEqual(rtn, (b'', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' ')
self.assertEqual(rtn, (' ', 'Enter a value'))
rtn = IS_NOT_EMPTY()(' \n\t')
+2 -2
View File
@@ -102,12 +102,12 @@ class TestWeb(LiveTest):
password='test',
_formname='login')
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
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.post('index', data=dict(password='testpass'))
+23 -5
View File
@@ -1320,7 +1320,7 @@ class AuthJWT(object):
# is the following safe or should we use
# calendar.timegm(datetime.datetime.utcnow().timetuple())
# result seem to be the same (seconds since epoch, in UTC)
now = time.mktime(datetime.datetime.now().timetuple())
now = time.mktime(datetime.datetime.utcnow().timetuple())
expires = now + self.expiration
payload = dict(
hmac_key=session_auth['hmac_key'],
@@ -1332,7 +1332,7 @@ class AuthJWT(object):
return payload
def refresh_token(self, orig_payload):
now = time.mktime(datetime.datetime.now().timetuple())
now = time.mktime(datetime.datetime.utcnow().timetuple())
if self.verify_expiration:
orig_exp = orig_payload['exp']
if orig_exp + self.leeway < now:
@@ -1792,6 +1792,7 @@ class Auth(AuthAPI):
servicevalidate='serviceValidate',
proxyvalidate='proxyValidate',
logout='logout'),
cas_create_user=True,
extra_fields={},
actions_disabled=[],
controller=controller,
@@ -2284,6 +2285,7 @@ class Auth(AuthAPI):
If the user doesn't yet exist, then they are created.
"""
table_user = self.table_user()
create_user = self.settings.cas_create_user
user = None
checks = []
# make a guess about who this user is
@@ -2316,6 +2318,11 @@ class Auth(AuthAPI):
update_keys[key] = keys[key]
user.update_record(**update_keys)
elif checks:
if create_user is False:
# Remove current open session a send message
self.logout(next=None, onlogout=None, log=None)
raise HTTP(403, "Forbidden. User need to be created first.")
if 'first_name' not in keys and 'first_name' in table_user.fields:
guess = keys.get('email', 'anonymous').split('@')[0]
keys['first_name'] = keys.get('username', guess)
@@ -2863,7 +2870,7 @@ class Auth(AuthAPI):
auth.settings.auth_two_factor_enabled = True
auth.messages.two_factor_comment = "Verify your OTP Client for the code."
auth.settings.two_factor_methods = [lambda user,
auth.settings.two_factor_methods = [lambda user,
auth_two_factor: _set_two_factor(user, auth_two_factor)]
auth.settings.two_factor_onvalidation = [lambda user, otp: verify_otp(user, otp)]
@@ -3656,6 +3663,16 @@ class Auth(AuthAPI):
if not self.is_logged_in():
redirect(self.settings.login_url,
client_side=self.settings.client_side)
# Go to external link to change the password
if self.settings.login_form != self:
cas = self.settings.login_form
# To prevent error if change_password_url function is not defined in alternate login
if hasattr(cas, 'change_password_url'):
next = cas.change_password_url(next)
if next is not None:
redirect(next)
db = self.db
table_user = self.table_user()
s = db(table_user.id == self.user.id)
@@ -3676,7 +3693,8 @@ class Auth(AuthAPI):
requires = [requires]
requires = list(filter(lambda t: isinstance(t, CRYPT), 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(
Field('old_password', 'password', requires=requires,
label=self.messages.old_password),
@@ -3760,9 +3778,9 @@ class Auth(AuthAPI):
if any(f.compute for f in extra_fields):
user = table_user[self.user.id]
self._update_session_user(user)
self.update_groups()
else:
self.user.update(table_user._filter_fields(form.vars))
session.flash = self.messages.profile_updated
self.log_event(log, self.user)
callback(onaccept, form)
+1 -1
View File
@@ -996,7 +996,7 @@ def is_empty(value, empty_regex=None):
value = value.strip()
if empty_regex is not None and empty_regex.match(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, False)
+6 -3
View File
@@ -140,7 +140,7 @@ class web2pyDialog(object):
else:
import tkinter
from tkinter import messagebox
bg_color = 'white'
root.withdraw()
@@ -463,7 +463,7 @@ class web2pyDialog(object):
import tkMessageBox as messagebox
else:
from tkinter import messagebox
messagebox.showerror('web2py start server', message)
def start(self):
@@ -1076,7 +1076,10 @@ def start_schedulers(options):
return
# Work around OS X problem: http://bugs.python.org/issue9405
import urllib
if PY2:
import urllib
else:
import urllib.request as urllib
urllib.getproxies()
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
sudo /etc/init.d/nginx stop
sudo systemctl start emperor.uwsgi.service
sudo systemctl stop emperor.uwsgi.service
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)