This commit is contained in:
pallav_fdsi
2015-07-01 17:49:47 -04:00
56 changed files with 1748 additions and 771 deletions
+2 -2
View File
@@ -17,14 +17,14 @@ install:
before_script: before_script:
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi; - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi;
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache python-coveralls; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache codecov; fi
script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage
after_success: after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --config_file=gluon/tests/coverage.ini; fi - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then codecov; fi
notifications: notifications:
email: true email: true
+4
View File
@@ -1,3 +1,7 @@
## 2.11.1
- Many small but significative improvements and bug fixes
## 2.10.1-2.10.2 ## 2.10.1-2.10.2
- welcome app defaults to Bootstrap 3 - welcome app defaults to Bootstrap 3
+1 -1
View File
@@ -32,7 +32,7 @@ update:
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: src:
### Use semantic versioning ### Use semantic versioning
echo 'Version 2.10.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.11.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files ### rm -f all junk files
make clean make clean
### clean up baisc apps ### clean up baisc apps
+5 -4
View File
@@ -13,7 +13,7 @@ Learn more at http://web2py.com
Then edit ./app.yaml and replace "yourappname" with yourappname. Then edit ./app.yaml and replace "yourappname" with yourappname.
## Import about this GIT repo ## Important reminder about this GIT repo
An important part of web2py is the Database Abstraction Layer (DAL). In early 2015 this was decoupled into a separate code-base (PyDAL). In terms of git, it is a sub-module of the main repository. An important part of web2py is the Database Abstraction Layer (DAL). In early 2015 this was decoupled into a separate code-base (PyDAL). In terms of git, it is a sub-module of the main repository.
@@ -38,9 +38,10 @@ PyDAL uses a separate stable release cycle to the rest of web2py. PyDAL releases
## Tests ## Tests
[![Build Status](https://img.shields.io/travis/web2py/web2py.svg?style=flat-square)](https://travis-ci.org/web2py/web2py) [![Build Status](https://img.shields.io/travis/web2py/web2py/master.svg?style=flat-square&label=Travis-CI)](https://travis-ci.org/web2py/web2py)
[![MS Build Status](https://img.shields.io/appveyor/ci/web2py/web2py/master.svg?style=flat-square&label=Appveyor-CI)](https://ci.appveyor.com/project/web2py/web2py)
[![Coverage Status](https://img.shields.io/codecov/c/github/web2py/web2py.svg?style=flat-square)](https://codecov.io/github/web2py/web2py)
[![Coverage Status](https://img.shields.io/coveralls/web2py/web2py.svg?style=flat-square)](https://coveralls.io/r/web2py/web2py)
## Installation Instructions ## Installation Instructions
@@ -63,7 +64,7 @@ That's it!!!
packages/ > web2py submodules packages/ > web2py submodules
dal/ dal/
contrib/ > third party libraries contrib/ > third party libraries
tests/ > unittests tests/ > unittests
applications/ > are the apps applications/ > are the apps
admin/ > web based IDE admin/ > web based IDE
... ...
+1 -1
View File
@@ -1 +1 @@
Version 2.10.3-stable+timestamp.2015.04.02.16.28.49 Version 2.11.2-stable+timestamp.2015.05.30.11.29.46
+5
View File
@@ -180,6 +180,11 @@ class Servers:
s = wsgi.WSGIServer(callable=app, bind="%s:%d" % address) s = wsgi.WSGIServer(callable=app, bind="%s:%d" % address)
s.start() s.start()
@staticmethod
def waitress(app, address, **options):
from waitress import serve
serve(app, host=address[0], port=address[1], _quiet=True)
def mongrel2_handler(application, conn, debug=False): def mongrel2_handler(application, conn, debug=False):
""" """
+10 -12
View File
@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(), auth.table_group(),
auth.table_permission()]) auth.table_permission()])
manager_role = manager_action.get('role', None) if manager_action else None manager_role = manager_action.get('role', None) if manager_action else None
auth.requires_membership(manager_role)(lambda: None)() if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
raise HTTP(403, "Not authorized")
menu = False menu = False
elif (request.application == 'admin' and not session.authorized) or \ elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)): (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request): def get_databases(request):
dbs = {} dbs = {}
for (key, value) in global_env.items(): for (key, value) in global_env.items():
cond = False
try: try:
cond = isinstance(value, GQLDB) cond = isinstance(value, GQLDB)
except: except:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(), 'oldest': time.time(),
'keys': [] 'keys': []
} }
disk = copy.copy(ram) disk = copy.copy(ram)
total = copy.copy(ram) total = copy.copy(ram)
disk['keys'] = [] disk['keys'] = []
@@ -480,12 +480,12 @@ def ccache():
disk['oldest'] = value[0] disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0]))) disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries'] ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
total['bytes'] = ram['bytes'] + disk['bytes'] ram_keys.remove('ratio')
total['objects'] = ram['objects'] + disk['objects'] ram_keys.remove('oldest')
total['hits'] = ram['hits'] + disk['hits'] for key in ram_keys:
total['misses'] = ram['misses'] + disk['misses'] total[key] = ram[key] + disk[key]
total['keys'] = ram['keys'] + disk['keys']
try: try:
total['ratio'] = total['hits'] * 100 / (total['hits'] + total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses']) total['misses'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if not subgraphs.has_key(group):
subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename) subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext', graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename)) label=table_template(tablename))
+1 -2
View File
@@ -220,7 +220,7 @@ def list_breakpoints():
"Return a list of linenumbers for current breakpoints" "Return a list of linenumbers for current breakpoints"
breakpoints = [] breakpoints = []
ok = None ok = False
try: try:
filename = os.path.join(request.env['applications_parent'], filename = os.path.join(request.env['applications_parent'],
'applications', request.vars.filename) 'applications', request.vars.filename)
@@ -235,5 +235,4 @@ def list_breakpoints():
ok = True ok = True
except Exception, e: except Exception, e:
session.flash = str(e) session.flash = str(e)
ok = False
return response.json({'ok': ok, 'breakpoints': breakpoints}) return response.json({'ok': ok, 'breakpoints': breakpoints})
+51 -26
View File
@@ -292,9 +292,6 @@ def site():
log_progress(appname) log_progress(appname)
session.flash = T(msg, dict(appname=appname, session.flash = T(msg, dict(appname=appname,
digest=md5_hash(installed))) digest=md5_hash(installed)))
elif f and form_update.vars.overwrite:
msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name))
else: else:
msg = 'unable to install application "%(appname)s"' msg = 'unable to install application "%(appname)s"'
session.flash = T(msg, dict(appname=form_update.vars.name)) session.flash = T(msg, dict(appname=form_update.vars.name))
@@ -370,25 +367,56 @@ def pack_plugin():
session.flash = T('internal error') session.flash = T('internal error')
redirect(URL('plugin', args=request.args)) redirect(URL('plugin', args=request.args))
def pack_exe(app, base, filenames=None):
import urllib
import zipfile
from cStringIO import StringIO
# Download latest web2py_win and open it with zipfile
download_url = 'http://www.web2py.com/examples/static/web2py_win.zip'
out = StringIO()
out.write(urllib.urlopen(download_url).read())
web2py_win = zipfile.ZipFile(out, mode='a')
# Write routes.py with the application as default
routes = u'# -*- coding: utf-8 -*-\nrouters = dict(BASE=dict(default_application="%s"))' % app
web2py_win.writestr('web2py/routes.py', routes.encode('utf-8'))
# Copy the application into the zipfile
common_root = os.path.dirname(base)
for filename in filenames:
fname = os.path.join(base, filename)
arcname = os.path.join('web2py/applications', app, filename)
web2py_win.write(fname, arcname)
web2py_win.close()
response.headers['Content-Type'] = 'application/zip'
response.headers['Content-Disposition'] = 'attachment; filename=web2py.app.%s.zip' % app
out.seek(0)
return response.stream(out)
def pack_custom(): def pack_custom():
app = get_app() app = get_app()
base = apath(app, r=request) base = apath(app, r=request)
if request.post_vars.file: if request.post_vars.file:
files = request.post_vars.file files = request.post_vars.file
files = [files] if not isinstance(files,list) else files files = [files] if not isinstance(files,list) else files
fname = 'web2py.app.%s.w2p' % app if request.post_vars.doexe is None:
try: fname = 'web2py.app.%s.w2p' % app
filename = app_pack(app, request, raise_ex=True, filenames=files) try:
except Exception, e: filename = app_pack(app, request, raise_ex=True, filenames=files)
filename = None except Exception, e:
if filename: filename = None
response.headers['Content-Type'] = 'application/w2p' if filename:
disposition = 'attachment; filename=%s' % fname response.headers['Content-Type'] = 'application/w2p'
response.headers['Content-Disposition'] = disposition disposition = 'attachment; filename=%s' % fname
return safe_read(filename, 'rb') response.headers['Content-Disposition'] = disposition
return safe_read(filename, 'rb')
else:
session.flash = T('internal error: %s', e)
redirect(URL(args=request.args))
else: else:
session.flash = T('internal error: %s', e) return pack_exe(app, base, files)
redirect(URL(args=request.args))
def ignore(fs): def ignore(fs):
return [f for f in fs if not ( return [f for f in fs if not (
f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))] f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))]
@@ -744,7 +772,7 @@ def edit():
viewlist.append(aviewpath + '.html') viewlist.append(aviewpath + '.html')
if len(viewlist): if len(viewlist):
editviewlinks = [] editviewlinks = []
for v in viewlist: for v in sorted(viewlist):
vf = os.path.split(v)[-1] vf = os.path.split(v)[-1]
vargs = "/".join([viewpath.replace(os.sep, "/"), vf]) vargs = "/".join([viewpath.replace(os.sep, "/"), vf])
editviewlinks.append(A(vf.split(".")[0], editviewlinks.append(A(vf.split(".")[0],
@@ -754,6 +782,7 @@ def edit():
if len(request.args) > 2 and request.args[1] == 'controllers': if len(request.args) > 2 and request.args[1] == 'controllers':
controller = (request.args[2])[:-3] controller = (request.args[2])[:-3]
functions = find_exposed_functions(data) functions = find_exposed_functions(data)
functions = functions and sorted(functions) or []
else: else:
(controller, functions) = (None, None) (controller, functions) = (None, None)
@@ -866,13 +895,9 @@ def resolve():
def getclass(item): def getclass(item):
""" Determine item class """ """ Determine item class """
operators = {' ':'normal', '+':'plus', '-':'minus'}
if item[0] == ' ':
return 'normal' return operators[item[0]]
if item[0] == '+':
return 'plus'
if item[0] == '-':
return 'minus'
if request.vars: if request.vars:
c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0] c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0]
@@ -1067,7 +1092,7 @@ def design():
for c in controllers: for c in controllers:
data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
items = find_exposed_functions(data) items = find_exposed_functions(data)
functions[c] = items functions[c] = items and sorted(items) or []
# Get all views # Get all views
views = sorted( views = sorted(
@@ -1205,7 +1230,7 @@ def plugin():
for c in controllers: for c in controllers:
data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) data = safe_read(apath('%s/controllers/%s' % (app, c), r=request))
items = find_exposed_functions(data) items = find_exposed_functions(data)
functions[c] = items functions[c] = items and sorted(items) or []
# Get all views # Get all views
views = sorted( views = sorted(
@@ -1509,7 +1534,7 @@ def upload_file():
if filename: if filename:
d = dict(filename=filename[len(path):]) d = dict(filename=filename[len(path):])
else: else:
d = dict(filename='unkown') d = dict(filename='unknown')
session.flash = T('cannot upload file "%(filename)s"', d) session.flash = T('cannot upload file "%(filename)s"', d)
redirect(request.vars.sender) redirect(request.vars.sender)
+23 -21
View File
@@ -490,12 +490,12 @@
* and prevent clicking on it */ * and prevent clicking on it */
disableElement: function(el) { disableElement: function(el) {
el.addClass('disabled'); el.addClass('disabled');
var method = el.is('button') ? 'html' : 'val'; var method = el.is('input') ? 'val' : 'html';
//method = el.attr('name') ? 'html' : 'val'; //method = el.attr('name') ? 'html' : 'val';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state if not already disabled */ /*store enabled state if not already disabled */
if(el.data('w2p:enable-with') === undefined) { if(el.data('w2p_enable_with') === undefined) {
el.data('w2p:enable-with', el[method]()); el.data('w2p_enable_with', el[method]());
} }
/*if you don't want to see "working..." on buttons, replace the following /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
@@ -515,11 +515,11 @@
/* restore element to its original state which was disabled by 'disableElement' above*/ /* restore element to its original state which was disabled by 'disableElement' above*/
enableElement: function(el) { enableElement: function(el) {
var method = el.is('button') ? 'val' : 'html'; var method = el.is('input') ? 'val' : 'html';
if(el.data('w2p:enable-with') !== undefined) { if(el.data('w2p_enable_with') !== undefined) {
/* set to old enabled state */ /* set to old enabled state */
el[method](el.data('w2p:enable-with')); el[method](el.data('w2p_enable_with'));
el.removeData('w2p:enable-with'); el.removeData('w2p_enable_with');
} }
el.removeClass('disabled'); el.removeClass('disabled');
el.unbind('click.w2pDisable'); el.unbind('click.w2pDisable');
@@ -586,12 +586,14 @@
if(pre_call != undefined) { if(pre_call != undefined) {
eval(pre_call); eval(pre_call);
} }
if(confirm_message != undefined) { if(confirm_message) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; if(confirm_message == 'default')
if(!web2py.confirm(confirm_message)) { confirm_message = w2p_ajax_confirm_message ||
web2py.stopEverything(e); 'Are you sure you want to delete this object?';
return; if(!web2py.confirm(confirm_message)) {
} web2py.stopEverything(e);
return;
}
} }
if(target == undefined) { if(target == undefined) {
if(method == 'GET') { if(method == 'GET') {
@@ -634,7 +636,7 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Caches element value in 'w2p:enable-with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-disable-with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
@@ -646,8 +648,8 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) element.data('w2p_disable_with', element[method]())
} }
if(element.data('w2p:enable-with') === undefined) { if(element.data('w2p_enable_with') === undefined) {
element.data('w2p:enable-with', element[method]()); element.data('w2p_enable_with', element[method]());
} }
element[method](element.data('w2p_disable_with')); element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
@@ -655,16 +657,16 @@
}, },
/* Re-enables disabled form elements: /* Re-enables disabled form elements:
- Replaces element text with cached value from 'w2p:enable-with' data store (created in `disableFormElements`) - Replaces element text with cached value from 'w2p_enable_with' data store (created in `disableFormElements`)
- Sets disabled property to false - Sets disabled property to false
*/ */
enableFormElements: function(form) { enableFormElements: function(form) {
form.find(web2py.enableSelector).each(function() { form.find(web2py.enableSelector).each(function() {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) { if(element.data('w2p_enable_with')) {
element[method](element.data('w2p:enable-with')); element[method](element.data('w2p_enable_with'));
element.removeData('w2p:enable-with'); element.removeData('w2p_enable_with');
} }
element.prop('disabled', false); element.prop('disabled', false);
}); });
@@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link; web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy; web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/ */
/* compatibility code - end*/ /* compatibility code - end*/
+3 -1
View File
@@ -1,5 +1,7 @@
{{extend 'layout.html'}} {{extend 'layout.html'}}
{{ {{
import re
regex_space = re.compile('\s+')
def all(items): def all(items):
return reduce(lambda a,b:a and b,items,True) return reduce(lambda a,b:a and b,items,True)
def peekfile(path,file,vars={},title=None): def peekfile(path,file,vars={},title=None):
@@ -304,7 +306,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
while path!=file_path: while path!=file_path:
if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]): if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]):
path.append(file_path[len(path)]) path.append(file_path[len(path)])
thispath='static__'+'__'.join(path) thispath = regex_space.sub('-', 'static__'+'__'.join(path))
}} }}
<li class="folder"><i>&nbsp;</i> <li class="folder"><i>&nbsp;</i>
<a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a> <a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a>
@@ -25,6 +25,7 @@
<form action="{{=URL(args=request.args)}}" method="POST"> <form action="{{=URL(args=request.args)}}" method="POST">
<h2>{{=T('Select Files to Package')}}</h2> <h2>{{=T('Select Files to Package')}}</h2>
<input type="submit" value="{{=T('Download .w2p')}}" class="btn"/> <input type="submit" value="{{=T('Download .w2p')}}" class="btn"/>
<input type="submit" name="doexe" value="{{=T('Download as .exe')}}" class="btn"/>
<div style="margin-top:20px"> <div style="margin-top:20px">
{{tree(base)}} {{tree(base)}}
</div> </div>
+10 -12
View File
@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(), auth.table_group(),
auth.table_permission()]) auth.table_permission()])
manager_role = manager_action.get('role', None) if manager_action else None manager_role = manager_action.get('role', None) if manager_action else None
auth.requires_membership(manager_role)(lambda: None)() if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
raise HTTP(403, "Not authorized")
menu = False menu = False
elif (request.application == 'admin' and not session.authorized) or \ elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)): (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request): def get_databases(request):
dbs = {} dbs = {}
for (key, value) in global_env.items(): for (key, value) in global_env.items():
cond = False
try: try:
cond = isinstance(value, GQLDB) cond = isinstance(value, GQLDB)
except: except:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(), 'oldest': time.time(),
'keys': [] 'keys': []
} }
disk = copy.copy(ram) disk = copy.copy(ram)
total = copy.copy(ram) total = copy.copy(ram)
disk['keys'] = [] disk['keys'] = []
@@ -480,12 +480,12 @@ def ccache():
disk['oldest'] = value[0] disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0]))) disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries'] ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
total['bytes'] = ram['bytes'] + disk['bytes'] ram_keys.remove('ratio')
total['objects'] = ram['objects'] + disk['objects'] ram_keys.remove('oldest')
total['hits'] = ram['hits'] + disk['hits'] for key in ram_keys:
total['misses'] = ram['misses'] + disk['misses'] total[key] = ram[key] + disk[key]
total['keys'] = ram['keys'] + disk['keys']
try: try:
total['ratio'] = total['hits'] * 100 / (total['hits'] + total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses']) total['misses'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if not subgraphs.has_key(group):
subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename) subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext', graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename)) label=table_template(tablename))
@@ -8,6 +8,7 @@
#### Learning and Demos #### Learning and Demos
- [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]] - [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]]
- [[Step by step tutorial https://milesm.pythonanywhere.com/wiki]]
- [[Killer Web Development Tutorial http://killer-web-development.com/]] - [[Killer Web Development Tutorial http://killer-web-development.com/]]
- [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!) - [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!)
- [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE) - [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE)
@@ -22,5 +23,6 @@
- [[More Plugins http://dev.s-cubism.com/web2py_plugins]] - [[More Plugins http://dev.s-cubism.com/web2py_plugins]]
- [[Appliances http://www.web2py.com/appliances popup]] - [[Appliances http://www.web2py.com/appliances popup]]
- [[web2py utils http://packages.python.org/web2py_utils/ popup]] - [[web2py utils http://packages.python.org/web2py_utils/ popup]]
- [[Sublime text 3 plugin https://bitbucket.org/kfog/w2p popup]]
#### [[Sites Powered by web2py http://www.web2py.com/poweredby popup]] #### [[Sites Powered by web2py http://www.web2py.com/poweredby popup]]
+23 -21
View File
@@ -490,12 +490,12 @@
* and prevent clicking on it */ * and prevent clicking on it */
disableElement: function(el) { disableElement: function(el) {
el.addClass('disabled'); el.addClass('disabled');
var method = el.is('button') ? 'html' : 'val'; var method = el.is('input') ? 'val' : 'html';
//method = el.attr('name') ? 'html' : 'val'; //method = el.attr('name') ? 'html' : 'val';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state if not already disabled */ /*store enabled state if not already disabled */
if(el.data('w2p:enable-with') === undefined) { if(el.data('w2p_enable_with') === undefined) {
el.data('w2p:enable-with', el[method]()); el.data('w2p_enable_with', el[method]());
} }
/*if you don't want to see "working..." on buttons, replace the following /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
@@ -515,11 +515,11 @@
/* restore element to its original state which was disabled by 'disableElement' above*/ /* restore element to its original state which was disabled by 'disableElement' above*/
enableElement: function(el) { enableElement: function(el) {
var method = el.is('button') ? 'val' : 'html'; var method = el.is('input') ? 'val' : 'html';
if(el.data('w2p:enable-with') !== undefined) { if(el.data('w2p_enable_with') !== undefined) {
/* set to old enabled state */ /* set to old enabled state */
el[method](el.data('w2p:enable-with')); el[method](el.data('w2p_enable_with'));
el.removeData('w2p:enable-with'); el.removeData('w2p_enable_with');
} }
el.removeClass('disabled'); el.removeClass('disabled');
el.unbind('click.w2pDisable'); el.unbind('click.w2pDisable');
@@ -586,12 +586,14 @@
if(pre_call != undefined) { if(pre_call != undefined) {
eval(pre_call); eval(pre_call);
} }
if(confirm_message != undefined) { if(confirm_message) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; if(confirm_message == 'default')
if(!web2py.confirm(confirm_message)) { confirm_message = w2p_ajax_confirm_message ||
web2py.stopEverything(e); 'Are you sure you want to delete this object?';
return; if(!web2py.confirm(confirm_message)) {
} web2py.stopEverything(e);
return;
}
} }
if(target == undefined) { if(target == undefined) {
if(method == 'GET') { if(method == 'GET') {
@@ -634,7 +636,7 @@
}); });
}, },
/* Disables form elements: /* Disables form elements:
- Caches element value in 'w2p:enable-with' data store - Caches element value in 'w2p_enable_with' data store
- Replaces element text with value of 'data-disable-with' attribute - Replaces element text with value of 'data-disable-with' attribute
- Sets disabled property to true - Sets disabled property to true
*/ */
@@ -646,8 +648,8 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) element.data('w2p_disable_with', element[method]())
} }
if(element.data('w2p:enable-with') === undefined) { if(element.data('w2p_enable_with') === undefined) {
element.data('w2p:enable-with', element[method]()); element.data('w2p_enable_with', element[method]());
} }
element[method](element.data('w2p_disable_with')); element[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
@@ -655,16 +657,16 @@
}, },
/* Re-enables disabled form elements: /* Re-enables disabled form elements:
- Replaces element text with cached value from 'w2p:enable-with' data store (created in `disableFormElements`) - Replaces element text with cached value from 'w2p_enable_with' data store (created in `disableFormElements`)
- Sets disabled property to false - Sets disabled property to false
*/ */
enableFormElements: function(form) { enableFormElements: function(form) {
form.find(web2py.enableSelector).each(function() { form.find(web2py.enableSelector).each(function() {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) { if(element.data('w2p_enable_with')) {
element[method](element.data('w2p:enable-with')); element[method](element.data('w2p_enable_with'));
element.removeData('w2p:enable-with'); element.removeData('w2p_enable_with');
} }
element.prop('disabled', false); element.prop('disabled', false);
}); });
@@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers;
web2py_trap_link = jQuery.web2py.trap_link; web2py_trap_link = jQuery.web2py.trap_link;
web2py_calc_entropy = jQuery.web2py.calc_entropy; web2py_calc_entropy = jQuery.web2py.calc_entropy;
*/ */
/* compatibility code - end*/ /* compatibility code - end*/
+6 -6
View File
@@ -18,7 +18,7 @@
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="alltables"> <div class="tab-pane active" id="alltables">
<table> <table class="table">
{{for db in sorted(databases):}} {{for db in sorted(databases):}}
{{for table in databases[db].tables:}} {{for table in databases[db].tables:}}
{{qry='%s.%s.id>0'%(db,table)}} {{qry='%s.%s.id>0'%(db,table)}}
@@ -40,7 +40,7 @@
{{=A("%s.%s" % (db,table),_href=URL('select',args=[db],vars=dict(query=qry)))}} {{=A("%s.%s" % (db,table),_href=URL('select',args=[db],vars=dict(query=qry)))}}
</th> </th>
<td> <td>
{{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn")}} {{=A(str(T('New Record')),_href=URL('insert',args=[db,table]),_class="btn btn-default")}}
</td> </td>
</tr> </tr>
{{pass}} {{pass}}
@@ -61,7 +61,7 @@
</pre> </pre>
{{pass}} {{pass}}
{{if table:}} {{if table:}}
{{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn")}}<br/><br/> {{=A(str(T('New Record')),_href=URL('insert',args=[request.args[0],table]),_class="btn btn-default")}}<br/><br/>
<h3>{{=T("Rows in Table")}}</h3><br/> <h3>{{=T("Rows in Table")}}</h3><br/>
{{else:}} {{else:}}
<h3>{{=T("Rows selected")}}</h3><br/> <h3>{{=T("Rows selected")}}</h3><br/>
@@ -72,8 +72,8 @@
{{=T('"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN')}}</p> {{=T('"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN')}}</p>
<br/><br/> <br/><br/>
<h4>{{=T("%s selected", nrows)}}</h4> <h4>{{=T("%s selected", nrows)}}</h4>
{{if start>0:}}{{=A(T('previous %s rows') % step,_href=URL('select',args=request.args[0],vars=dict(start=start-step)),_class="btn")}}{{pass}} {{if start>0:}}{{=A(T('previous %s rows') % step,_href=URL('select',args=request.args[0],vars=dict(start=start-step)),_class="btn btn-default")}}{{pass}}
{{if stop<nrows:}}{{=A(T('next %s rows') % step,_href=URL('select',args=request.args[0],vars=dict(start=start+step)),_class="btn")}}{{pass}} {{if stop<nrows:}}{{=A(T('next %s rows') % step,_href=URL('select',args=request.args[0],vars=dict(start=start+step)),_class="btn btn-default")}}{{pass}}
{{if rows:}} {{if rows:}}
<div style="overflow:auto; width:80%;"> <div style="overflow:auto; width:80%;">
{{linkto = lambda f, t, r: URL('update', args=[request.args[0], r, f]) if f else "#"}} {{linkto = lambda f, t, r: URL('update', args=[request.args[0], r, f]) if f else "#"}}
@@ -82,7 +82,7 @@
</div> </div>
{{pass}} {{pass}}
<br/><br/><h3>{{=T("Import/Export")}}</h3><br/> <br/><br/><h3>{{=T("Import/Export")}}</h3><br/>
<a href="{{=URL('csv',args=request.args[0],vars=dict(query=query))}}" class="btn">{{=T("export as csv file")}}</a> <a href="{{=URL('csv',args=request.args[0],vars=dict(query=query))}}" class="btn btn-default">{{=T("export as csv file")}}</a>
{{=formcsv or ''}} {{=formcsv or ''}}
{{elif request.function=='insert':}} {{elif request.function=='insert':}}
@@ -36,7 +36,7 @@
</center> </center>
<p style="text-align:left;"> <p style="text-align:left;">
The source code version works on all supported platforms, including Linux, but it requires Python 2.5, 2.6, or 2.7. The source code version works on all supported platforms, including Linux, but it requires Python 2.6, or 2.7 (recommended).
It runs on Windows and most Unix systems, including <b>Linux</b> and <b>BSD</b>. It runs on Windows and most Unix systems, including <b>Linux</b> and <b>BSD</b>.
</p> </p>
@@ -17,22 +17,30 @@
<ul> <ul>
<li><a target="_blank" href="http://experts4solutions.com">Experts4Soutions</a> (worldwide)</li> <li><a target="_blank" href="http://experts4solutions.com">Experts4Soutions</a> (worldwide)</li>
<li><a target="_blank" href="http://www.planethost.com">PlanetHost</a> (USA)</li> <li><a target="_blank" href="http://www.planethost.com">PlanetHost</a> (USA)</li>
<li><a target="_blank" href="http://www.10biosystems.com">10BioSystems</a> (USA)</li>
<li><a target="_blank" href="http://www.formatics.nl">Formatics</a> (Netherlands)</li> <li><a target="_blank" href="http://www.formatics.nl">Formatics</a> (Netherlands)</li>
<li><a target="_blank" href="http://www.corebyte.nl">Corebyte</a> (Netherlands)</li>
<li><a target="_blank" href="http://www.dutveul.nl">Dutveul</a> (Netherlands)</li>
<li><a target="_blank" href="http://www.onemewebservices.com">OneMeWebServices</a> (Canada)</li> <li><a target="_blank" href="http://www.onemewebservices.com">OneMeWebServices</a> (Canada)</li>
<li><a target="_blank" href="http://www.budgetbytes.nl">BudgetBytes</a> (The Netherlands)</li> <li><a target="_blank" href="http://www.budgetbytes.nl">BudgetBytes</a> (The Netherlands)</li>
<li><a target="_blank" href="http://www.androsoft.pl">ANDROSoft</a> (Poland)</li> <li><a target="_blank" href="http://www.androsoft.pl">ANDROSoft</a> (Poland)</li>
<li><a target="_blank" href="www.sonnetech.com.br">Sonne Tech</a> (Brazil)</li> <li><a target="_blank" href="http://www.sonnetech.com.br">Sonne Tech</a> (Brazil)</li>
<li><a target="_blank" href="http://www.nrg.com.br">NRG Internet Solutions</a> (Brazil)</li> <li><a target="_blank" href="http://www.nrg.com.br">NRG Internet Solutions</a> (Brazil)</li>
<li><a target="_blank" href="http://itjp.net.br/">ITJP</a> (Brazil)</li> <li><a target="_blank" href="http://itjp.net.br/">ITJP</a> (Brazil)</li>
<li><a target="_blank" href="http://i-am.pt">I am Consultoria</a> (Portugal)</li> <li><a target="_blank" href="http://i-am.pt">I am Consultoria</a> (Portugal)</li>
<li><a target="_blank" href="http://www.definescope.com/">DefineScope</a> (Portugal)</li>
<li><a target="_blank" href="http://lpfx.com.br">LPFX</a> (Brazil)</li> <li><a target="_blank" href="http://lpfx.com.br">LPFX</a> (Brazil)</li>
<li><a target="_blank" href="http://emotionull.com">Emotionull</a> (Greece and Cyprus)</li> <li><a target="_blank" href="http://emotionull.com">Emotionull</a> (Greece and Cyprus)</li>
<li><a target="_blank" href="http://www.vsa-services.com/">VSA Services</a> (Singapore)</li> <li><a target="_blank" href="http://www.vsa-services.com/">VSA Services</a> (Singapore)</li>
<li><a target="_blank" href="http://www.albendas.com">Albendas</a> (Spain)</li> <li><a target="_blank" href="http://www.albendas.com">Albendas</a> (Spain)</li>
<li><a target="_blank" href="www.corebyte.nl">Corebyte</a> (Netherland)</li>
<li><a target="_blank" href="https://loadinfo-net.appspot.com">LoadInfo</a> (Bulgaria)</li> <li><a target="_blank" href="https://loadinfo-net.appspot.com">LoadInfo</a> (Bulgaria)</li>
<li><a target="_blank" href="http://www.appliedobjects.com">Applied Objects</a> (New Zealand)</li> <li><a target="_blank" href="http://www.appliedobjects.com">Applied Objects</a> (New Zealand)</li>
<li><a target="_blank" href="http://www.sistemasagiles.com.ar/">Sistemas Ágiles</a> ("Agile Systems") (Argentina)</li> <li><a target="_blank" href="http://www.sistemasagiles.com.ar/">Sistemas Ágiles</a> ("Agile Systems") (Argentina)</li>
<li><a target="_blank" href="http://www.definescope.com/en/services/consulting/">DefineScope</a> (Portugal)</li>
<li><a target="_blank" href="http://10Biosystems.com">10BioSystems</a></li>
<li><a target="_blank" href="http://www.dutveul.nl">Dutveul</a> (Netherlands)</li>
</ul> </ul>
</div> </div>
+10 -12
View File
@@ -49,7 +49,8 @@ if request.function == 'manage':
auth.table_group(), auth.table_group(),
auth.table_permission()]) auth.table_permission()])
manager_role = manager_action.get('role', None) if manager_action else None manager_role = manager_action.get('role', None) if manager_action else None
auth.requires_membership(manager_role)(lambda: None)() if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
raise HTTP(403, "Not authorized")
menu = False menu = False
elif (request.application == 'admin' and not session.authorized) or \ elif (request.application == 'admin' and not session.authorized) or \
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)): (request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
@@ -80,7 +81,6 @@ if False and request.tickets_db:
def get_databases(request): def get_databases(request):
dbs = {} dbs = {}
for (key, value) in global_env.items(): for (key, value) in global_env.items():
cond = False
try: try:
cond = isinstance(value, GQLDB) cond = isinstance(value, GQLDB)
except: except:
@@ -420,7 +420,7 @@ def ccache():
'oldest': time.time(), 'oldest': time.time(),
'keys': [] 'keys': []
} }
disk = copy.copy(ram) disk = copy.copy(ram)
total = copy.copy(ram) total = copy.copy(ram)
disk['keys'] = [] disk['keys'] = []
@@ -480,12 +480,12 @@ def ccache():
disk['oldest'] = value[0] disk['oldest'] = value[0]
disk['keys'].append((key, GetInHMS(time.time() - value[0]))) disk['keys'].append((key, GetInHMS(time.time() - value[0])))
total['entries'] = ram['entries'] + disk['entries'] ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
total['bytes'] = ram['bytes'] + disk['bytes'] ram_keys.remove('ratio')
total['objects'] = ram['objects'] + disk['objects'] ram_keys.remove('oldest')
total['hits'] = ram['hits'] + disk['hits'] for key in ram_keys:
total['misses'] = ram['misses'] + disk['misses'] total[key] = ram[key] + disk[key]
total['keys'] = ram['keys'] + disk['keys']
try: try:
total['ratio'] = total['hits'] * 100 / (total['hits'] + total['ratio'] = total['hits'] * 100 / (total['hits'] +
total['misses']) total['misses'])
@@ -577,9 +577,7 @@ def bg_graph_model():
group = meta_graphmodel['group'].replace(' ', '') group = meta_graphmodel['group'].replace(' ', '')
if not subgraphs.has_key(group): if not subgraphs.has_key(group):
subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
subgraphs[group]['tables'].append(tablename) subgraphs[group]['tables'].append(tablename)
else:
subgraphs[group]['tables'].append(tablename)
graph.add_node(tablename, name=tablename, shape='plaintext', graph.add_node(tablename, name=tablename, shape='plaintext',
label=table_template(tablename)) label=table_template(tablename))
@@ -30,6 +30,7 @@ def user():
http://..../[app]/default/user/retrieve_password http://..../[app]/default/user/retrieve_password
http://..../[app]/default/user/change_password http://..../[app]/default/user/change_password
http://..../[app]/default/user/manage_users (requires membership in http://..../[app]/default/user/manage_users (requires membership in
http://..../[app]/default/user/bulk_register
use @auth.requires_login() use @auth.requires_login()
@auth.requires_membership('group name') @auth.requires_membership('group name')
@auth.requires_permission('read','table name',record_id) @auth.requires_permission('read','table name',record_id)
+2 -2
View File
@@ -58,11 +58,11 @@ service = Service()
plugins = PluginManager() plugins = PluginManager()
## create all tables needed by auth if not custom tables ## create all tables needed by auth if not custom tables
auth.define_tables(username=False, signature=False) auth.define_tables(username=False, signature=False, enable_tokens=False)
## configure email ## configure email
mail = auth.settings.mailer mail = auth.settings.mailer
mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.sender') mail.settings.server = 'logging' if request.is_local else myconf.take('smtp.server')
mail.settings.sender = myconf.take('smtp.sender') mail.settings.sender = myconf.take('smtp.sender')
mail.settings.login = myconf.take('smtp.login') mail.settings.login = myconf.take('smtp.login')
+2 -2
View File
@@ -7,11 +7,11 @@
# Language from default.py or 'en' (if the file is not found) is used as # Language from default.py or 'en' (if the file is not found) is used as
# a default_language # a default_language
# #
# See <web2py-root-dir>/router.example.py for parameter's detail # See <web2py-root-dir>/examples/routes.parametric.example.py for parameter's detail
#------------------------------------------------------------------------------------- #-------------------------------------------------------------------------------------
# To enable this route file you must do the steps: # To enable this route file you must do the steps:
# #
# 1. rename <web2py-root-dir>/router.example.py to routes.py # 1. rename <web2py-root-dir>/examples/routes.parametric.example.py to routes.py
# 2. rename this APP/routes.example.py to APP/routes.py # 2. rename this APP/routes.example.py to APP/routes.py
# (where APP - is your application directory) # (where APP - is your application directory)
# 3. restart web2py (or reload routes in web2py admin interfase) # 3. restart web2py (or reload routes in web2py admin interfase)
File diff suppressed because one or more lines are too long
@@ -24,7 +24,9 @@ div.flash.alert:hover {
.ie-lte8 div.flash:hover { .ie-lte8 div.flash:hover {
filter: alpha(opacity=25); filter: alpha(opacity=25);
} }
.main-container {
margin-top: 20px;
}
div.error { div.error {
width: auto; width: auto;
@@ -283,6 +285,7 @@ li.w2p_grid_breadcrumb_elem {
.web2py_console .form-control { .web2py_console .form-control {
width: 20%; width: 20%;
display: inline; display: inline;
height: 100%;
} }
.web2py_console #w2p_keywords { .web2py_console #w2p_keywords {
width: 50%; width: 50%;
+8 -6
View File
@@ -586,12 +586,14 @@
if(pre_call != undefined) { if(pre_call != undefined) {
eval(pre_call); eval(pre_call);
} }
if(confirm_message != undefined) { if(confirm_message) {
if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; if(confirm_message == 'default')
if(!web2py.confirm(confirm_message)) { confirm_message = w2p_ajax_confirm_message ||
web2py.stopEverything(e); 'Are you sure you want to delete this object?';
return; if(!web2py.confirm(confirm_message)) {
} web2py.stopEverything(e);
return;
}
} }
if(target == undefined) { if(target == undefined) {
if(method == 'GET') { if(method == 'GET') {
+1 -1
View File
@@ -75,7 +75,7 @@
{{end}} {{end}}
<!-- Main ========================================= --> <!-- Main ========================================= -->
<!-- Begin page content --> <!-- Begin page content -->
<div class="container-fluid"> <div class="container-fluid main-container">
{{if left_sidebar_enabled:}} {{if left_sidebar_enabled:}}
<div class="col-md-3 left-sidebar"> <div class="col-md-3 left-sidebar">
{{block left_sidebar}} {{block left_sidebar}}
+25
View File
@@ -0,0 +1,25 @@
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
COVERAGE_PROCESS_START: gluon/tests/coverage.ini
clone_depth: 50
init:
- "ECHO %PYTHON%"
- set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
install:
- ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py
- python get-pip.py
- pip install codecov
- git submodule update --init --recursive
test_script:
- python web2py.py --run_system_tests --with_coverage
after_test:
- coverage combine
- codecov
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- app configuration for web2py on IIS -->
<configuration>
<appSettings>
<add key="WSGI_HANDLER" value="gluon.main.wsgibase" />
<add key="WSGI_RESTART_FILE_REGEX" value=".*((routes\.py)|(\.config))$" />
</appSettings>
<system.webServer>
<rewrite>
<rules>
<clear />
<rule name="static" enabled="true" stopProcessing="true">
<match url="^(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="applications/{R:1}/static/{R:2}" logRewrittenUrl="false" />
</rule>
<rule name="web2py_app" enabled="true" stopProcessing="true">
<match url="(.*)" ignoreCase="false" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
<action type="Rewrite" url="handler.web2py/{R:1}" appendQueryString="true" />
</rule>
</rules>
<outboundRules>
<rule name="static_version_cache_control" preCondition="static_version">
<match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
<action type="Rewrite" value="max-age=315360000" />
<conditions>
</conditions>
</rule>
<rule name="static_version_Expires" preCondition="static_version">
<match serverVariable="RESPONSE_Expires" pattern=".*" />
<action type="Rewrite" value="Thu, 31 Dec 2037 23:59:59 GMT" />
</rule>
<preConditions>
<preCondition name="static_version">
<add input="{REQUEST_URI}" pattern="(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
<handlers>
<!-- replace SCRIPT_PROCESSOR with the configured handler for python -->
<add name="Python_via_FastCGI" path="handler.web2py" verb="*" modules="FastCgiModule" scriptProcessor="SCRIPT_PROCESSOR" resourceType="Unspecified" requireAccess="Script" />
</handlers>
</system.webServer>
</configuration>
+9 -4
View File
@@ -99,7 +99,7 @@ class CacheAbstract(object):
""" """
cache_stats_name = 'web2py_cache_statistics' cache_stats_name = 'web2py_cache_statistics'
max_ram_utilization = 90 # percent max_ram_utilization = None # percent
def __init__(self, request=None): def __init__(self, request=None):
"""Initializes the object """Initializes the object
@@ -353,7 +353,7 @@ class CacheOnDisk(CacheAbstract):
raise KeyError raise KeyError
self.wait_portalock(val_file) self.wait_portalock(val_file)
value = pickle.load(recfile.open(key, 'rb', path=self.folder)) value = pickle.load(val_file)
val_file.close() val_file.close()
return value return value
@@ -473,9 +473,14 @@ class CacheOnDisk(CacheAbstract):
if item and ((dt is None) or (item[0] > now - dt)): if item and ((dt is None) or (item[0] > now - dt)):
value = item[1] value = item[1]
else: else:
value = f() try:
value = f()
except:
self.storage.release(CacheAbstract.cache_stats_name)
self.storage.release(key)
raise
self.storage[key] = (now, value) self.storage[key] = (now, value)
self.storage.safe_apply(CacheAbstract.cache_stats_name, inc_misses, self.storage.safe_apply(CacheAbstract.cache_stats_name, inc_misses,
default_value={'hit_total': 0, 'misses': 0}) default_value={'hit_total': 0, 'misses': 0})
self.storage.release(CacheAbstract.cache_stats_name) self.storage.release(CacheAbstract.cache_stats_name)
+9 -8
View File
@@ -261,7 +261,7 @@ class LoadFactory(object):
import globals import globals
target = target or 'c' + str(random.random())[2:] target = target or 'c' + str(random.random())[2:]
attr['_id'] = target attr['_id'] = target
request = self.environment['request'] request = current.request
if '.' in f: if '.' in f:
f, extension = f.rsplit('.', 1) f, extension = f.rsplit('.', 1)
if url or ajax: if url or ajax:
@@ -532,10 +532,11 @@ def run_models_in(environment):
It tries pre-compiled models first before compiling them. It tries pre-compiled models first before compiling them.
""" """
folder = environment['request'].folder request = current.request
c = environment['request'].controller folder = request.folder
c = request.controller
#f = environment['request'].function #f = environment['request'].function
response = environment['response'] response = current.response
path = pjoin(folder, 'models') path = pjoin(folder, 'models')
cpath = pjoin(folder, 'compiled') cpath = pjoin(folder, 'compiled')
@@ -577,7 +578,7 @@ def run_controller_in(controller, function, environment):
""" """
# if compiled should run compiled! # if compiled should run compiled!
folder = environment['request'].folder folder = current.request.folder
path = pjoin(folder, 'compiled') path = pjoin(folder, 'compiled')
badc = 'invalid controller (%s/%s)' % (controller, function) badc = 'invalid controller (%s/%s)' % (controller, function)
badf = 'invalid function (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function)
@@ -631,7 +632,7 @@ def run_controller_in(controller, function, environment):
layer = filename + ':' + function layer = filename + ':' + function
code = getcfs(layer, filename, lambda: compile2(code, layer)) code = getcfs(layer, filename, lambda: compile2(code, layer))
restricted(code, environment, filename) restricted(code, environment, filename)
response = environment['response'] response = current.response
vars = response._vars vars = response._vars
if response.postprocessing: if response.postprocessing:
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars) vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
@@ -649,8 +650,8 @@ def run_view_in(environment):
or `view/generic.extension` or `view/generic.extension`
It tries the pre-compiled views_controller_function.pyc before compiling it. It tries the pre-compiled views_controller_function.pyc before compiling it.
""" """
request = environment['request'] request = current.request
response = environment['response'] response = current.response
view = response.view view = response.view
folder = request.folder folder = request.folder
path = pjoin(folder, 'compiled') path = pjoin(folder, 'compiled')
+13 -3
View File
@@ -521,9 +521,19 @@ def ldap_auth(server='ldap', port=None,
logging.error( logging.error(
'There is no username or email for %s!' % username) 'There is no username or email for %s!' % username)
raise raise
db_group_search = db((db.auth_membership.user_id == db_user_id) & # if old pydal version, assume this is a relational database which can do joins
(db.auth_user.id == db.auth_membership.user_id) & db_can_join = db.can_join() if hasattr(db, 'can_join') else True
(db.auth_group.id == db.auth_membership.group_id)) if db_can_join:
db_group_search = db(
(db.auth_membership.user_id == db_user_id) &
(db.auth_user.id == db.auth_membership.user_id) &
(db.auth_group.id == db.auth_membership.group_id))
else:
# no joins on NoSQL databases, perform two queries
db_group_search = db(db.auth_membership.user_id == db_user_id)
group_ids = [x.group_id for x in db_group_search.select(
db.auth_membership.group_id, distinct=True)]
db_group_search = db(db.auth_group.id.belongs(group_ids))
db_groups_of_the_user = list() db_groups_of_the_user = list()
db_group_id = dict() db_group_id = dict()
+439 -421
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -67,11 +67,12 @@ def RedisCache(*args, **vars):
locker.acquire() locker.acquire()
try: try:
if not hasattr(RedisCache, 'redis_instance'): instance_name = 'redis_instance_' + current.request.application
RedisCache.redis_instance = RedisClient(*args, **vars) if not hasattr(RedisCache, instance_name):
setattr(RedisCache, instance_name, RedisClient(*args, **vars))
return getattr(RedisCache, instance_name)
finally: finally:
locker.release() locker.release()
return RedisCache.redis_instance
class RedisClient(object): class RedisClient(object):
+1 -1
View File
@@ -126,7 +126,7 @@ class History:
def globals_dict(self): def globals_dict(self):
"""Returns a dictionary view of the globals. """Returns a dictionary view of the globals.
""" """
return dict((name, cPickle.loads(val)) return dict((name, pickle.loads(val))
for name, val in zip(self.global_names, self.globals)) for name, val in zip(self.global_names, self.globals))
def add_unpicklable(self, statement, names): def add_unpicklable(self, statement, names):
+35 -35
View File
@@ -4,7 +4,24 @@ from hashlib import sha1
class Stripe: class Stripe:
""" """
Usage: Use in WEB2PY (guaranteed PCI compliant)
def pay():
from gluon.contrib.stripe import StripeForm
form = StripeForm(
pk=STRIPE_PUBLISHABLE_KEY,
sk=STRIPE_SECRET_KEY,
amount=150, # $1.5 (amount is in cents)
description="Nothing").process()
if form.accepted:
payment_id = form.response['id']
redirect(URL('thank_you'))
elif form.errors:
redirect(URL('pay_error'))
return dict(form=form)
Low level API:
key='<api key>' key='<api key>'
d = Stripe(key).charge( d = Stripe(key).charge(
amount=100, # 1 dollar!!!! amount=100, # 1 dollar!!!!
@@ -22,22 +39,6 @@ class Stripe:
{u'fee': 0, u'description': u'test charge', u'created': 1321242072, u'refunded': False, u'livemode': False, u'object': u'charge', u'currency': u'usd', u'amount': 100, u'paid': True, u'id': u'ch_sdjasgfga83asf', u'card': {u'exp_month': 5, u'country': u'US', u'object': u'card', u'last4': u'4242', u'exp_year': 2012, u'type': u'Visa'}} {u'fee': 0, u'description': u'test charge', u'created': 1321242072, u'refunded': False, u'livemode': False, u'object': u'charge', u'currency': u'usd', u'amount': 100, u'paid': True, u'id': u'ch_sdjasgfga83asf', u'card': {u'exp_month': 5, u'country': u'US', u'object': u'card', u'last4': u'4242', u'exp_year': 2012, u'type': u'Visa'}}
if paid is True than transaction was processed if paid is True than transaction was processed
Use in WEB2PY (guaranteed PCI compliant)
def pay():
from gluon.contrib.stripe import StripeForm
form = StripeForm(
pk=STRIPE_PUBLISHABLE_KEY,
sk=STRIPE_SECRET_KEY,
amount=150, # $1.5 (amount is in cents)
description="Nothing").process()
if form.accepted:
payment_id = form.response['id']
redirect(URL('thank_you'))
elif form.errors:
redirect(URL('pay_error'))
return dict(form=form)
""" """
URL_CHARGE = 'https://%s:@api.stripe.com/v1/charges' URL_CHARGE = 'https://%s:@api.stripe.com/v1/charges'
@@ -188,37 +189,36 @@ jQuery(function(){
<h3>Payment Amount: {{=currency_symbol}} {{="%.2f" % (0.01*amount)}}</h3> <h3>Payment Amount: {{=currency_symbol}} {{="%.2f" % (0.01*amount)}}</h3>
<form action="" method="POST" id="payment-form" class="form-horizontal"> <form action="" method="POST" id="payment-form" class="form-horizontal">
<div class="form-row control-group"> <div class="form-row form-group">
<label class="control-label">Card Number</label> <label class="col-sm-2 control-label">Card Number</label>
<div class="controls"> <div class="controls col-sm-10">
<input type="text" size="20" data-stripe="number" <input type="text" size="20" data-stripe="number"
placeholder="4242424242424242"/> placeholder="4242424242424242" class="form-control"/>
</div> </div>
</div> </div>
<div class="form-row control-group"> <div class="form-row form-group">
<label class="control-label">CVC</label> <label class="col-sm-2 control-label">CVC</label>
<div class="controls"> <div class="controls col-sm-10">
<input type="text" size="4" style="width:80px" data-stripe="cvc" <input type="text" size="4" style="width:80px" data-stripe="cvc"
placeholder="XXX"/> placeholder="XXX" class="form-control"/>
<a href="http://en.wikipedia.org/wiki/Card_Verification_Code" target="_blank">What is this?</a> <a href="http://en.wikipedia.org/wiki/Card_Verification_Code" target="_blank">What is this?</a>
</div> </div>
</div> </div>
<div class="form-row control-group"> <div class="form-row form-group">
<label class="control-label">Expiration</label> <label class="col-sm-2 control-label">Expiration</label>
<div class="controls"> <div class="controls col-sm-10">
<input type="text" size="2" style="width:40px" data-stripe="exp-month" <input type="text" size="2" style="width:40px; display:inline-block"
placeholder="MM"/> data-stripe="exp-month" placeholder="MM" class="form-control"/>
/ /
<input type="text" size="4" style="width:80px" data-stripe="exp-year" <input type="text" size="4" style="width:80px; display:inline-block"
placeholder="YYYY"/> data-stripe="exp-year" placeholder="YYYY" class="form-control"/>
</div> </div>
</div> </div>
<div class="form-row form-group">
<div class="control-group"> <div class="controls col-sm-offset-2 col-sm-10">
<div class="controls">
<button type="submit" class="btn btn-primary">Submit Payment</button> <button type="submit" class="btn btn-primary">Submit Payment</button>
<div class="payment-errors error hidden"></div> <div class="payment-errors error hidden"></div>
</div> </div>
+5 -5
View File
@@ -7,14 +7,14 @@
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
Takes care of adapting pyDAL to web2py's needs Takes care of adapting pyDAL to web2py's needs
-------------------------------------------- -----------------------------------------------
""" """
from pydal import DAL as DAL from pydal import DAL as DAL
from pydal import Field from pydal import Field
from pydal.objects import Row, Rows, Table, Query, Expression from pydal.objects import Row, Rows, Table, Query, Set, Expression
from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon from pydal import SQLCustomType, geoPoint, geoLine, geoPolygon
import copy_reg as copyreg
def _default_validators(db, field): def _default_validators(db, field):
""" """
@@ -81,12 +81,12 @@ def _default_validators(db, field):
requires[0] = validators.IS_EMPTY_OR(requires[0]) requires[0] = validators.IS_EMPTY_OR(requires[0])
return requires return requires
from gluon import serializers as w2p_serializers from gluon.serializers import custom_json, xml
from gluon.utils import web2py_uuid from gluon.utils import web2py_uuid
from gluon import sqlhtml from gluon import sqlhtml
DAL.serializers = w2p_serializers DAL.serializers = {'json': custom_json, 'xml': xml}
DAL.validators_method = _default_validators DAL.validators_method = _default_validators
DAL.uuid = lambda x: web2py_uuid() DAL.uuid = lambda x: web2py_uuid()
DAL.representers = { DAL.representers = {
+48 -32
View File
@@ -26,6 +26,8 @@ import gluon.settings as settings
from gluon.utils import web2py_uuid, secure_dumps, secure_loads from gluon.utils import web2py_uuid, secure_dumps, secure_loads
from gluon.settings import global_settings from gluon.settings import global_settings
from gluon import recfile from gluon import recfile
from gluon.cache import CacheInRam
from gluon.fileutils import copystream
import hashlib import hashlib
import portalocker import portalocker
try: try:
@@ -47,8 +49,7 @@ import cgi
import urlparse import urlparse
import copy import copy
import tempfile import tempfile
from gluon.cache import CacheInRam
from gluon.fileutils import copystream
FMT = '%a, %d-%b-%Y %H:%M:%S PST' FMT = '%a, %d-%b-%Y %H:%M:%S PST'
PAST = 'Sat, 1-Jan-1971 00:00:00' PAST = 'Sat, 1-Jan-1971 00:00:00'
@@ -82,13 +83,22 @@ less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />'
css_inline = '<style type="text/css">\n%s\n</style>' css_inline = '<style type="text/css">\n%s\n</style>'
js_inline = '<script type="text/javascript">\n%s\n</script>' js_inline = '<script type="text/javascript">\n%s\n</script>'
template_mapping = {
'css': css_template,
'js': js_template,
'coffee': coffee_template,
'ts': typescript_template,
'less': less_template,
'css:inline': css_inline,
'js:inline': js_inline
}
# IMPORTANT: # IMPORTANT:
# this is required so that pickled dict(s) and class.__dict__ # this is required so that pickled dict(s) and class.__dict__
# are sorted and web2py can detect without ambiguity when a session changes # are sorted and web2py can detect without ambiguity when a session changes
class SortingPickler(Pickler): class SortingPickler(Pickler):
def save_dict(self, obj): def save_dict(self, obj):
self.write(EMPTY_DICT if self.bin else MARK+DICT) self.write(EMPTY_DICT if self.bin else MARK + DICT)
self.memoize(obj) self.memoize(obj)
self._batch_setitems([(key, obj[key]) for key in sorted(obj)]) self._batch_setitems([(key, obj[key]) for key in sorted(obj)])
@@ -193,6 +203,7 @@ class Request(Storage):
self.is_https = False self.is_https = False
self.is_local = False self.is_local = False
self.global_settings = settings.global_settings self.global_settings = settings.global_settings
self._uuid = None
def parse_get_vars(self): def parse_get_vars(self):
"""Takes the QUERY_STRING and unpacks it to get_vars """Takes the QUERY_STRING and unpacks it to get_vars
@@ -275,7 +286,7 @@ class Request(Storage):
""" """
self._vars = copy.copy(self.get_vars) self._vars = copy.copy(self.get_vars)
for key, value in self.post_vars.iteritems(): for key, value in self.post_vars.iteritems():
if not key in self._vars: if key not in self._vars:
self._vars[key] = value self._vars[key] = value
else: else:
if not isinstance(self._vars[key], list): if not isinstance(self._vars[key], list):
@@ -306,13 +317,21 @@ class Request(Storage):
self.parse_all_vars() self.parse_all_vars()
return self._vars return self._vars
@property
def uuid(self):
"""Lazily uuid
"""
if self._uuid is None:
self.compute_uuid()
return self._uuid
def compute_uuid(self): def compute_uuid(self):
self.uuid = '%s/%s.%s.%s' % ( self._uuid = '%s/%s.%s.%s' % (
self.application, self.application,
self.client.replace(':', '_'), self.client.replace(':', '_'),
self.now.strftime('%Y-%m-%d.%H-%M-%S'), self.now.strftime('%Y-%m-%d.%H-%M-%S'),
web2py_uuid()) web2py_uuid())
return self.uuid return self._uuid
def user_agent(self): def user_agent(self):
from gluon.contrib import user_agent_parser from gluon.contrib import user_agent_parser
@@ -436,29 +455,32 @@ class Response(Storage):
return page return page
def include_meta(self): def include_meta(self):
s = "\n"; s = "\n"
for meta in (self.meta or {}).iteritems(): for meta in (self.meta or {}).iteritems():
k, v = meta k, v = meta
if isinstance(v,dict): if isinstance(v, dict):
s = s+'<meta'+''.join(' %s="%s"' % (xmlescape(key), xmlescape(v[key])) for key in v) +' />\n' s += '<meta' + ''.join(' %s="%s"' % (xmlescape(key), xmlescape(v[key])) for key in v) +' />\n'
else: else:
s = s+'<meta name="%s" content="%s" />\n' % (k, xmlescape(v)) s += '<meta name="%s" content="%s" />\n' % (k, xmlescape(v))
self.write(s, escape=False) self.write(s, escape=False)
def include_files(self, extensions=None): def include_files(self, extensions=None):
""" """
Caching method for writing out files. Includes files (usually in the head).
Can minify and cache local files
By default, caches in ram for 5 minutes. To change, By default, caches in ram for 5 minutes. To change,
response.cache_includes = (cache_method, time_expire). response.cache_includes = (cache_method, time_expire).
Example: (cache.disk, 60) # caches to disk for 1 minute. Example: (cache.disk, 60) # caches to disk for 1 minute.
""" """
from gluon import URL
files = [] files = []
ext_files = []
has_js = has_css = False has_js = has_css = False
for item in self.files: for item in self.files:
if extensions and not item.split('.')[-1] in extensions: if isinstance(item, (list, tuple)):
ext_files.append(item)
continue
if extensions and not item.rpartition('.')[2] in extensions:
continue continue
if item in files: if item in files:
continue continue
@@ -487,10 +509,13 @@ class Response(Storage):
time_expire) time_expire)
else: else:
files = call_minify() files = call_minify()
s = ''
files.extend(ext_files)
s = []
for item in files: for item in files:
if isinstance(item, str): if isinstance(item, str):
f = item.lower().split('?')[0] f = item.lower().split('?')[0]
ext = f.rpartition('.')[2]
# if static_version we need also to check for # if static_version we need also to check for
# static_version_urls. In that case, the _.x.x.x # static_version_urls. In that case, the _.x.x.x
# bit would have already been added by the URL() # bit would have already been added by the URL()
@@ -498,24 +523,15 @@ class Response(Storage):
if self.static_version and not self.static_version_urls: if self.static_version and not self.static_version_urls:
item = item.replace( item = item.replace(
'/static/', '/static/_%s/' % self.static_version, 1) '/static/', '/static/_%s/' % self.static_version, 1)
if f.endswith('.css'): tmpl = template_mapping.get(ext)
s += css_template % item if tmpl:
elif f.endswith('.js'): s.append(tmpl % item)
s += js_template % item
elif f.endswith('.coffee'):
s += coffee_template % item
elif f.endswith('.ts'):
# http://www.typescriptlang.org/
s += typescript_template % item
elif f.endswith('.less'):
s += less_template % item
elif isinstance(item, (list, tuple)): elif isinstance(item, (list, tuple)):
f = item[0] f = item[0]
if f == 'css:inline': tmpl = template_mapping.get(f)
s += css_inline % item[1] if tmpl:
elif f == 'js:inline': s.append(tmpl % item[1])
s += js_inline % item[1] self.write(''.join(s), escape=False)
self.write(s, escape=False)
def stream(self, def stream(self,
stream, stream,
@@ -663,7 +679,7 @@ class Response(Storage):
return handler(request, self, methods) return handler(request, self, methods)
def toolbar(self): def toolbar(self):
from html import DIV, SCRIPT, BEAUTIFY, TAG, URL, A from gluon.html import DIV, SCRIPT, BEAUTIFY, TAG, A
BUTTON = TAG.button BUTTON = TAG.button
admin = URL("admin", "default", "design", extension='html', admin = URL("admin", "default", "design", extension='html',
args=current.request.application) args=current.request.application)
-1
View File
@@ -376,7 +376,6 @@ def wsgibase(environ, responder):
request.env.http_x_forwarded_proto in HTTPS_SCHEMES \ request.env.http_x_forwarded_proto in HTTPS_SCHEMES \
or env.https == 'on' or env.https == 'on'
) )
request.compute_uuid() # requires client
request.url = environ['PATH_INFO'] request.url = environ['PATH_INFO']
# ################################################## # ##################################################
+1 -1
View File
@@ -119,7 +119,7 @@ class LockedFile(object):
lock(self.file, LOCK_EX) lock(self.file, LOCK_EX)
if not 'a' in mode: if not 'a' in mode:
self.file.seek(0) self.file.seek(0)
self.file.truncate() self.file.truncate(0)
else: else:
raise RuntimeError("invalid LockedFile(...,mode)") raise RuntimeError("invalid LockedFile(...,mode)")
+19 -15
View File
@@ -66,14 +66,15 @@ class XssCleaner(HTMLParser):
#to strip or escape disallowed tags? #to strip or escape disallowed tags?
self.strip_disallowed = strip_disallowed self.strip_disallowed = strip_disallowed
self.in_disallowed = False # there might be data after final closing tag, that is to be ignored
self.in_disallowed = [False]
def handle_data(self, data): def handle_data(self, data):
if data and not self.in_disallowed: if data and not self.in_disallowed[-1]:
self.result += xssescape(data) self.result += xssescape(data)
def handle_charref(self, ref): def handle_charref(self, ref):
if self.in_disallowed: if self.in_disallowed[-1]:
return return
elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe
self.result += '&#%s;' % ref self.result += '&#%s;' % ref
@@ -81,7 +82,7 @@ class XssCleaner(HTMLParser):
self.result += xssescape('&#%s' % ref) self.result += xssescape('&#%s' % ref)
def handle_entityref(self, ref): def handle_entityref(self, ref):
if self.in_disallowed: if self.in_disallowed[-1]:
return return
elif ref in entitydefs: elif ref in entitydefs:
self.result += '&%s;' % ref self.result += '&%s;' % ref
@@ -89,7 +90,7 @@ class XssCleaner(HTMLParser):
self.result += xssescape('&%s' % ref) self.result += xssescape('&%s' % ref)
def handle_comment(self, comment): def handle_comment(self, comment):
if self.in_disallowed: if self.in_disallowed[-1]:
return return
elif comment: elif comment:
self.result += xssescape('<!--%s-->' % comment) self.result += xssescape('<!--%s-->' % comment)
@@ -100,11 +101,11 @@ class XssCleaner(HTMLParser):
attrs attrs
): ):
if tag not in self.permitted_tags: if tag not in self.permitted_tags:
if self.strip_disallowed: self.in_disallowed.append(True)
self.in_disallowed = True if (not self.strip_disallowed):
else:
self.result += xssescape('<%s>' % tag) self.result += xssescape('<%s>' % tag)
else: else:
self.in_disallowed.append(False)
bt = '<' + tag bt = '<' + tag
if tag in self.allowed_attributes: if tag in self.allowed_attributes:
attrs = dict(attrs) attrs = dict(attrs)
@@ -119,6 +120,7 @@ class XssCleaner(HTMLParser):
else: else:
bt += ' %s=%s' % (xssescape(attribute), bt += ' %s=%s' % (xssescape(attribute),
quoteattr(attrs[attribute])) quoteattr(attrs[attribute]))
# deal with <a> without href and <img> without src
if bt == '<a' or bt == '<img': if bt == '<a' or bt == '<img':
return return
if tag in self.requires_no_close: if tag in self.requires_no_close:
@@ -129,10 +131,9 @@ class XssCleaner(HTMLParser):
def handle_endtag(self, tag): def handle_endtag(self, tag):
bracketed = '</%s>' % tag bracketed = '</%s>' % tag
self.in_disallowed.pop()
if tag not in self.permitted_tags: if tag not in self.permitted_tags:
if self.strip_disallowed: if (not self.strip_disallowed):
self.in_disallowed = False
else:
self.result += xssescape(bracketed) self.result += xssescape(bracketed)
elif tag in self.open_tags: elif tag in self.open_tags:
self.result += bracketed self.result += bracketed
@@ -143,10 +144,13 @@ class XssCleaner(HTMLParser):
Accepts relative, absolute, and mailto urls Accepts relative, absolute, and mailto urls
""" """
parsed = urlparse(url) if url.startswith('#'):
return (parsed[0] in self.allowed_schemes and '.' in parsed[1]) \ return True
or (parsed[0] in self.allowed_schemes and '@' in parsed[2]) \ else:
or (parsed[0] == '' and parsed[2].startswith('/')) parsed = 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('/')))
def strip(self, rawstring, escape=True): def strip(self, rawstring, escape=True):
""" """
+1 -3
View File
@@ -41,9 +41,7 @@ def enable_autocomplete_and_history(adir, env):
except ImportError: except ImportError:
pass pass
else: else:
readline.parse_and_bind("bind ^I rl_complete" readline.parse_and_bind("tab: complete")
if sys.platform == 'darwin'
else "tab: complete")
history_file = os.path.join(adir, '.pythonhistory') history_file = os.path.join(adir, '.pythonhistory')
try: try:
readline.read_history_file(history_file) readline.read_history_file(history_file)
+57 -40
View File
@@ -58,9 +58,12 @@ def represent(field, value, record):
f = field.represent f = field.represent
if not callable(f): if not callable(f):
return str(value) return str(value)
n = f.func_code.co_argcount - len(f.func_defaults or []) if hasattr(f,'func_code'):
if getattr(f, 'im_self', None): n = f.func_code.co_argcount - len(f.func_defaults or [])
n -= 1 if getattr(f, 'im_self', None):
n -= 1
else:
n = 1
if n == 1: if n == 1:
return f(value) return f(value)
elif n == 2: elif n == 2:
@@ -856,14 +859,14 @@ def formstyle_bootstrap3_stacked(form, fields):
label = '' label = ''
elif isinstance(controls, (SELECT, TEXTAREA)): elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control') controls.add_class('form-control')
elif isinstance(controls, SPAN): elif isinstance(controls, SPAN):
_controls = P(controls.components) _controls = P(controls.components)
elif isinstance(controls, UL): elif isinstance(controls, UL):
for e in controls.elements("input"): for e in controls.elements("input"):
e.add_class('form-control') e.add_class('form-control')
if isinstance(label, LABEL): if isinstance(label, LABEL):
label['_class'] = 'control-label' label['_class'] = 'control-label'
@@ -906,9 +909,9 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3):
label = '' label = ''
elif isinstance(controls, (SELECT, TEXTAREA)): elif isinstance(controls, (SELECT, TEXTAREA)):
controls.add_class('form-control') controls.add_class('form-control')
elif isinstance(controls, SPAN): elif isinstance(controls, SPAN):
_controls = P(controls.components, _controls = P(controls.components,
_class="form-control-static %s" % col_class) _class="form-control-static %s" % col_class)
elif isinstance(controls, UL): elif isinstance(controls, UL):
for e in controls.elements("input"): for e in controls.elements("input"):
@@ -1205,6 +1208,9 @@ class SQLFORM(FORM):
elif field.type == 'boolean': elif field.type == 'boolean':
inp = self.widgets.boolean.widget( inp = self.widgets.boolean.widget(
field, default, _disabled=True) field, default, _disabled=True)
elif isinstance(field.type, SQLCustomType) and callable(field.type.represent):
# SQLCustomType has a represent, use it
inp = field.type.represent(default, record)
else: else:
inp = field.formatter(default) inp = field.formatter(default)
if getattr(field, 'show_if', None): if getattr(field, 'show_if', None):
@@ -1246,6 +1252,9 @@ class SQLFORM(FORM):
dspval = '' dspval = ''
elif field.type == 'blob': elif field.type == 'blob':
continue continue
elif isinstance(field.type, SQLCustomType) and callable(field.type.widget):
# SQLCustomType has a widget, use it
inp = field.type.widget(field, default)
else: else:
field_type = widget_class.match(str(field.type)).group() field_type = widget_class.match(str(field.type)).group()
field_type = field_type in self.widgets and field_type or 'string' field_type = field_type in self.widgets and field_type or 'string'
@@ -1682,6 +1691,7 @@ class SQLFORM(FORM):
AUTOTYPES = { AUTOTYPES = {
type(''): ('string', None), type(''): ('string', None),
type(u''): ('string',None),
type(True): ('boolean', None), type(True): ('boolean', None),
type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)), type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
type(1.0): ('double', IS_FLOAT_IN_RANGE()), type(1.0): ('double', IS_FLOAT_IN_RANGE()),
@@ -1746,10 +1756,16 @@ class SQLFORM(FORM):
keywords = keywords[0] keywords = keywords[0]
request.vars.keywords = keywords request.vars.keywords = keywords
key = keywords.strip() key = keywords.strip()
if key and ' ' not in key and not '"' in key and not "'" in key: if key and not '"' in key:
SEARCHABLE_TYPES = ('string', 'text', 'list:string') SEARCHABLE_TYPES = ('string', 'text', 'list:string')
parts = [field.contains( sfields = [field for field in fields if field.type in SEARCHABLE_TYPES]
key) for field in fields if field.type in SEARCHABLE_TYPES] if settings.global_settings.web2py_runtime_gae:
return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields])
else:
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 # from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4
# needs testing... # needs testing...
@@ -1763,10 +1779,6 @@ class SQLFORM(FORM):
# filters.append(reduce(lambda a, b: (a & b), all_words_filters)) # filters.append(reduce(lambda a, b: (a & b), all_words_filters))
#parts = filters #parts = filters
else:
parts = None
if parts:
return reduce(lambda a, b: a | b, parts)
else: else:
return smart_query(fields, key) return smart_query(fields, key)
@@ -1829,15 +1841,19 @@ class SQLFORM(FORM):
operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control') operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control')
_id = "%s_%s" % (value_id, name) _id = "%s_%s" % (value_id, name)
if field_type in ['boolean', 'double', 'time', 'integer']: if field_type in ['boolean', 'double', 'time', 'integer']:
value_input = SQLFORM.widgets[field_type].widget(field, field.default, _id=_id, _class='form-control') widget_ = SQLFORM.widgets[field_type]
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control')
elif field_type == 'date': elif field_type == 'date':
iso_format = {'_data-w2p_date_format' : '%Y-%m-%d'} iso_format = {'_data-w2p_date_format': '%Y-%m-%d'}
value_input = SQLFORM.widgets.date.widget(field, field.default, _id=_id, _class='form-control', **iso_format) widget_ = SQLFORM.widgets.date
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
elif field_type == 'datetime': elif field_type == 'datetime':
iso_format = {'_data-w2p_datetime_format' : '%Y-%m-%d %H:%M:%S'} iso_format = {'_data-w2p_datetime_format': '%Y-%m-%d %H:%M:%S'}
value_input = SQLFORM.widgets.datetime.widget(field, field.default, _id=_id, _class='form-control', **iso_format) widget_ = SQLFORM.widgets.datetime
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
elif (field_type.startswith('reference ') or elif (field_type.startswith('reference ') or
field_type.startswith('list:reference ')) and \ field_type.startswith('list:reference ')) and \
hasattr(field.requires, 'options') or \
hasattr(field.requires, 'options'): hasattr(field.requires, 'options'):
value_input = SELECT( value_input = SELECT(
*[OPTION(v, _value=k) *[OPTION(v, _value=k)
@@ -1847,7 +1863,8 @@ class SQLFORM(FORM):
elif field_type.startswith('reference ') or \ elif field_type.startswith('reference ') or \
field_type.startswith('list:integer') or \ field_type.startswith('list:integer') or \
field_type.startswith('list:reference '): field_type.startswith('list:reference '):
value_input = SQLFORM.widgets.integer.widget(field, field.default, _id=_id, _class='form-control') widget_ = SQLFORM.widgets.integer
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control')
else: else:
value_input = INPUT( value_input = INPUT(
_type='text', _id=_id, _type='text', _id=_id,
@@ -1958,7 +1975,8 @@ class SQLFORM(FORM):
cache_count=None, cache_count=None,
client_side_delete=False, client_side_delete=False,
ignore_common_filters=None, ignore_common_filters=None,
auto_pagination=True): auto_pagination=True,
use_cursor=False):
formstyle = formstyle or current.response.formstyle formstyle = formstyle or current.response.formstyle
@@ -2060,18 +2078,15 @@ class SQLFORM(FORM):
# is unique and usually indexed. See issue #679 # is unique and usually indexed. See issue #679
if not orderby: if not orderby:
orderby = field_id orderby = field_id
else: elif isinstance(orderby, list):
if isinstance(orderby, Expression): orderby = reduce(lambda a,b: a|b, orderby)
if orderby.first: elif isinstance(orderby, Field) and orderby is not field_id:
# here we're with a DESC order on a field # here we're with an ASC order on a field stored as orderby
# stored as orderby.first orderby = orderby | field_id
if orderby.first is not field_id: elif (isinstance(orderby, Expression) and
orderby = orderby | field_id orderby.first and orderby.first is not field_id):
else: # here we're with a DESC order on a field stored as orderby.first
# here we're with an ASC order on a field orderby = orderby | field_id
# stored as orderby
if orderby is not field_id:
orderby = orderby | field_id
return orderby return orderby
def url(**b): def url(**b):
@@ -2099,10 +2114,8 @@ class SQLFORM(FORM):
# - url has valid signature (vars are not signed, only path_info) # - url has valid signature (vars are not signed, only path_info)
# = url does not contain 'create','delete','edit' (readonly) # = url does not contain 'create','delete','edit' (readonly)
if user_signature: if user_signature:
if not ( if not ('/'.join(map(str,args)) == '/'.join(map(str,request.args)) or
'/'.join(str(a) for a in args) == '/'.join(request.args) or URL.verify(request, user_signature=user_signature, hash_vars=False) or
URL.verify(request, user_signature=user_signature,
hash_vars=False) or
(request.args(len(args)) == 'view' and not logged)): (request.args(len(args)) == 'view' and not logged)):
session.flash = T('not authorized') session.flash = T('not authorized')
redirect(referrer) redirect(referrer)
@@ -2533,7 +2546,7 @@ class SQLFORM(FORM):
cursor = True cursor = True
# figure out what page we are one to setup the limitby # figure out what page we are one to setup the limitby
if paginate and dbset._db._adapter.dbengine == 'google:datastore': if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
cursor = request.vars.cursor or True cursor = request.vars.cursor or True
limitby = (0, paginate) limitby = (0, paginate)
try: try:
@@ -2555,7 +2568,7 @@ class SQLFORM(FORM):
table_fields = [field for field in fields table_fields = [field for field in fields
if (field.tablename in tablenames and if (field.tablename in tablenames and
not(isinstance(field, Field.Virtual)))] not(isinstance(field, Field.Virtual)))]
if dbset._db._adapter.dbengine == 'google:datastore': if dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
rows = dbset.select(left=left, orderby=orderby, rows = dbset.select(left=left, orderby=orderby,
groupby=groupby, limitby=limitby, groupby=groupby, limitby=limitby,
reusecursor=cursor, reusecursor=cursor,
@@ -2565,6 +2578,7 @@ class SQLFORM(FORM):
rows = dbset.select(left=left, orderby=orderby, rows = dbset.select(left=left, orderby=orderby,
groupby=groupby, limitby=limitby, groupby=groupby, limitby=limitby,
cacheable=True, *table_fields) cacheable=True, *table_fields)
next_cursor = None
except SyntaxError: except SyntaxError:
rows = None rows = None
next_cursor = None next_cursor = None
@@ -2583,7 +2597,7 @@ class SQLFORM(FORM):
console.append(DIV(message or '', _class='web2py_counter')) console.append(DIV(message or '', _class='web2py_counter'))
paginator = UL() paginator = UL()
if paginate and dbset._db._adapter.dbengine == 'google:datastore': if paginate and dbset._db._adapter.dbengine == 'google:datastore' and use_cursor:
# this means we may have a large table with an unknown number of rows. # this means we may have a large table with an unknown number of rows.
try: try:
page = int(request.vars.page or 1) - 1 page = int(request.vars.page or 1) - 1
@@ -2708,6 +2722,9 @@ class SQLFORM(FORM):
_href='%s/%s' % (upload, value)) _href='%s/%s' % (upload, value))
else: else:
value = '' value = ''
elif isinstance(field.type, SQLCustomType) and callable(field.type.represent):
# SQLCustomType has a represent, use it
value = field.type.represent(value, row)
if isinstance(value, str): if isinstance(value, str):
value = truncate_string(value, maxlength) value = truncate_string(value, maxlength)
elif not isinstance(value, XmlComponent): elif not isinstance(value, XmlComponent):
+31 -28
View File
@@ -269,31 +269,33 @@ class FastStorage(dict):
class List(list): class List(list):
""" """
Like a regular python list but a[i] if i is out of bounds returns None Like a regular python list but callable.
instead of `IndexOutOfBounds` When a(i) is called if i is out of bounds returns None
instead of `IndexError`.
""" """
def __call__(self, i, default=DEFAULT, cast=None, otherwise=None): def __call__(self, i, default=DEFAULT, cast=None, otherwise=None):
"""Allows to use a special syntax for fast-check of `request.args()` """Allows to use a special syntax for fast-check of
validity `request.args()` validity.
:params:
Args:
i: index i: index
default: use this value if arg not found default: use this value if arg not found
cast: type cast cast: type cast
otherwise: can be: otherwise:
will be executed when:
- None: results in a 404 - casts fail
- str: redirect to this address - value not found, dont have default and otherwise is
- callable: calls the function (nothing is passed) especified
can be:
- None: results in a 404
- str: redirect to this address
- callable: calls the function (nothing is passed)
Example: Example:
You can use:: You can use::
request.args(0,default=0,cast=int,otherwise='http://error_url') request.args(0,default=0,cast=int,otherwise='http://error_url')
request.args(0,default=0,cast=int,otherwise=lambda:...) request.args(0,default=0,cast=int,otherwise=lambda:...)
""" """
n = len(self) n = len(self)
if 0 <= i < n or -n <= i < 0: if 0 <= i < n or -n <= i < 0:
@@ -301,23 +303,24 @@ class List(list):
elif default is DEFAULT: elif default is DEFAULT:
value = None value = None
else: else:
value, cast = default, False value, cast, otherwise = default, False, False
if cast: try:
try: if cast:
value = cast(value) value = cast(value)
except (ValueError, TypeError): if not value and otherwise:
from http import HTTP, redirect raise ValueError('Otherwise will raised.')
if otherwise is None: except (ValueError, TypeError):
raise HTTP(404) from http import HTTP, redirect
elif isinstance(otherwise, str): if otherwise is None:
redirect(otherwise) raise HTTP(404)
elif callable(otherwise): elif isinstance(otherwise, str):
return otherwise() redirect(otherwise)
else: elif callable(otherwise):
raise RuntimeError("invalid otherwise") return otherwise()
else:
raise RuntimeError("invalid otherwise")
return value return value
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
doctest.testmod() doctest.testmod()
+1
View File
@@ -4,6 +4,7 @@ from test_http import *
from test_cache import * from test_cache import *
from test_contenttype import * from test_contenttype import *
from test_fileutils import * from test_fileutils import *
from test_globals import *
from test_html import * from test_html import *
from test_is_url import * from test_is_url import *
from test_languages import * from test_languages import *
+1
View File
@@ -26,6 +26,7 @@ exclude_lines =
ignore_errors = True ignore_errors = True
omit = gluon/contrib/* omit = gluon/contrib/*
gluon/tests/* gluon/tests/*
gluon/packages/*
[html] [html]
directory = coverage_html_report directory = coverage_html_report
+34 -1
View File
@@ -13,6 +13,7 @@ fix_sys_path(__file__)
from storage import Storage from storage import Storage
from cache import CacheInRam, CacheOnDisk, Cache from cache import CacheInRam, CacheOnDisk, Cache
from gluon.dal import DAL, Field
oldcwd = None oldcwd = None
@@ -30,6 +31,11 @@ def tearDownModule():
if oldcwd: if oldcwd:
os.chdir(oldcwd) os.chdir(oldcwd)
oldcwd = None oldcwd = None
try:
os.unlink('dummy.db')
except:
pass
class TestCache(unittest.TestCase): class TestCache(unittest.TestCase):
@@ -107,7 +113,34 @@ class TestCache(unittest.TestCase):
cache.clear(regex=r'a*') cache.clear(regex=r'a*')
self.assertEqual(cache('a1', lambda: 2, 0), 2) self.assertEqual(cache('a1', lambda: 2, 0), 2)
self.assertEqual(cache('a2', lambda: 3, 100), 3) self.assertEqual(cache('a2', lambda: 3, 100), 3)
return
def testDALcache(self):
s = Storage({'application': 'admin',
'folder': 'applications/admin'})
cache = Cache(s)
db = DAL(check_reserved=['all'])
db.define_table('t_a', Field('f_a'))
db.t_a.insert(f_a='test')
db.commit()
a = db(db.t_a.id > 0).select(cache=(cache.ram, 60), cacheable=True)
b = db(db.t_a.id > 0).select(cache=(cache.ram, 60), cacheable=True)
self.assertEqual(a.as_csv(), b.as_csv())
c = db(db.t_a.id > 0).select(cache=(cache.disk, 60), cacheable=True)
d = db(db.t_a.id > 0).select(cache=(cache.disk, 60), cacheable=True)
self.assertEqual(c.as_csv(), d.as_csv())
self.assertEqual(a.as_csv(), c.as_csv())
self.assertEqual(b.as_csv(), d.as_csv())
e = db(db.t_a.id > 0).select(cache=(cache.disk, 60))
f = db(db.t_a.id > 0).select(cache=(cache.disk, 60))
self.assertEqual(e.as_csv(), f.as_csv())
self.assertEqual(a.as_csv(), f.as_csv())
g = db(db.t_a.id > 0).select(cache=(cache.ram, 60))
h = db(db.t_a.id > 0).select(cache=(cache.ram, 60))
self.assertEqual(g.as_csv(), h.as_csv())
self.assertEqual(a.as_csv(), h.as_csv())
db.t_a.drop()
db.close()
if __name__ == '__main__': if __name__ == '__main__':
setUpModule() # pre-python-2.7 setUpModule() # pre-python-2.7
+19 -3
View File
@@ -4,6 +4,7 @@
Unit tests for gluon.dal Unit tests for gluon.dal
""" """
import os
import unittest import unittest
from fix_path import fix_sys_path from fix_path import fix_sys_path
@@ -12,28 +13,43 @@ fix_sys_path(__file__)
from gluon.dal import DAL, Field from gluon.dal import DAL, Field
def tearDownModule():
try:
os.unlink('dummy.db')
except:
pass
class TestDALSubclass(unittest.TestCase): class TestDALSubclass(unittest.TestCase):
def testRun(self): def testRun(self):
import gluon.serializers as mserializers from gluon.serializers import custom_json, xml
from gluon import sqlhtml from gluon import sqlhtml
db = DAL(check_reserved=['all']) db = DAL(check_reserved=['all'])
self.assertEqual(db.serializers, mserializers) self.assertEqual(db.serializers['json'], custom_json)
self.assertEqual(db.serializers['xml'], xml)
self.assertEqual(db.representers['rows_render'], sqlhtml.represent) self.assertEqual(db.representers['rows_render'], sqlhtml.represent)
self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE) self.assertEqual(db.representers['rows_xml'], sqlhtml.SQLTABLE)
db.close()
def testSerialization(self): def testSerialization(self):
import pickle import pickle
db = DAL(check_reserved=['all']) db = DAL(check_reserved=['all'])
db.define_table('t_a', Field('f_a')) db.define_table('t_a', Field('f_a'))
db.t_a.insert(f_a='test') db.t_a.insert(f_a='test')
a = db(db.t_a.id>0).select(cacheable=True) a = db(db.t_a.id > 0).select(cacheable=True)
s = pickle.dumps(a) s = pickle.dumps(a)
b = pickle.loads(s) b = pickle.loads(s)
self.assertEqual(a.db, b.db) self.assertEqual(a.db, b.db)
db.t_a.drop()
db.close()
""" TODO: """ TODO:
class TestDefaultValidators(unittest.TestCase): class TestDefaultValidators(unittest.TestCase):
def testRun(self): def testRun(self):
pass pass
""" """
if __name__ == '__main__':
unittest.main()
tearDownModule()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Unit tests for gluon.globals
"""
import unittest
from fix_path import fix_sys_path
fix_sys_path(__file__)
from gluon.globals import Response
from gluon import URL
class testResponse(unittest.TestCase):
def test_include_files(self):
def return_includes(response, extensions=None):
response.include_files(extensions)
return response.body.getvalue()
response = Response()
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response)
self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
response = Response()
response.files.append(URL('a', 'static', 'css/file.js'))
content = return_includes(response)
self.assertEqual(content, '<script src="/a/static/css/file.js" type="text/javascript"></script>')
response = Response()
response.files.append(URL('a', 'static', 'css/file.coffee'))
content = return_includes(response)
self.assertEqual(content, '<script src="/a/static/css/file.coffee" type="text/coffee"></script>')
response = Response()
response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response)
self.assertEqual(content, '<script src="/a/static/css/file.ts" type="text/typescript"></script>')
response = Response()
response.files.append(URL('a', 'static', 'css/file.less'))
content = return_includes(response)
self.assertEqual(content, '<link href="/a/static/css/file.less" rel="stylesheet/less" type="text/css" />')
response = Response()
response.files.append(('css:inline', 'background-color; white;'))
content = return_includes(response)
self.assertEqual(content, '<style type="text/css">\nbackground-color; white;\n</style>')
response = Response()
response.files.append(('js:inline', 'alert("hello")'))
content = return_includes(response)
self.assertEqual(content, '<script type="text/javascript">\nalert("hello")\n</script>')
response = Response()
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js')
content = return_includes(response)
self.assertEqual(content, '<script src="https://code.jquery.com/jquery-1.11.3.min.js" type="text/javascript"></script>')
response = Response()
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
content = return_includes(response)
self.assertEqual(content, '<script src="https://code.jquery.com/jquery-1.11.3.min.js?var=0" type="text/javascript"></script>')
response = Response()
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
response.files.append(URL('a', 'static', 'css/file.css'))
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response)
self.assertEqual(content,
'<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" />')
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
response.files.append('https://code.jquery.com/jquery-1.11.3.min.js?var=0')
response.files.append(URL('a', 'static', 'css/file.css'))
response.files.append(URL('a', 'static', 'css/file.ts'))
content = return_includes(response)
self.assertEqual(content,
'<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>'
)
response = Response()
response.files.append(URL('a', 'static', 'css/file.js'))
response.files.append(URL('a', 'static', 'css/file.css'))
content = return_includes(response, extensions=['css'])
self.assertEqual(content, '<link href="/a/static/css/file.css" rel="stylesheet" type="text/css" />')
#regr test for #628
response = Response()
response.files.append('http://maps.google.com/maps/api/js?sensor=false')
content = return_includes(response)
self.assertEqual(content, '')
#regr test for #628
response = Response()
response.files.append(('js', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response)
self.assertEqual(content, '<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>')
response = Response()
response.files.append(['js', 'http://maps.google.com/maps/api/js?sensor=false'])
content = return_includes(response)
self.assertEqual(content, '<script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>')
response = Response()
response.files.append(('js1', 'http://maps.google.com/maps/api/js?sensor=false'))
content = return_includes(response)
self.assertEqual(content, '')
if __name__ == '__main__':
unittest.main()
+18
View File
@@ -120,6 +120,7 @@ class TestStorageList(unittest.TestCase):
class TestList(unittest.TestCase): class TestList(unittest.TestCase):
""" Tests Storage.List (fast-check for request.args()) """ """ Tests Storage.List (fast-check for request.args()) """
def test_listcall(self): def test_listcall(self):
@@ -134,6 +135,23 @@ class TestList(unittest.TestCase):
self.assertEqual(a(3, cast=int), 1234) self.assertEqual(a(3, cast=int), 1234)
a.append('x') a.append('x')
self.assertRaises(HTTP, a, 4, cast=int) self.assertRaises(HTTP, a, 4, cast=int)
b = List()
# default is always returned when especified
self.assertEqual(b(0, cast=int, default=None), None)
self.assertEqual(b(0, cast=int, default=None, otherwise='teste'), None)
self.assertEqual(b(0, cast=int, default='a', otherwise='teste'), 'a')
# if don't have value and otherwise is especified it will called
self.assertEqual(b(0, otherwise=lambda: 'something'), 'something')
self.assertEqual(b(0, cast=int, otherwise=lambda: 'something'),
'something')
# except if default is especified
self.assertEqual(b(0, default=0, otherwise=lambda: 'something'), 0)
def test_listgetitem(self):
'''Mantains list behaviour.'''
a = List((1, 2, 3))
self.assertEqual(a[0], 1)
self.assertEqual(a[::-1], [3, 2, 1])
if __name__ == '__main__': if __name__ == '__main__':
+365 -25
View File
@@ -60,7 +60,7 @@ except ImportError:
# fallback to pure-Python module # fallback to pure-Python module
import gluon.contrib.simplejson as json_parser import gluon.contrib.simplejson as json_parser
__all__ = ['Mail', 'Auth', 'Recaptcha', 'Crud', 'Service', 'Wiki', __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
### mind there are two loggers here (logger and crud.settings.logger)! ### mind there are two loggers here (logger and crud.settings.logger)!
@@ -767,8 +767,8 @@ class Mail(object):
if self.settings.server == 'logging': if self.settings.server == 'logging':
logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' %
('-' * 40, sender, ('-' * 40, sender,
', '.join(to), subject, ', '.join(to), subject,
text or html, '-' * 40)) text or html, '-' * 40))
elif self.settings.server == 'gae': elif self.settings.server == 'gae':
xcc = dict() xcc = dict()
if cc: if cc:
@@ -779,23 +779,23 @@ class Mail(object):
xcc['reply_to'] = reply_to xcc['reply_to'] = reply_to
from google.appengine.api import mail from google.appengine.api import mail
attachments = attachments and [mail.Attachment( attachments = attachments and [mail.Attachment(
a.my_filename, a.my_filename,
a.my_payload, a.my_payload,
contebt_id='<attachment-%s>' % k contebt_id='<attachment-%s>' % k
) for k,a in enumerate(attachments) if not raw] ) for k,a in enumerate(attachments) if not raw]
if attachments: if attachments:
result = mail.send_mail( result = mail.send_mail(
sender=sender, to=origTo, sender=sender, to=origTo,
subject=subject, body=text, html=html, subject=unicode(subject), body=unicode(text), html=html,
attachments=attachments, **xcc) attachments=attachments, **xcc)
elif html and (not raw): elif html and (not raw):
result = mail.send_mail( result = mail.send_mail(
sender=sender, to=origTo, sender=sender, to=origTo,
subject=subject, body=text, html=html, **xcc) subject=unicode(subject), body=unicode(text), html=html, **xcc)
else: else:
result = mail.send_mail( result = mail.send_mail(
sender=sender, to=origTo, sender=sender, to=origTo,
subject=subject, body=text, **xcc) subject=unicode(subject), body=unicode(text), **xcc)
else: else:
smtp_args = self.settings.server.split(':') smtp_args = self.settings.server.split(':')
kwargs = dict(timeout=self.settings.timeout) kwargs = dict(timeout=self.settings.timeout)
@@ -965,7 +965,142 @@ class Recaptcha(DIV):
return XML(captcha).xml() return XML(captcha).xml()
# this should only be used for catcha and perhaps not even for that class Recaptcha2(DIV):
"""
Experimental:
Creates a DIV holding the newer Recaptcha from Google (v2)
Args:
request : the request. If not passed, uses current request
public_key : the public key Google gave you
private_key : the private key Google gave you
error_message : the error message to show if verification fails
label : the label to use
options (dict) : takes these parameters
- hl
- theme
- type
- tabindex
- callback
- expired-callback
see https://developers.google.com/recaptcha/docs/display for docs about those
comment : the comment
Examples:
Use as::
form = FORM(Recaptcha2(public_key='...',private_key='...'))
or::
form = SQLFORM(...)
form.append(Recaptcha2(public_key='...',private_key='...'))
to protect the login page instead, use::
from gluon.tools import Recaptcha2
auth.settings.captcha = Recaptcha2(request, public_key='...',private_key='...')
"""
API_URI = 'https://www.google.com/recaptcha/api.js'
VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify'
def __init__(self,
request=None,
public_key='',
private_key='',
error_message='invalid',
label='Verify:',
options=None,
comment='',
):
request = request or current.request
self.request_vars = request and request.vars or current.request.vars
self.remote_addr = request.env.remote_addr
self.public_key = public_key
self.private_key = private_key
self.errors = Storage()
self.error_message = error_message
self.components = []
self.attributes = {}
self.label = label
self.options = options or {}
self.comment = comment
def _validate(self):
recaptcha_response_field = self.request_vars.pop('g-recaptcha-response', None)
remoteip = self.remote_addr
if not recaptcha_response_field:
self.errors['captcha'] = self.error_message
return False
params = urllib.urlencode({
'secret': self.private_key,
'remoteip': remoteip,
'response': recaptcha_response_field,
})
request = urllib2.Request(
url=self.VERIFY_SERVER,
data=params,
headers={'Content-type': 'application/x-www-form-urlencoded',
'User-agent': 'reCAPTCHA Python'})
httpresp = urllib2.urlopen(request)
content = httpresp.read()
httpresp.close()
try:
response_dict = json_parser.loads(content)
except:
self.errors['captcha'] = self.error_message
return False
if response_dict.get('success', False):
self.request_vars.captcha = ''
return True
else:
self.errors['captcha'] = self.error_message
return False
def xml(self):
api_uri = self.API_URI
hl = self.options.pop('hl', None)
if hl:
api_uri = self.API_URI + '?hl=%s' % hl
public_key = self.public_key
self.options['sitekey'] = public_key
captcha = DIV(
SCRIPT(_src=api_uri, _async='', _defer=''),
DIV(_class="g-recaptcha", data=self.options),
TAG.noscript(XML("""
<div style="width: 302px; height: 352px;">
<div style="width: 302px; height: 352px; position: relative;">
<div style="width: 302px; height: 352px; position: absolute;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=%(public_key)s"
frameborder="0" scrolling="no"
style="width: 302px; height:352px; border-style: none;">
</iframe>
</div>
<div style="width: 250px; height: 80px; position: absolute; border-style: none;
bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
class="g-recaptcha-response"
style="width: 250px; height: 80px; border: 1px solid #c1c1c1;
margin: 0px; padding: 0px; resize: none;" value="">
</textarea>
</div>
</div>
</div>""" % dict(public_key=public_key))
)
)
if not self.errors.captcha:
return XML(captcha).xml()
else:
captcha.append(DIV(self.errors['captcha'], _class='error'))
return XML(captcha).xml()
# this should only be used for captcha and perhaps not even for that
def addrow(form, a, b, c, style, _id, position=-1): def addrow(form, a, b, c, style, _id, position=-1):
if style == "divs": if style == "divs":
form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'), form[0].insert(position, DIV(DIV(LABEL(a), _class='w2p_fl'),
@@ -987,6 +1122,15 @@ def addrow(form, a, b, c, style, _id, position=-1):
DIV(b, SPAN(c, _class='inline-help'), DIV(b, SPAN(c, _class='inline-help'),
_class='controls'), _class='controls'),
_class='control-group', _id=_id)) _class='control-group', _id=_id))
elif style == "bootstrap3_inline":
form[0].insert(position, DIV(LABEL(a, _class='control-label col-sm-3'),
DIV(b, SPAN(c, _class='help-block'),
_class='col-sm-9'),
_class='form-group', _id=_id))
elif style == "bootstrap3_stacked":
form[0].insert(position, DIV(LABEL(a, _class='control-label'),
b, SPAN(c, _class='help-block'),
_class='form-group', _id=_id))
else: else:
form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'), form[0].insert(position, TR(TD(LABEL(a), _class='w2p_fl'),
TD(b, _class='w2p_fw'), TD(b, _class='w2p_fw'),
@@ -1002,6 +1146,7 @@ class Auth(object):
reset_password_requires_verification=False, reset_password_requires_verification=False,
registration_requires_verification=False, registration_requires_verification=False,
registration_requires_approval=False, registration_requires_approval=False,
bulk_register_enabled=False,
login_after_registration=False, login_after_registration=False,
login_after_password_change=True, login_after_password_change=True,
alternate_requires_registration=False, alternate_requires_registration=False,
@@ -1035,6 +1180,7 @@ class Auth(object):
table_permission_name='auth_permission', table_permission_name='auth_permission',
table_event_name='auth_event', table_event_name='auth_event',
table_cas_name='auth_cas', table_cas_name='auth_cas',
table_token_name='auth_token',
table_user=None, table_user=None,
table_group=None, table_group=None,
table_membership=None, table_membership=None,
@@ -1103,6 +1249,8 @@ class Auth(object):
retrieve_password_subject='Password retrieve', retrieve_password_subject='Password retrieve',
reset_password='Click on the link %(link)s to reset your password', reset_password='Click on the link %(link)s to reset your password',
reset_password_subject='Password reset', reset_password_subject='Password reset',
bulk_invite_subject='Invitation to join%(site)s',
bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process',
invalid_reset_password='Invalid reset password', invalid_reset_password='Invalid reset password',
profile_updated='Profile updated', profile_updated='Profile updated',
new_password='New password', new_password='New password',
@@ -1316,6 +1464,7 @@ class Auth(object):
settings.update(Auth.default_settings) settings.update(Auth.default_settings)
settings.update( settings.update(
cas_domains=[request.env.http_host], cas_domains=[request.env.http_host],
enable_tokens=False,
cas_provider=cas_provider, cas_provider=cas_provider,
cas_actions=dict(login='login', cas_actions=dict(login='login',
validate='validate', validate='validate',
@@ -1330,8 +1479,7 @@ class Auth(object):
logged_url=URL(controller, function, args='profile'), logged_url=URL(controller, function, args='profile'),
download_url=URL(controller, 'download'), download_url=URL(controller, 'download'),
mailer=(mailer is True) and Mail() or mailer, mailer=(mailer is True) and Mail() or mailer,
on_failed_authorization = on_failed_authorization = URL(controller, function, args='not_authorized'),
URL(controller, function, args='not_authorized'),
login_next = url_index, login_next = url_index,
login_onvalidation = [], login_onvalidation = [],
login_onaccept = [], login_onaccept = [],
@@ -1356,6 +1504,8 @@ class Auth(object):
change_password_onvalidation = [], change_password_onvalidation = [],
change_password_onaccept = [], change_password_onaccept = [],
retrieve_password_onvalidation = [], retrieve_password_onvalidation = [],
request_reset_password_onvalidation = [],
request_reset_password_onaccept = [],
reset_password_onvalidation = [], reset_password_onvalidation = [],
reset_password_onaccept = [], reset_password_onaccept = [],
hmac_key = hmac_key, hmac_key = hmac_key,
@@ -1417,6 +1567,9 @@ class Auth(object):
def table_cas(self): def table_cas(self):
return self.db[self.settings.table_cas_name] return self.db[self.settings.table_cas_name]
def table_token(self):
return self.db[self.settings.table_token_name]
def _HTTP(self, *a, **b): def _HTTP(self, *a, **b):
""" """
only used in lambda: self._HTTP(404) only used in lambda: self._HTTP(404)
@@ -1444,7 +1597,8 @@ class Auth(object):
'retrieve_username', 'retrieve_password', 'retrieve_username', 'retrieve_password',
'reset_password', 'request_reset_password', 'reset_password', 'request_reset_password',
'change_password', 'profile', 'groups', 'change_password', 'profile', 'groups',
'impersonate', 'not_authorized'): 'impersonate', 'not_authorized', 'confirm_registration',
'bulk_register','manage_tokens'):
if len(request.args) >= 2 and args[0] == 'impersonate': if len(request.args) >= 2 and args[0] == 'impersonate':
return getattr(self, args[0])(request.args[1]) return getattr(self, args[0])(request.args[1])
else: else:
@@ -1771,7 +1925,7 @@ class Auth(object):
writable=False, readable=False, writable=False, readable=False,
label=T('Modified By'), ondelete=ondelete)) label=T('Modified By'), ondelete=ondelete))
def define_tables(self, username=None, signature=None, def define_tables(self, username=None, signature=None, enable_tokens=False,
migrate=None, fake_migrate=None): migrate=None, fake_migrate=None):
""" """
To be called unless tables are defined manually To be called unless tables are defined manually
@@ -1798,6 +1952,7 @@ class Auth(object):
username = settings.use_username username = settings.use_username
else: else:
settings.use_username = username settings.use_username = username
settings.enable_tokens = enable_tokens
if not self.signature: if not self.signature:
self.define_signature() self.define_signature()
if signature == True: if signature == True:
@@ -1981,6 +2136,21 @@ class Auth(object):
migrate=self.__get_migrate( migrate=self.__get_migrate(
settings.table_cas_name, migrate), settings.table_cas_name, migrate),
fake_migrate=fake_migrate)) fake_migrate=fake_migrate))
if settings.enable_tokens:
extra_fields = settings.extra_fields.get(
settings.table_token_name, []) + signature_list
if not settings.table_token_name in db.tables:
db.define_table(
settings.table_token_name,
Field('user_id', reference_table_user, default=None,
label=self.messages.label_user_id),
Field('expires_on', 'datetime', default=datetime.datetime(2999,12,31)),
Field('token',writable=False,default=web2py_uuid(),unique=True),
*extra_fields,
**dict(
migrate=self.__get_migrate(
settings.table_token_name, migrate),
fake_migrate=fake_migrate))
if not db._lazy_tables: if not db._lazy_tables:
settings.table_user = db[settings.table_user_name] settings.table_user = db[settings.table_user_name]
settings.table_group = db[settings.table_group_name] settings.table_group = db[settings.table_group_name]
@@ -2180,8 +2350,8 @@ class Auth(object):
# user not in database try other login methods # user not in database try other login methods
for login_method in self.settings.login_methods: for login_method in self.settings.login_methods:
if login_method != self and login_method(username, password): if login_method != self and login_method(username, password):
self.user = username self.user = user
return username return user
return False return False
def register_bare(self, **fields): def register_bare(self, **fields):
@@ -2190,14 +2360,16 @@ class Auth(object):
and a raw password. and a raw password.
""" """
settings = self._get_login_settings() settings = self._get_login_settings()
if not fields.get(settings.passfield): # users can register_bare even if no password is provided,
raise ValueError("register_bare: " + # in this case they will have to reset their password to login
"password not provided or invalid") if fields.get(settings.passfield):
elif not fields.get(settings.userfield): fields[settings.passfield] = \
settings.table_user[settings.passfield].validate(fields[settings.passfield])[0]
if not fields.get(settings.userfield):
raise ValueError("register_bare: " + raise ValueError("register_bare: " +
"userfield not provided or invalid") "userfield not provided or invalid")
fields[settings.passfield] = settings.table_user[settings.passfield].validate(fields[settings.passfield])[0] user = self.get_or_create_user(fields, login=False, get=False,
user = self.get_or_create_user(fields, login=False, get=False, update_fields=self.settings.update_fields) update_fields=self.settings.update_fields)
if not user: if not user:
# get or create did not create a user (it ignores duplicate records) # get or create did not create a user (it ignores duplicate records)
return False return False
@@ -2993,6 +3165,147 @@ class Auth(object):
table_user.email.requires = old_requires table_user.email.requires = old_requires
return form return form
def confirm_registration(
self,
next=DEFAULT,
onvalidation=DEFAULT,
onaccept=DEFAULT,
log=DEFAULT,
):
"""
Returns a form to confirm user registration
"""
table_user = self.table_user()
request = current.request
# response = current.response
session = current.session
if next is DEFAULT:
next = self.get_vars_next() or self.settings.reset_password_next
if self.settings.prevent_password_reset_attacks:
key = request.vars.key
if not key and len(request.args)>1:
key = request.args[-1]
if key:
session._reset_password_key = key
redirect(self.url(args='confirm_registration'))
else:
key = session._reset_password_key
else:
key = request.vars.key or getarg(-1)
try:
t0 = int(key.split('-')[0])
if time.time() - t0 > 60 * 60 * 24:
raise Exception
user = table_user(reset_password_key=key)
if not user:
raise Exception
except Exception as e:
session.flash = self.messages.invalid_reset_password
redirect(self.url('login', vars=dict(test=e)))
redirect(next, client_side=self.settings.client_side)
passfield = self.settings.password_field
form = SQLFORM.factory(
Field('first_name',
label='First Name',
required=True),
Field('last_name',
label='Last Name',
required=True),
Field('new_password', 'password',
label=self.messages.new_password,
requires=self.table_user()[passfield].requires),
Field('new_password2', 'password',
label=self.messages.verify_password,
requires=[IS_EXPR(
'value==%s' % repr(request.vars.new_password),
self.messages.mismatched_password)]),
submit_button='Confirm Registration',
hidden=dict(_next=next),
formstyle=self.settings.formstyle,
separator=self.settings.label_separator
)
if form.process().accepted:
user.update_record(
**{passfield: str(form.vars.new_password),
'first_name': str(form.vars.first_name),
'last_name': str(form.vars.last_name),
'registration_key': '',
'reset_password_key': ''})
session.flash = self.messages.password_changed
if self.settings.login_after_password_change:
self.login_user(user)
redirect(next, client_side=self.settings.client_side)
return form
def email_registration(self, subject, body, user):
"""
Sends and email invitation to a user informing they have been registered with the application
"""
reset_password_key = str(int(time.time())) + '-' + web2py_uuid()
link = self.url(self.settings.function,
args=('confirm_registration',), vars={'key': reset_password_key},
scheme=True)
d = dict(user)
d.update(dict(key=reset_password_key, link=link, site=current.request.env.http_host))
if self.settings.mailer and self.settings.mailer.send(
to=user.email,
subject=subject % d,
message=body % d):
user.update_record(reset_password_key=reset_password_key)
return True
return False
def bulk_register(self, max_emails=100):
"""
Creates a form for ther user to send invites to other users to join
"""
if not self.user:
redirect(self.settings.login_url)
if not self.setting.bulk_register_enabled:
return HTTP(404)
form = SQLFORM.factory(
Field('subject','string',default=self.messages.bulk_invite_subject,requires=IS_NOT_EMPTY()),
Field('emails','text',requires=IS_NOT_EMPTY()),
Field('message','text',default=self.messages.bulk_invite_body,requires=IS_NOT_EMPTY()),
formstyle=self.settings.formstyle)
if form.process().accepted:
emails = re.compile('[^\s\'"@<>,;:]+\@[^\s\'"@<>,;:]+').findall(form.vars.emails)
# send the invitations
emails_sent = []
emails_fail = []
emails_exist = []
for email in emails[:max_emails]:
if self.table_user()(email=email):
emails_exist.append(email)
else:
user = self.register_bare(email=email)
if self.email_registration(form.vars.subject, form.vars.message, user):
emails_sent.append(email)
else:
emails_fail.append(email)
emails_fail += emails[max_emails:]
form = DIV(H4('Emails sent'),UL(*[A(x,_href='mailto:'+x) for x in emails_sent]),
H4('Emails failed'),UL(*[A(x,_href='mailto:'+x) for x in emails_fail]),
H4('Emails existing'),UL(*[A(x,_href='mailto:'+x) for x in emails_exist]))
return form
def manage_tokens(self):
if not self.user:
redirect(self.settings.login_url)
table_token =self.table_token()
table_token.user_id.writable = False
table_token.user_id.default = self.user.id
table_token.token.writable = False
if current.request.args(1) == 'new':
table_token.token.readable = False
form = SQLFORM.grid(table_token, args=['manage_tokens'])
return form
def reset_password(self, def reset_password(self,
next=DEFAULT, next=DEFAULT,
onvalidation=DEFAULT, onvalidation=DEFAULT,
@@ -3030,6 +3343,12 @@ class Auth(object):
except Exception: except Exception:
session.flash = self.messages.invalid_reset_password session.flash = self.messages.invalid_reset_password
redirect(next, client_side=self.settings.client_side) redirect(next, client_side=self.settings.client_side)
if onvalidation is DEFAULT:
onvalidation = self.settings.reset_password_onvalidation
if onaccept is DEFAULT:
onaccept = self.settings.reset_password_onaccept
passfield = self.settings.password_field passfield = self.settings.password_field
form = SQLFORM.factory( form = SQLFORM.factory(
Field('new_password', 'password', Field('new_password', 'password',
@@ -3045,7 +3364,7 @@ class Auth(object):
formstyle=self.settings.formstyle, formstyle=self.settings.formstyle,
separator=self.settings.label_separator separator=self.settings.label_separator
) )
if form.accepts(request, session, if form.accepts(request, session, onvalidation=onvalidation,
hideerror=self.settings.hideerror): hideerror=self.settings.hideerror):
user.update_record( user.update_record(
**{passfield: str(form.vars.new_password), **{passfield: str(form.vars.new_password),
@@ -3054,6 +3373,7 @@ class Auth(object):
session.flash = self.messages.password_changed session.flash = self.messages.password_changed
if self.settings.login_after_password_change: if self.settings.login_after_password_change:
self.login_user(user) self.login_user(user)
callback(onaccept, form)
redirect(next, client_side=self.settings.client_side) redirect(next, client_side=self.settings.client_side)
return form return form
@@ -3079,9 +3399,9 @@ class Auth(object):
response.flash = self.messages.function_disabled response.flash = self.messages.function_disabled
return '' return ''
if onvalidation is DEFAULT: if onvalidation is DEFAULT:
onvalidation = self.settings.reset_password_onvalidation onvalidation = self.settings.request_reset_password_onvalidation
if onaccept is DEFAULT: if onaccept is DEFAULT:
onaccept = self.settings.reset_password_onaccept onaccept = self.settings.request_reset_password_onaccept
if log is DEFAULT: if log is DEFAULT:
log = self.messages['reset_password_log'] log = self.messages['reset_password_log']
userfield = self.settings.login_userfield or 'username' \ userfield = self.settings.login_userfield or 'username' \
@@ -3451,6 +3771,26 @@ class Auth(object):
""" """
return self.requires(True, otherwise=otherwise) return self.requires(True, otherwise=otherwise)
def requires_login_or_token(self, otherwise=None):
if self.settings.enable_tokens == True:
user = None
request = current.request
token = request.env.http_web2py_user_token or request.vars._token
table_token = self.table_token()
table_user = self.table_user()
from gluon.settings import global_settings
if global_settings.web2py_runtime_gae:
row = table_token(token=token)
if row:
user = table_user(row.user_id)
else:
row = self.db(table_token.token==token)(table_user.id==table_token.user_id).select().first()
if row:
user = row[table_user._tablename]
if user:
self.login_user(user)
return self.requires(True, otherwise=otherwise)
def requires_membership(self, role=None, group_id=None, otherwise=None): def requires_membership(self, role=None, group_id=None, otherwise=None):
""" """
Decorator that prevents access to action if not logged in or Decorator that prevents access to action if not logged in or
@@ -3571,7 +3911,7 @@ class Auth(object):
return record.id return record.id
else: else:
id = membership.insert(group_id=group_id, user_id=user_id) id = membership.insert(group_id=group_id, user_id=user_id)
if role: if role:
self.user_groups[group_id] = role self.user_groups[group_id] = role
else: else:
self.update_groups() self.update_groups()
@@ -5362,7 +5702,7 @@ class Expose(object):
if current.request.raw_args: if current.request.raw_args:
self.args = [arg for arg in current.request.raw_args.split('/') if arg] self.args = [arg for arg in current.request.raw_args.split('/') if arg]
else: else:
self.args = [arg for arg in current.request.args if args] self.args = [arg for arg in current.request.args if arg]
filename = os.path.join(base, *self.args) filename = os.path.join(base, *self.args)
if not os.path.exists(filename): if not os.path.exists(filename):
raise HTTP(404, "FILE NOT FOUND") raise HTTP(404, "FILE NOT FOUND")
+6 -3
View File
@@ -200,8 +200,11 @@ class IS_MATCH(Validator):
self.is_unicode = is_unicode self.is_unicode = is_unicode
def __call__(self, value): def __call__(self, value):
if self.is_unicode and not isinstance(value, unicode): if self.is_unicode:
match = self.regex.search(str(value).decode('utf8')) if isinstance(value,unicode):
match = self.regex.search(value)
else:
match = self.regex.search(str(value).decode('utf8'))
else: else:
match = self.regex.search(str(value)) match = self.regex.search(str(value))
if match is not None: if match is not None:
@@ -611,7 +614,7 @@ class IS_IN_DB(Validator):
def count(values, s=self.dbset, f=field): def count(values, s=self.dbset, f=field):
return s(f.belongs(map(int, values))).count() return s(f.belongs(map(int, values))).count()
if isinstance(self.dbset.db._adapter, GoogleDatastoreAdapter): if GoogleDatastoreAdapter is not None and isinstance(self.dbset.db._adapter, GoogleDatastoreAdapter):
range_ids = range(0, len(values), 30) range_ids = range(0, len(values), 30)
total = sum(count(values[i:i + 30]) for i in range_ids) total = sum(count(values[i:i + 30]) for i in range_ids)
if total == len(values): if total == len(values):
+1 -1
View File
@@ -84,7 +84,7 @@ server {
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m; ssl_session_timeout 10m;
ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA; ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA;
ssl_protocols SSLv3 TLSv1; ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
keepalive_timeout 70; keepalive_timeout 70;
location / { location / {
#uwsgi_pass 127.0.0.1:9001; #uwsgi_pass 127.0.0.1:9001;
+169
View File
@@ -0,0 +1,169 @@
"This script will work fine for a few cases 'by default':"
" - completely CLEAN WS2012R2 host"
" - python 2.7 installed in the default path"
" - wfasctgi installed on the default path"
"It'll install web2py under the default website "
" You can use it as a boilerplate to automate your deployments"
" but it still is released AS IT IS. "
"BIG FAT WARNING: It will install a bunch of dependecies
Inspect the source before executing it"
""
""
$ErrorActionPreference = 'stop'
$REALLY_SURE = Read-Host "Do you want to start with web2py deployment? [y/N]"
if (!@('y', 'Y') -contains $REALLY_SURE) {
"Ok, Exiting without doing anything"
exit 1
}
#setting root folder
$rootfolder = $pwd
### utilities - start
function ask_a_question($question) {
$response = Read-Host "$question [Y/n]"
if (@('Y', 'y', '', $null) -contains $response) {
return $true
} else {
return $false
}
}
function unzip_me {
#Load the assembly
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
#Unzip the file
[System.IO.Compression.ZipFile]::ExtractToDirectory($pathToZip, $targetDir)
}
### utilities - end
#install 4.5 that is needed for a bunch of things anyway
Install-WindowsFeature Net-Framework-45-Core
#fetch web2py
$web2py_url = 'http://www.web2py.com/examples/static/web2py_src.zip'
$web2py_file = "$pwd\web2py_src.zip"
if (!(Test-Path $web2py_file)) {
(new-object net.webclient).DownloadFile($web2py_url, $web2py_file)
}
#Load the assembly
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") | Out-Null
#Unzip the file
[System.IO.Compression.ZipFile]::ExtractToDirectory($web2py_file, $pwd)
#features installation (IIS, needed modules, python, chocolatey, etc)
$installfeatures = ask_a_question('Do you want to install needed features?')
if ($installfeatures) {
Install-WindowsFeature Web-Server,Web-Default-Doc,Web-Static-Content,Web-Http-Redirect,Web-Http-Logging,Web-Request-Monitor,`
Web-Http-Tracing,Web-Stat-Compression,Web-Dyn-Compression,Web-Filtering,Web-Basic-Auth,Web-Windows-Auth,Web-AppInit,`
Web-CGI,Web-WebSockets,Web-Mgmt-Console,Web-Net-Ext45
}
$copy_web2py = ask_a_question("Copy web2py to the default website root?")
if ($copy_web2py) {
Import-Module WebAdministration
$available_websites = Get-Website
if ($available_websites[0] -eq $null) {
$default_one = $available_websites
} else {
$default_one = $available_websites[0]
}
$iis_root = [System.Environment]::ExpandEnvironmentVariables($default_one.PhysicalPath)
Copy-Item "$rootfolder\web2py\*" $iis_root -Recurse
$rootfolder = $iis_root
$acl = (Get-Item $rootfolder).GetAccessControl('Access')
$identity = "BUILTIN\IIS_IUSRS"
$fileSystemRights = "Modify"
$inheritanceFlags = "ContainerInherit, ObjectInherit"
$propagationFlags = "None"
$accessControlType = "Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fileSystemRights, $inheritanceFlags, $propagationFlags, $accessControlType)
$acl.SetAccessRule($rule)
Set-Acl $rootfolder $acl
}
$create_cert = ask_a_question("Do you want to create a self-signed SSL cert?")
if ($create_cert) {
$cert = New-SelfSignedCertificate -DnsName ("localtest.me","*.localtest.me") -CertStoreLocation cert:\LocalMachine\My
$rootStore = Get-Item cert:\LocalMachine\Root
$rootStore.Open("ReadWrite")
$rootStore.Add($cert)
$rootStore.Close();
Import-Module WebAdministration
Set-Location IIS:\SslBindings
New-WebBinding -Name "Default Web Site" -IP "*" -Port 443 -Protocol https
$cert | New-Item 0.0.0.0!443
Set-Location $pwd
}
"checking for chocolatey"
if (Get-Command "choco.exe" -ErrorAction SilentlyContinue)
{
"chocolatey found"
} else {
"installing chocolatey"
(new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1') | iex
}
"installing url-rewrite"
choco install UrlRewrite
$pythonexe = Read-Host 'Python.exe path [C:\Python27\python.exe]'
if (($pythonexe -eq '') -or ($pythonexe -eq $null)) {
$pythonexe = 'C:\Python27\python.exe'
}
if (!(Test-Path $pythonexe)) {
"ERROR: python executable not found"
$pythonwanted = ask_a_question("do you want to install it automatically?")
if ($pythonwanted) {
choco install webpicmd
WebpiCmd.exe /Install /Products:WFastCgi_21_279
$pythonexe = 'C:\Python27\python.exe'
}
else {
exit 1
}
}
$wfastcgipath = Read-Host 'wfastcgi.py path [C:\Python27\Scripts\wfastcgi.py]'
if (($wfastcgipath -eq '') -or ($wfastcgipath -eq $null)) {
$wfastcgipath = 'C:\Python27\Scripts\wfastcgi.py'
}
if (-not (Test-Path $wfastcgipath)) {
"ERROR: wfastcgi.py not found"
$wfastcgiwanted = ask_a_question("do you want to install it automatically?")
if ($wfastcgiwanted) {
choco install webpicmd
WebpiCmd.exe /Install /Products:WFastCgi_21_279
} else {
exit 1
}
}
$pythondir = Split-Path c:\python27\python.exe
#installing dependencies
$env:Path = $env:Path + ";$pythondir;$pythondir\Scripts"
pip install pypiwin32
$PW = Read-Host 'Web2py Admin Password'
$appcmdpath = "$env:windir\system32\inetsrv\appcmd.exe"
& $appcmdpath set config /section:system.webServer/fastCGI "/+[fullPath='$pythonexe', arguments='$wfastcgipath']"
& $appcmdpath unlock config -section:system.webServer/handlers
& cd $rootfolder
& $pythonexe -c "from gluon.main import save_password; save_password('$PW',443)"
$webconfig_template = Join-Path $rootfolder "examples\web.config"
$destination = Join-Path $rootfolder "web.config"
$scriptprocessor = 'scriptProcessor="{0}|{1}"' -f $pythonexe, $wfastcgipath
(Get-Content $webconfig_template) | Foreach-Object {$_ -replace 'scriptProcessor="SCRIPT_PROCESSOR"', $scriptprocessor} | where {$_ -ne ""} | Set-Content $destination
""
"Installation finished. Web2py is available either on http://localhost/ or at https://localtest.me/"
""
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Post error tickets to slack on a 5 minute schedule.
#
# Proper use depends on having created a web-hook through Slack, and having set
# that value in your app's model as the value of global_settings.slack_hook.
# Details on creating web-hooks can be found at https://slack.com/integrations
#
# requires the Requests module for posting to slack, other requirements are
# standard or provided by web2py
#
# Usage (on Unices), replace myapp with the name of your application and run:
# nohup python web2py.py -S myapp -M -R scripts/tickets2slack.py &
import sys
import os
import time
import pickle
import json
try:
import requests
except ImportError as e:
print "missing module 'Requests', aborting."
sys.exit(1)
from gluon import URL
from gluon.utils import md5_hash
from gluon.restricted import RestrictedError
from gluon.settings import global_settings
path = os.path.join(request.folder, 'errors')
sent_errors_file = os.path.join(path, 'slack_errors.pickle')
hashes = {}
if os.path.exists(sent_errors_file):
try:
with open(sent_errors_file, 'rb') as f:
hashes = pickle.load(f)
except Exception as _:
pass
# ## CONFIGURE HERE
SLEEP_MINUTES = 5
ALLOW_DUPLICATES = False
global_settings.slack_hook = global_settings.slack_hook or \
'https://hooks.slack.com/services/your_service'
# ## END CONFIGURATION
while 1:
for file_name in os.listdir(path):
if file_name == 'slack_errors.pickle':
continue
if not ALLOW_DUPLICATES:
key = md5_hash(file_name)
if key in hashes:
continue
hashes[key] = 1
error = RestrictedError()
try:
error.load(request, request.application, file_name)
except Exception as _:
continue # not an exception file?
url = URL(a='admin', f='ticket', args=[request.application, file],
scheme=True)
payload = json.dumps(dict(text="Error in %(app)s.\n%(url)s" %
dict(app=request.application, url=url)))
requests.post(global_settings.slack_hook, data=dict(payload=payload))
with open(sent_errors_file, 'wb') as f:
pickle.dump(hashes, f)
time.sleep(SLEEP_MINUTES * 60)
+1 -1
View File
@@ -62,7 +62,7 @@ do_start()
start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \ start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \
&& return 1 && return 1
start-stop-daemon --start --quiet --pidfile $PIDFILE \ start-stop-daemon --start --quiet -m --pidfile $PIDFILE \
${DAEMON_USER:+--chuid $DAEMON_USER} --chdir $DAEMON_DIR \ ${DAEMON_USER:+--chuid $DAEMON_USER} --chdir $DAEMON_DIR \
--background --exec $DAEMON -- $DAEMON_ARGS \ --background --exec $DAEMON -- $DAEMON_ARGS \
|| return 2 || return 2