diff --git a/.travis.yml b/.travis.yml index 77028bbd..99bed1e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,14 +17,14 @@ install: before_script: - 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 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 after_success: - 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: email: true diff --git a/CHANGELOG b/CHANGELOG index 9e7e76c8..a1efbb0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +## 2.11.1 + +- Many small but significative improvements and bug fixes + ## 2.10.1-2.10.2 - welcome app defaults to Bootstrap 3 diff --git a/Makefile b/Makefile index a2a4fdec..e16e0967 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ update: echo "remember that pymysql was tweaked" src: ### 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 make clean ### clean up baisc apps diff --git a/README.markdown b/README.markdown index 237a27df..1c906755 100644 --- a/README.markdown +++ b/README.markdown @@ -13,7 +13,7 @@ Learn more at http://web2py.com 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. @@ -38,9 +38,10 @@ PyDAL uses a separate stable release cycle to the rest of web2py. PyDAL releases ## Tests -[](https://travis-ci.org/web2py/web2py) +[](https://travis-ci.org/web2py/web2py) +[](https://ci.appveyor.com/project/web2py/web2py) +[](https://codecov.io/github/web2py/web2py) -[](https://coveralls.io/r/web2py/web2py) ## Installation Instructions @@ -63,7 +64,7 @@ That's it!!! packages/ > web2py submodules dal/ contrib/ > third party libraries - tests/ > unittests + tests/ > unittests applications/ > are the apps admin/ > web based IDE ... diff --git a/VERSION b/VERSION index 0058b2a4..4b49a8a7 100644 --- a/VERSION +++ b/VERSION @@ -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 diff --git a/anyserver.py b/anyserver.py index a7d7eec8..a500922a 100644 --- a/anyserver.py +++ b/anyserver.py @@ -180,6 +180,11 @@ class Servers: s = wsgi.WSGIServer(callable=app, bind="%s:%d" % address) 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): """ diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 8af1047c..4d208944 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -49,7 +49,8 @@ if request.function == 'manage': auth.table_group(), auth.table_permission()]) 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 elif (request.application == 'admin' and not session.authorized) or \ (request.application != 'admin' and not gluon.fileutils.check_credentials(request)): @@ -80,7 +81,6 @@ if False and request.tickets_db: def get_databases(request): dbs = {} for (key, value) in global_env.items(): - cond = False try: cond = isinstance(value, GQLDB) except: @@ -420,7 +420,7 @@ def ccache(): 'oldest': time.time(), 'keys': [] } - + disk = copy.copy(ram) total = copy.copy(ram) disk['keys'] = [] @@ -480,12 +480,12 @@ def ccache(): disk['oldest'] = value[0] disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - total['entries'] = ram['entries'] + disk['entries'] - total['bytes'] = ram['bytes'] + disk['bytes'] - total['objects'] = ram['objects'] + disk['objects'] - total['hits'] = ram['hits'] + disk['hits'] - total['misses'] = ram['misses'] + disk['misses'] - total['keys'] = ram['keys'] + disk['keys'] + ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses'] + ram_keys.remove('ratio') + ram_keys.remove('oldest') + for key in ram_keys: + total[key] = ram[key] + disk[key] + try: total['ratio'] = total['hits'] * 100 / (total['hits'] + total['misses']) @@ -577,9 +577,7 @@ def bg_graph_model(): group = meta_graphmodel['group'].replace(' ', '') if not subgraphs.has_key(group): subgraphs[group] = dict(meta=meta_graphmodel, tables=[]) - subgraphs[group]['tables'].append(tablename) - else: - subgraphs[group]['tables'].append(tablename) + subgraphs[group]['tables'].append(tablename) graph.add_node(tablename, name=tablename, shape='plaintext', label=table_template(tablename)) diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index 8c8522ac..7176e09c 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -220,7 +220,7 @@ def list_breakpoints(): "Return a list of linenumbers for current breakpoints" breakpoints = [] - ok = None + ok = False try: filename = os.path.join(request.env['applications_parent'], 'applications', request.vars.filename) @@ -235,5 +235,4 @@ def list_breakpoints(): ok = True except Exception, e: session.flash = str(e) - ok = False return response.json({'ok': ok, 'breakpoints': breakpoints}) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 69634040..3e615c1e 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -292,9 +292,6 @@ def site(): log_progress(appname) session.flash = T(msg, dict(appname=appname, 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: msg = 'unable to install application "%(appname)s"' session.flash = T(msg, dict(appname=form_update.vars.name)) @@ -370,25 +367,56 @@ def pack_plugin(): session.flash = T('internal error') 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(): app = get_app() base = apath(app, r=request) if request.post_vars.file: + files = request.post_vars.file files = [files] if not isinstance(files,list) else files - fname = 'web2py.app.%s.w2p' % app - try: - filename = app_pack(app, request, raise_ex=True, filenames=files) - except Exception, e: - filename = None - if filename: - response.headers['Content-Type'] = 'application/w2p' - disposition = 'attachment; filename=%s' % fname - response.headers['Content-Disposition'] = disposition - return safe_read(filename, 'rb') + if request.post_vars.doexe is None: + fname = 'web2py.app.%s.w2p' % app + try: + filename = app_pack(app, request, raise_ex=True, filenames=files) + except Exception, e: + filename = None + if filename: + response.headers['Content-Type'] = 'application/w2p' + disposition = 'attachment; filename=%s' % fname + response.headers['Content-Disposition'] = disposition + return safe_read(filename, 'rb') + else: + session.flash = T('internal error: %s', e) + redirect(URL(args=request.args)) else: - session.flash = T('internal error: %s', e) - redirect(URL(args=request.args)) + return pack_exe(app, base, files) def ignore(fs): return [f for f in fs if not ( f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))] @@ -744,7 +772,7 @@ def edit(): viewlist.append(aviewpath + '.html') if len(viewlist): editviewlinks = [] - for v in viewlist: + for v in sorted(viewlist): vf = os.path.split(v)[-1] vargs = "/".join([viewpath.replace(os.sep, "/"), vf]) editviewlinks.append(A(vf.split(".")[0], @@ -754,6 +782,7 @@ def edit(): if len(request.args) > 2 and request.args[1] == 'controllers': controller = (request.args[2])[:-3] functions = find_exposed_functions(data) + functions = functions and sorted(functions) or [] else: (controller, functions) = (None, None) @@ -866,13 +895,9 @@ def resolve(): def getclass(item): """ Determine item class """ - - if item[0] == ' ': - return 'normal' - if item[0] == '+': - return 'plus' - if item[0] == '-': - return 'minus' + operators = {' ':'normal', '+':'plus', '-':'minus'} + + return operators[item[0]] if request.vars: 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: data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) items = find_exposed_functions(data) - functions[c] = items + functions[c] = items and sorted(items) or [] # Get all views views = sorted( @@ -1205,7 +1230,7 @@ def plugin(): for c in controllers: data = safe_read(apath('%s/controllers/%s' % (app, c), r=request)) items = find_exposed_functions(data) - functions[c] = items + functions[c] = items and sorted(items) or [] # Get all views views = sorted( @@ -1509,7 +1534,7 @@ def upload_file(): if filename: d = dict(filename=filename[len(path):]) else: - d = dict(filename='unkown') + d = dict(filename='unknown') session.flash = T('cannot upload file "%(filename)s"', d) redirect(request.vars.sender) diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index 2e463733..257f87cd 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -490,12 +490,12 @@ * and prevent clicking on it */ disableElement: function(el) { el.addClass('disabled'); - var method = el.is('button') ? 'html' : 'val'; + var method = el.is('input') ? 'val' : 'html'; //method = el.attr('name') ? 'html' : 'val'; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; /*store enabled state if not already disabled */ - if(el.data('w2p:enable-with') === undefined) { - el.data('w2p:enable-with', el[method]()); + if(el.data('w2p_enable_with') === undefined) { + el.data('w2p_enable_with', el[method]()); } /*if you don't want to see "working..." on buttons, replace the following * two lines with this one @@ -515,11 +515,11 @@ /* restore element to its original state which was disabled by 'disableElement' above*/ enableElement: function(el) { - var method = el.is('button') ? 'val' : 'html'; - if(el.data('w2p:enable-with') !== undefined) { + var method = el.is('input') ? 'val' : 'html'; + if(el.data('w2p_enable_with') !== undefined) { /* set to old enabled state */ - el[method](el.data('w2p:enable-with')); - el.removeData('w2p:enable-with'); + el[method](el.data('w2p_enable_with')); + el.removeData('w2p_enable_with'); } el.removeClass('disabled'); el.unbind('click.w2pDisable'); @@ -586,12 +586,14 @@ if(pre_call != undefined) { eval(pre_call); } - if(confirm_message != undefined) { - if(confirm_message == 'default') confirm_message = w2p_ajax_confirm_message || 'Are you sure you want to delete this object?'; - if(!web2py.confirm(confirm_message)) { - web2py.stopEverything(e); - return; - } + if(confirm_message) { + if(confirm_message == 'default') + confirm_message = w2p_ajax_confirm_message || + 'Are you sure you want to delete this object?'; + if(!web2py.confirm(confirm_message)) { + web2py.stopEverything(e); + return; + } } if(target == undefined) { if(method == 'GET') { @@ -634,7 +636,7 @@ }); }, /* 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 - Sets disabled property to true */ @@ -646,8 +648,8 @@ if(disable_with == undefined) { element.data('w2p_disable_with', element[method]()) } - if(element.data('w2p:enable-with') === undefined) { - element.data('w2p:enable-with', element[method]()); + if(element.data('w2p_enable_with') === undefined) { + element.data('w2p_enable_with', element[method]()); } element[method](element.data('w2p_disable_with')); element.prop('disabled', true); @@ -655,16 +657,16 @@ }, /* 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 */ enableFormElements: function(form) { form.find(web2py.enableSelector).each(function() { var element = $(this), method = element.is('button') ? 'html' : 'val'; - if(element.data('w2p:enable-with')) { - element[method](element.data('w2p:enable-with')); - element.removeData('w2p:enable-with'); + if(element.data('w2p_enable_with')) { + element[method](element.data('w2p_enable_with')); + element.removeData('w2p_enable_with'); } element.prop('disabled', false); }); @@ -730,4 +732,4 @@ web2py_event_handlers = jQuery.web2py.event_handlers; web2py_trap_link = jQuery.web2py.trap_link; web2py_calc_entropy = jQuery.web2py.calc_entropy; */ -/* compatibility code - end*/ \ No newline at end of file +/* compatibility code - end*/ diff --git a/applications/admin/views/default/design.html b/applications/admin/views/default/design.html index b7fc4991..2d918b83 100644 --- a/applications/admin/views/default/design.html +++ b/applications/admin/views/default/design.html @@ -1,5 +1,7 @@ {{extend 'layout.html'}} {{ +import re +regex_space = re.compile('\s+') def all(items): return reduce(lambda a,b:a and b,items,True) 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: if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]): path.append(file_path[len(path)]) - thispath='static__'+'__'.join(path) + thispath = regex_space.sub('-', 'static__'+'__'.join(path)) }}