Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9fd0fd71e | ||
|
|
d59e7f35ed | ||
|
|
999e7b7512 | ||
|
|
7035398681 | ||
|
|
2c2c9d3aa2 | ||
|
|
65b3e6dda9 | ||
|
|
d424e5d317 | ||
|
|
a1417df67c | ||
|
|
551c19bcaf | ||
|
|
8aa07760d2 | ||
|
|
8bd8db5edc | ||
|
|
7c1fb6643e | ||
|
|
e44a12eaf8 | ||
|
|
12253ab757 | ||
|
|
30b3d84f24 | ||
|
|
166e268308 | ||
|
|
14e58276cf | ||
|
|
eb7222aa92 | ||
|
|
009a0b87f2 | ||
|
|
aeb0244b2c | ||
|
|
57522ddeb1 | ||
|
|
934042fc04 | ||
|
|
4bfa9c1686 | ||
|
|
925f928843 | ||
|
|
2f5eb409b6 | ||
|
|
228d3c41b6 | ||
|
|
3ab91b9fe9 | ||
|
|
5db3b21437 | ||
|
|
2aa8fd22a2 | ||
|
|
d5eb8d8f17 | ||
|
|
c4e08eeeb3 | ||
|
|
c1a3d5d67e | ||
|
|
b2841de6f3 | ||
|
|
c7d415fefd | ||
|
|
2e572aee9a | ||
|
|
dd14d1c8c9 | ||
|
|
afa5fac074 | ||
|
|
a5b76f1dec | ||
|
|
1e586b32a4 | ||
|
|
dfcffe1a9d | ||
|
|
2e6f529dfd | ||
|
|
0ae3ee506f | ||
|
|
6b103f7e3a | ||
|
|
02c32ebace | ||
|
|
603cc7092a | ||
|
|
8590aae2e8 | ||
|
|
511245a68c | ||
|
|
4a347a4f78 | ||
|
|
5f4c47729b | ||
|
|
5975e4767f | ||
|
|
1800304ade | ||
|
|
9af0d8c3c9 | ||
|
|
2e2d3482e4 | ||
|
|
48cc63741e | ||
|
|
a605e43fa6 | ||
|
|
8eda21ca86 | ||
|
|
e8cf50326d | ||
|
|
27c9250efb | ||
|
|
3ecdd1c11b | ||
|
|
912c22d593 | ||
|
|
4b38186b51 | ||
|
|
83f9016528 | ||
|
|
a6044068cd | ||
|
|
4aefb93ab4 | ||
|
|
2861dc4215 | ||
|
|
087280ec17 | ||
|
|
69e6e79e23 | ||
|
|
3292f760ca |
@@ -1,3 +1,9 @@
|
||||
## 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
|
||||
|
||||
16
Makefile
16
Makefile
@@ -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.4-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
|
||||
@@ -98,7 +100,7 @@ win:
|
||||
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
|
||||
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:
|
||||
|
||||
2
VERSION
2
VERSION
@@ -1 +1 @@
|
||||
Version 2.15.4-stable+timestamp.2017.09.01.22.38.25
|
||||
Version 2.16.1-stable+timestamp.2017.11.13.23.50.07
|
||||
|
||||
8
applications/admin/static/js/jquery.js
vendored
8
applications/admin/static/js/jquery.js
vendored
File diff suppressed because one or more lines are too long
@@ -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"> × </span>').slideDown();
|
||||
if (flash.html()) flash.append('<span id="closeflash"> × </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*/
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"> × </span>').slideDown();
|
||||
if (flash.html()) flash.append('<span id="closeflash"> × </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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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('™ '),
|
||||
_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()
|
||||
|
||||
@@ -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 |
10
applications/welcome/static/js/bootstrap.min.js
vendored
10
applications/welcome/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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='­<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){v(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},g=function(a){return a.replace(c.regex.minmaxwh,"").match(c.regex.other)};if(c.ajax=f,c.queue=d,c.unsupportedmq=g,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,comments:/\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,maxw:/\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,minmaxwh:/\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,other:/\([^\)]*\)/g},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var h,i,j,k=a.document,l=k.documentElement,m=[],n=[],o=[],p={},q=30,r=k.getElementsByTagName("head")[0]||l,s=k.getElementsByTagName("base")[0],t=r.getElementsByTagName("link"),u=function(){var a,b=k.createElement("div"),c=k.body,d=l.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=k.createElement("body"),c.style.background="none"),l.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&l.insertBefore(c,l.firstChild),a=b.offsetWidth,f?l.removeChild(c):c.removeChild(b),l.style.fontSize=d,e&&(c.style.fontSize=e),a=j=parseFloat(a)},v=function(b){var c="clientWidth",d=l[c],e="CSS1Compat"===k.compatMode&&d||k.body[c]||d,f={},g=t[t.length-1],p=(new Date).getTime();if(b&&h&&q>p-h)return a.clearTimeout(i),i=a.setTimeout(v,q),void 0;h=p;for(var s in m)if(m.hasOwnProperty(s)){var w=m[s],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?j||u():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?j||u():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(n[w.rules]))}for(var C in o)o.hasOwnProperty(C)&&o[C]&&o[C].parentNode===r&&r.removeChild(o[C]);o.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=k.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,r.insertBefore(E,g.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(k.createTextNode(F)),o.push(E)}},w=function(a,b,d){var e=a.replace(c.regex.comments,"").replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},i=!f&&d;b.length&&(b+="/"),i&&(f=1);for(var j=0;f>j;j++){var k,l,o,p;i?(k=d,n.push(h(a))):(k=e[j].match(c.regex.findStyles)&&RegExp.$1,n.push(RegExp.$2&&h(RegExp.$2))),o=k.split(","),p=o.length;for(var q=0;p>q;q++)l=o[q],g(l)||m.push({media:l.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:n.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}v()},x=function(){if(d.length){var b=d.shift();f(b.href,function(c){w(c,b.href,b.media),p[b.href]=!0,a.setTimeout(function(){x()},0)})}},y=function(){for(var b=0;b<t.length;b++){var c=t[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!p[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(w(c.styleSheet.rawCssText,e,f),p[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!s||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}x()};y(),c.update=y,c.getEmValue=u,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
|
||||
@@ -13,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 */
|
||||
@@ -616,7 +618,7 @@
|
||||
var flash = $('.w2p_flash');
|
||||
web2py.hide_flash();
|
||||
flash.html(message).addClass(status);
|
||||
if (flash.html()) flash.append('<span id="closeflash"> × </span>').slideDown();
|
||||
if (flash.html()) flash.append('<span id="closeflash"> × </span>')[animateIn]();
|
||||
},
|
||||
hide_flash: function () {
|
||||
$('.w2p_flash').fadeOut(0).html('');
|
||||
@@ -630,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();
|
||||
}
|
||||
};
|
||||
@@ -823,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*/
|
||||
|
||||
@@ -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}}
|
||||
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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 ==================================== -->
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -988,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', '')
|
||||
|
||||
@@ -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 = \
|
||||
@@ -678,7 +678,7 @@ def run_view_in(environment):
|
||||
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:
|
||||
@@ -709,23 +709,22 @@ 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:
|
||||
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)
|
||||
# Compile template
|
||||
ccode = compile2(scode, filename)
|
||||
layer = filename
|
||||
layer = filename
|
||||
restricted(ccode, environment, layer=layer, scode=scode)
|
||||
# parse_template saves everything in response body
|
||||
return environment['response'].body.getvalue()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
86
gluon/dal.py
86
gluon/dal.py
@@ -32,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()))
|
||||
@@ -50,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}
|
||||
|
||||
@@ -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,7 +336,7 @@ class Request(Storage):
|
||||
user_agent = session._user_agent
|
||||
if user_agent:
|
||||
return user_agent
|
||||
http_user_agent = 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):
|
||||
|
||||
@@ -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', "\\%{}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Submodule gluon/packages/dal updated: f9f0fdfc9a...d43275953d
125
gluon/sqlhtml.py
125
gluon/sqlhtml.py
@@ -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
|
||||
@@ -682,7 +683,7 @@ 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,
|
||||
user_signature=user_signature, hash_vars=hash_vars)
|
||||
@@ -1087,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):
|
||||
|
||||
@@ -1175,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,
|
||||
)
|
||||
|
||||
@@ -1243,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
|
||||
@@ -1327,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:
|
||||
@@ -2347,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)
|
||||
@@ -2570,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))
|
||||
@@ -3327,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))
|
||||
@@ -3570,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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -3693,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),
|
||||
@@ -3777,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
38
scripts/check_lang_progress.py
Normal file
38
scripts/check_lang_progress.py
Normal 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)
|
||||
82
scripts/update_languages.py
Normal file
82
scripts/update_languages.py
Normal 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)
|
||||
Reference in New Issue
Block a user