diff --git a/VERSION b/VERSION index 518850f7..bee3b373 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.8.2-stable+timestamp.2013.11.28.07.51.37 +Version 2.8.2-stable+timestamp.2013.12.17.16.49.17 diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 221458de..2e616459 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -30,10 +30,18 @@ from gluon.languages import (read_possible_languages, read_dict, write_dict, read_plural_dict, write_plural_dict) -if DEMO_MODE and request.function in ['change_password', 'pack', 'pack_custom','pack_plugin', 'upgrade_web2py', 'uninstall', 'cleanup', 'compile_app', 'remove_compiled_app', 'delete', 'delete_plugin', 'create_file', 'upload_file', 'update_languages', 'reload_routes', 'git_push', 'git_pull']: +if DEMO_MODE and request.function in ['change_password', 'pack', +'pack_custom','pack_plugin', 'upgrade_web2py', 'uninstall', +'cleanup', 'compile_app', 'remove_compiled_app', 'delete', +'delete_plugin', 'create_file', 'upload_file', 'update_languages', +'reload_routes', 'git_push', 'git_pull', 'install_plugin']: session.flash = T('disabled in demo mode') redirect(URL('site')) +if is_gae and request.function in ('edit', 'edit_language', +'edit_plurals', 'update_languages', 'create_file', 'install_plugin'): + session.flash = T('disabled in GAE mode') + redirect(URL('site')) if not is_manager() and request.function in ['change_password', 'upgrade_web2py']: session.flash = T('disabled in multi user mode') @@ -63,10 +71,12 @@ def log_progress(app, mode='EDIT', filename=None, progress=0): def safe_open(a, b): - if DEMO_MODE and ('w' in b or 'a' in b): + if (DEMO_MODE or is_gae) and ('w' in b or 'a' in b): class tmp: def write(self, data): pass + def close(self): + pass return tmp() return open(a, b) @@ -750,6 +760,7 @@ def edit(): return response.json({'file_hash': file_hash, 'saved_on': saved_on, 'functions': functions, 'controller': controller, 'application': request.args[0], 'highlight': highlight}) else: file_details = dict(app=request.args[0], + lineno=request.vars.lineno or 1, editor_settings=preferences, filename=filename, realfilename=realfilename, @@ -773,6 +784,35 @@ def edit(): else: return response.json(file_details) +def todolist(): + """ Returns all TODO of the requested app + """ + app = request.vars.app or '' + app_path = apath('%(app)s' % {'app':app}, r=request) + dirs=['models', 'controllers', 'modules', 'private' ] + def listfiles(app, dir, regexp='.*\.py$'): + files = sorted( listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp)) + files = [x.replace(os.path.sep, '/') for x in files if not x.endswith('.bak')] + return files + + pattern = '#\s*(todo)+\s+(.*)' + regex = re.compile(pattern, re.IGNORECASE) + + output = [] + for d in dirs: + for f in listfiles(app, d): + matches = [] + filename= apath(os.path.join(app, d, f), r=request) + with open(filename, 'r') as f_s: + src = f_s.read() + for m in regex.finditer(src): + start = m.start() + lineno = src.count('\n', 0, start) + 1 + matches.append({'text':m.group(0), 'lineno':lineno}) + if len(matches) != 0: + output.append({'filename':f,'matches':matches, 'dir':d}) + + return {'todo':output, 'app': app} def resolve(): """ @@ -1053,11 +1093,12 @@ def design(): #Get crontab cronfolder = apath('%s/cron' % app, r=request) - if not os.path.exists(cronfolder): - os.mkdir(cronfolder) crontab = apath('%s/cron/crontab' % app, r=request) - if not os.path.exists(crontab): - safe_write(crontab, '#crontab') + if not is_gae: + if not os.path.exists(cronfolder): + os.mkdir(cronfolder) + if not os.path.exists(crontab): + safe_write(crontab, '#crontab') plugins = [] @@ -1209,7 +1250,6 @@ def plugin(): languages=languages, crontab=crontab) - def create_file(): """ Create files handler """ if request.vars and not request.vars.token == session.token: @@ -1220,6 +1260,8 @@ def create_file(): app = get_app(request.vars.app) path = abspath(request.vars.location) else: + if request.vars.dir: + request.vars.location += request.vars.dir + '/' app = get_app(name=request.vars.location.split('/')[0]) path = apath(request.vars.location, r=request) filename = re.sub('[^\w./-]+', '_', request.vars.filename) @@ -1347,7 +1389,11 @@ def create_file(): safe_write(full_filename, text) log_progress(app, 'CREATE', filename) - session.flash = T('file "%(filename)s" created', + if request.vars.dir: + result = T('file "%(filename)s" created', + dict(filename=full_filename[len(path):])) + else: + session.flash = T('file "%(filename)s" created', dict(filename=full_filename[len(path):])) vars = {} if request.vars.id: @@ -1356,13 +1402,51 @@ def create_file(): vars['app'] = request.vars.app redirect(URL('edit', args=[os.path.join(request.vars.location, filename)], vars=vars)) + except Exception, e: if not isinstance(e, HTTP): session.flash = T('cannot create file') - redirect(request.vars.sender + anchor) + if request.vars.dir: + response.flash = result + response.headers['web2py-component-content'] = 'append' + response.headers['web2py-component-command'] = """ + $.web2py.invalidate('#files_menu'); + load_file('%s'); + $.web2py.enableElement($('#form form').find($.web2py.formInputClickSelector)); + """ % URL('edit', args=[app,request.vars.dir,filename]) + return '' + else: + redirect(request.vars.sender + anchor) +def listfiles(app, dir, regexp='.*\.py$'): + files = sorted( + listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp)) + files = [x.replace('\\', '/') for x in files if not x.endswith('.bak')] + return files + +def editfile(path,file,vars={}, app = None): + args=(path,file) if 'app' in vars else (app,path,file) + url = URL('edit', args=args, vars=vars) + return A(file, _class='editor_filelink', _href=url, _style='word-wrap: nowrap;') + +def files_menu(): + app = request.vars.app or 'welcome' + dirs=[{'name':'models', 'reg':'.*\.py$'}, + {'name':'controllers', 'reg':'.*\.py$'}, + {'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'}, + {'name':'modules', 'reg':'.*\.py$'}, + {'name':'static', 'reg': '[^\.#].*'}] + result_files = [] + for dir in dirs: + result_files.append(TAG[''](LI(dir['name'], _class="nav-header component", _onclick="collapse('" + dir['name'] + "_files');"), + LI(UL(*[LI(editfile(dir['name'], f, dict(id=dir['name'] + f.replace('.','__')), app), _style="overflow:hidden", _id=dir['name']+"__"+f.replace('.','__')) + for f in listfiles(app, dir['name'], regexp=dir['reg'])], + _class="nav nav-list small-font"), + _id=dir['name'] + '_files', _style="display: none;"))) + return dict(result_files = result_files) + def upload_file(): """ File uploading handler """ if request.vars and not request.vars.token == session.token: @@ -1422,8 +1506,11 @@ def errors(): import hashlib app = get_app() - - method = request.args(1) or 'new' + if is_gae: + method = 'dbold' if ('old' in + (request.args(1) or '')) else 'dbnew' + else: + method = request.args(1) or 'new' db_ready = {} db_ready['status'] = get_ticket_storage(app) db_ready['errmessage'] = T( @@ -1490,32 +1577,30 @@ def errors(): for fn in tk_db(tk_table.id > 0).select(): try: error = pickle.loads(fn.ticket_data) - except AttributeError: + hash = hashlib.md5(error['traceback']).hexdigest() + + if hash in delete_hashes: + tk_db(tk_table.id == fn.id).delete() + tk_db.commit() + else: + try: + hash2error[hash]['count'] += 1 + except KeyError: + error_lines = error['traceback'].split("\n") + last_line = error_lines[-2] + error_causer = os.path.split(error['layer'])[1] + hash2error[hash] = dict(count=1, + pickel=error, causer=error_causer, + last_line=last_line, hash=hash, + ticket=fn.ticket_id) + except AttributeError, e: tk_db(tk_table.id == fn.id).delete() tk_db.commit() - hash = hashlib.md5(error['traceback']).hexdigest() - - if hash in delete_hashes: - tk_db(tk_table.id == fn.id).delete() - tk_db.commit() - else: - try: - hash2error['hash']['count'] += 1 - except KeyError: - error_lines = error['traceback'].split("\n") - last_line = error_lines[-2] - error_causer = os.path.split(error['layer'])[1] - hash2error[hash] = dict(count=1, pickel=error, - causer=error_causer, - last_line=last_line, - hash=hash, ticket=fn.ticket_id) - decorated = [(x['count'], x) for x in hash2error.values()] - decorated.sort(key=operator.itemgetter(0), reverse=True) - - return dict(errors=[x[1] for x in decorated], app=app, method=method) + return dict(errors=[x[1] for x in decorated], app=app, + method=method, db_ready=db_ready) elif method == 'dbold': tk_db, tk_table = get_ticket_storage(app) @@ -1523,16 +1608,18 @@ def errors(): if item[:7] == 'delete_': tk_db(tk_table.ticket_id == item[7:]).delete() tk_db.commit() - tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id, tk_table.created_datetime, orderby=~tk_table.created_datetime) + tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id, + tk_table.created_datetime, + orderby=~tk_table.created_datetime) tickets = [row.ticket_id for row in tickets_] - times = dict( - [(row.ticket_id, row.created_datetime) for row in tickets_]) - - return dict(app=app, tickets=tickets, method=method, times=times) + times = dict([(row.ticket_id, row.created_datetime) for + row in tickets_]) + return dict(app=app, tickets=tickets, method=method, + times=times, db_ready=db_ready) else: for item in request.vars: - # delete_all} rows doesn't contain any ticket + # delete_all rows doesn't contain any ticket # Remove anything else as requested if item[:7] == 'delete_' and (not item == "delete_all}"): os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request)) @@ -1552,6 +1639,9 @@ def get_ticket_storage(app): if os.path.exists(ticket_file): db_string = open(ticket_file).read() db_string = db_string.strip().replace('\r', '').replace('\n', '') + elif is_gae: + # use Datastore as fallback if there is no ticket_file + db_string = "google:datastore" else: return False tickets_table = 'web2py_ticket' @@ -1804,24 +1894,7 @@ def plugins(): "public/api.json/action/list/content/Package?package" + "_type=plugin&search_index=false").read() session.plugins = loads_json(rawlist) - plugins = TABLE( - *[TR(TD(H5(article["article"]["title"]), - A(T("Install"), - _href=URL(c="default", - f="install_plugin", - args=[app,], - vars={"source": - article["package_data"]["download"], - "plugin": article["article"]["title"]} - ))), - TD(article["article"]["description"], BR(), - A(T("Plugin page"), - _href="http://www.web2pyslices.com/slice/show/%s/" % \ - article["article"]["id"])), - TD(IMG(_src="http://www.web2pyslices.com/download/%s" % \ - article["article"]["thumbnail"]))) - for article in session.plugins["results"]]) - return dict(plugins=plugins, app=request.args(0)) + return dict(plugins=session.plugins["results"], app=request.args(0)) def install_plugin(): app = request.args(0) diff --git a/applications/admin/controllers/shell.py b/applications/admin/controllers/shell.py index 6d394861..3a835c7d 100644 --- a/applications/admin/controllers/shell.py +++ b/applications/admin/controllers/shell.py @@ -3,6 +3,7 @@ import cStringIO import gluon.contrib.shell import code import thread +import cgi from gluon.shell import env if DEMO_MODE or MULTI_USER_MODE: @@ -40,7 +41,7 @@ def callback(): k = len(session['commands:' + app]) - 1 #output = PRE(output) #return TABLE(TR('In[%i]:'%k,PRE(command)),TR('Out[%i]:'%k,output)) - return 'In [%i] : %s%s\n' % (k + 1, command, output) + return cgi.escape('In [%i] : %s%s\n' % (k + 1, command, output)) def reset(): diff --git a/applications/admin/languages/es.py b/applications/admin/languages/es.py index e064e408..d8c4c4ef 100644 --- a/applications/admin/languages/es.py +++ b/applications/admin/languages/es.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'es', '!langname!': 'Español', @@ -9,6 +9,7 @@ '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access, experimental)': '(requiere acceso a internet, experimental)', '(something like "it-it")': '(algo como "it-it")', +'(version %s)': '(version %s)', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(file **gluon/contrib/plural_rules/%s.py** is not found)', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', @@ -122,6 +123,7 @@ 'delete plugin': 'eliminar plugin', 'Delete this file (you will be asked to confirm deletion)': 'Elimine este fichero (se le pedirá confirmación)', 'Delete:': 'Elimine:', +'Demo': 'Demo', 'Deploy': 'Deploy', 'Deploy on Google App Engine': 'Instale en Google App Engine', 'Deploy to OpenShift': 'Instale en OpenShift', @@ -134,6 +136,7 @@ 'direction: ltr': 'direction: ltr', 'Disable': 'Deshabilitar', 'docs': 'docs', +'Docs': 'Docs', 'Done!': 'Done!', 'done!': 'listo!', 'Download': 'Descargar', @@ -157,6 +160,7 @@ 'Editing myemail': 'Editing myemail', 'Editing rbare': 'Editing rbare', 'Editing ul': 'Editing ul', +'Enable': 'Enable', 'Enterprise Web Framework': 'Armazón Empresarial para Internet', 'Error': 'Error', 'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"', @@ -190,6 +194,8 @@ 'First name': 'Nombre', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', 'Globals##debug': 'Globals', 'graph model': 'graph model', 'Group ID': 'ID de Grupo', @@ -213,6 +219,7 @@ 'internal error': 'error interno', 'Internal State': 'Estado Interno', 'Invalid action': 'Acción inválida', +'Invalid application name': 'Invalid application name', 'Invalid email': 'Correo inválido', 'invalid password': 'contraseña inválida', 'invalid password.': 'invalid password.', @@ -230,6 +237,7 @@ 'Last name': 'Apellido', 'Last saved on:': 'Guardado en:', 'License for': 'Licencia para', +'License:': 'License:', 'lists by ticket': 'lists by ticket', 'loading...': 'cargando...', 'locals': 'locals', @@ -250,6 +258,7 @@ 'new application "%s" created': 'nueva aplicación "%s" creada', 'New application wizard': 'Asistente para nueva aplicación', 'new plugin installed': 'nuevo plugin instalado', +'New plugin installed: %s': 'New plugin installed: %s', 'New plugin installed: web2py.plugin.attachment.w2p': 'New plugin installed: web2py.plugin.attachment.w2p', 'New plugin installed: web2py.plugin.dialog.w2p': 'New plugin installed: web2py.plugin.dialog.w2p', 'New plugin installed: web2py.plugin.math2py.w2p': 'New plugin installed: web2py.plugin.math2py.w2p', @@ -310,6 +319,8 @@ 'Removed Breakpoint on %s at line %s': 'Eliminado punto de ruptura en %s en la línea %s', 'Replace': 'Reemplazar', 'Replace All': 'Reemplazar todos', +'Repository (%s)': 'Repository (%s)', +'Repository: %s': 'Repository: %s', 'request': 'request', 'Resolve Conflict file': 'archivo Resolución de Conflicto', 'response': 'response', @@ -329,6 +340,8 @@ 'Save file: %s': 'Guardar: %s', 'Save via Ajax': 'Guardar via Ajax', 'Saved file hash:': 'Hash del archivo guardado:', +'Screenshot %s': 'Screenshot %s', +'Screenshots': 'Screenshots', 'selected': 'seleccionado(s)', 'session': 'session', 'session expired': 'sesión expirada', @@ -336,6 +349,7 @@ 'shell': 'shell', 'Site': 'sitio', 'some files could not be removed': 'algunos archivos no pudieron ser removidos', +'source : filesystem': 'source : filesystem', 'Start searching': 'Iniciar búsqueda', 'Start wizard': 'Iniciar asistente', 'state': 'estado', @@ -355,8 +369,8 @@ 'test': 'probar', 'Testing application': 'Probando aplicación', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "consulta" es una condición como "db.tabla1.campo1==\'valor\'". Algo como "db.tabla1.campo1==db.tabla2.campo2" resulta en un JOIN SQL.', -'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', 'The application logic, each URL path is mapped in one exposed function in the controller': 'La lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', +'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', 'the data representation, define database tables and sets': 'la representación de datos, define tablas y conjuntos de base de datos', 'The data representation, define database tables and sets': 'La representación de datos, define tablas y conjuntos de base de datos', 'The presentations layer, views are also known as templates': 'La capa de presentación, las vistas también son llamadas plantillas', diff --git a/applications/admin/models/access.py b/applications/admin/models/access.py index fee8d464..823ec438 100644 --- a/applications/admin/models/access.py +++ b/applications/admin/models/access.py @@ -144,7 +144,7 @@ if session.is_mobile == 'true': elif session.is_mobile == 'false': is_mobile = False else: - is_mobile = request.user_agent().is_mobile + is_mobile = request.user_agent().get('is_mobile',False) if DEMO_MODE: session.authorized = True diff --git a/applications/admin/models/buttons.py b/applications/admin/models/buttons.py index 41c8d7d2..67d9ec92 100644 --- a/applications/admin/models/buttons.py +++ b/applications/admin/models/buttons.py @@ -24,7 +24,7 @@ def button_enable(href, app): return A(label, _class='button btn', _id=id, callback=href, target=id) def sp_button(href, label): - if request.user_agent().is_mobile: + if request.user_agent().get('is_mobile'): ret = A_button(SPAN(label), _href=href) else: ret = A(SPAN(label), _class='button special btn btn-inverse', _href=href) @@ -37,4 +37,4 @@ def searchbox(elementid): return SPAN(LABEL(IMG(_id="search_start", _src=URL('static', 'images/search.png'), _alt=T('filter')), _class='icon', _for=elementid), ' ', INPUT(_id=elementid, _type='text', _size=12, _class="input-medium"), - _class="searchbox") \ No newline at end of file + _class="searchbox") diff --git a/applications/admin/static/css/web2py-codemirror.css b/applications/admin/static/css/web2py-codemirror.css index a11e8d88..39b5311b 100644 --- a/applications/admin/static/css/web2py-codemirror.css +++ b/applications/admin/static/css/web2py-codemirror.css @@ -1,7 +1,13 @@ +/* TODO rename this file as web2py-editor.css */ /* Fullscreen */ .CodeMirror-fullscreen { z-index: 1030; } +.CodeMirror { + border-top: 1px solid #ddd; + /*border-left: 1px solid #ddd;*/ + border-bottom: 1px solid #ddd; +} /* BREAKPOINTS */ @@ -36,3 +42,15 @@ /*.nav-tabs>li { min-width: 100px; }*/ + +#windows_divs > div { + position: fixed; + height: 30%; + left: 0; + background: white; + right: 0; + bottom: 41px; + z-index: 1030; + overflow: inherit; + border-top: 1px solid #ddd; +} diff --git a/applications/admin/static/js/ajax_editor.js b/applications/admin/static/js/ajax_editor.js index 2b355fe4..8a7916a4 100644 --- a/applications/admin/static/js/ajax_editor.js +++ b/applications/admin/static/js/ajax_editor.js @@ -248,7 +248,7 @@ function keepalive(url) { }); } -function load_file(url) { +function load_file(url, lineno) { $.ajax({ type: "GET", contentType: 'application/json', @@ -263,13 +263,13 @@ function load_file(url) { var tab_header = '
  • ' + json['realfilename'] + '
  • '; var tab_body = '
    ' + json['plain_html'] + '
    '; if(json['force'] === false) { - $('#filesTab').append($(tab_header)); - $('#myTabContent').append($(tab_body)); + $('#myTabContent').append($(tab_body)); // First load the body + $('#filesTab').append($(tab_header)); // Then load the header which trigger the shown event } else { $('#' + json['id']).html($(tab_body)); } } - $("a[href='#" + json['id'] + "']").click(); + $("a[href='#" + json['id'] + "']").trigger('click', lineno); } }, error: function (x) { diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index e714d3cc..e11610cb 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -485,8 +485,10 @@ el.addClass('disabled'); var method = el.prop('type') == 'submit' ? 'val' : 'html'; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; - /*store enabled state*/ - el.data('w2p:enable-with', el[method]()); + /*store enabled state if not already disabled */ + 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 * el.data('w2p_disable_with', el[method]()); @@ -633,7 +635,9 @@ if(disable_with == undefined) { element.data('w2p_disable_with', element[method]()) } - 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); }); @@ -647,7 +651,10 @@ 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')); + if(element.data('w2p:enable-with')) { + element[method](element.data('w2p:enable-with')); + element.removeData('w2p:enable-with'); + } element.prop('disabled', false); }); }, @@ -659,7 +666,18 @@ el.on('ajax:complete', 'form[data-w2p_target]', function (e) { web2py.enableFormElements($(this)); }); - } + }, + /* Invalidate and force reload of a web2py component + */ + invalidate: function(target) { + $('div[data-w2p_remote]', target).each(function () { + var el = $('#' + $(this).attr('id')).get(0); + if (el.timing !== undefined) { // Block triggering regular routines + clearInterval(el.timing); + } + }); + $.web2py.component_handler(target); + }, } /*end of functions */ @@ -679,7 +697,7 @@ /* compatibility code - start */ ajax = jQuery.web2py.ajax; web2py_component = jQuery.web2py.component; -web2py_websocket = jQuery.web2py.websocket; +web2py_websocket = jQuery.web2py.web2py_websocket; web2py_ajax_page = jQuery.web2py.ajax_page; /*needed for IS_STRONG(entropy)*/ web2py_validate_entropy = jQuery.web2py.validate_entropy; diff --git a/applications/admin/views/default/edit.html b/applications/admin/views/default/edit.html index 9187a4c4..4e0aeac2 100644 --- a/applications/admin/views/default/edit.html +++ b/applications/admin/views/default/edit.html @@ -1,4 +1,32 @@ {{extend 'layout.html'}} +{{ +dirs=[{'name':'models', 'reg':'.*\.py$'}, + {'name':'controllers', 'reg':'.*\.py$'}, + {'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'}, + {'name':'modules', 'reg':'.*\.py$'}, + {'name':'static', 'reg': '[^\.#].*'}] + +def file_create_form(location, anchor=None, helptext=""): + form=FORM( + LABEL(T("create file with filename:")), + SELECT(_name='dir', _style='width:100px;', + *[OPTION(dir['name'], _value=dir['name']) for dir in dirs]), + XML(' '),LABEL('/', _style='display:inline-block;'),XML(' '), + INPUT(_type="text",_name="filename",requires=IS_NOT_EMPTY(),_class=''), + TAG['SMALL'](helptext,_class="help-block"), + INPUT(_type='submit', name=T('filename'), _value=T('Create'), _style='display:block', _id='btn_file_create'), + INPUT(_type="hidden",_name="editor"), + INPUT(_type="hidden",_name="location",_value=location), + INPUT(_type="hidden",_name="sender",_value=URL('design',args=app)), + INPUT(_type="hidden",_name="token",_value=session.token), + #INPUT(_type="hidden",_name="id",_value=anchor), + _action=URL('create_file'), + _id='file_create_form', + _class="generatedbyw2p well well-small") + return form + +}} + {{ def shortcut(combo, description): @@ -45,6 +73,7 @@ + @@ -52,12 +81,17 @@ @@ -197,27 +232,22 @@ $(document).on('click', 'a.font_button', function (e) {
    @@ -227,13 +257,47 @@ $(document).on('click', 'a.font_button', function (e) {
    +
    +
    + {{=LOAD('default', 'todolist.load', vars={'app':app}, ajax=True, timeout=60000, times="infinity")}} +
    +
    +{{block footer}} + + + +{{end}} diff --git a/applications/admin/views/default/edit_js.html b/applications/admin/views/default/edit_js.html index 62e6fb54..571839b8 100644 --- a/applications/admin/views/default/edit_js.html +++ b/applications/admin/views/default/edit_js.html @@ -74,15 +74,15 @@ request.env['wsgi_url_scheme'], request.env['http_host'], URL(c='debug', f='toggle_breakpoint')))}}, sel); }); - function makeMarker() { - var marker = document.createElement("div"); - marker.style.color = "#822"; - marker.innerHTML = "●"; - marker.className = "breakpoint"; - return marker; - } + function makeMarker() { + var marker = document.createElement("div"); + marker.style.color = "#822"; + marker.innerHTML = "●"; + marker.className = "breakpoint"; + return marker; + } - {{if filetype in ('html', 'javascript', 'css'):}} + {{if filetype in ('html', 'javascript', 'css'):}} // must be here or break emmet/zencoding CodeMirror.defaults.extraKeys["Ctrl-S"] = function(instance) { @@ -94,26 +94,27 @@ CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) { if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); } - {{pass}} - {{if filetype=='python':}} - // must be here or break emmet/zencoding for python - CodeMirror.defaults.extraKeys["Ctrl-S"] = - function(instance) { - doClickSave();}; - CodeMirror.defaults.extraKeys["Ctrl-Space"] = "autocomplete"; - CodeMirror.defaults.extraKeys["Tab"] = "indentMore"; - CodeMirror.defaults.extraKeys["Shift-Tab"] = "indentLess"; - CodeMirror.defaults.extraKeys["Ctrl-F11"] = function(cm) { - cm.setOption("fullScreen", !cm.getOption("fullScreen")); - }, - CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) { - if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); - } - //for autocomplete - CodeMirror.commands.autocomplete = function(cm) { - CodeMirror.showHint(cm, CodeMirror.pythonHint); - } - {{pass}} + {{pass}} + {{if filetype=='python':}} + // must be here or break emmet/zencoding for python + CodeMirror.defaults.extraKeys["Ctrl-S"] = + function(instance) { + doClickSave();}; + CodeMirror.defaults.extraKeys["Ctrl-Space"] = "autocomplete"; + CodeMirror.defaults.extraKeys["Tab"] = "indentMore"; + CodeMirror.defaults.extraKeys["Shift-Tab"] = "indentLess"; + CodeMirror.defaults.extraKeys["Ctrl-F11"] = function(cm) { + cm.setOption("fullScreen", !cm.getOption("fullScreen")); + }, + CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) { + if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); + } + //for autocomplete + CodeMirror.commands.autocomplete = function(cm) { + CodeMirror.showHint(cm, CodeMirror.pythonHint); + } + {{pass}} + CodeMirror.defaults.extraKeys["Ctrl-/"] = "toggleComment"; store_changes_function = function(instance, changeObj) { jQuery(instance).data('saved', false); instance.off("change", store_changes_function); @@ -128,6 +129,19 @@ request.env['wsgi_url_scheme'], request.env['http_host'], URL(c='debug', f='list_breakpoints')))}}, editor); + // TODO move it in a separated file + CodeMirror.defineExtension("centerOnCursor", function(limit) { + var coords = this.cursorCoords(null, "local"); + if (this.getScrollerElement().clientHeight === 0 && limit !== 10) { + if (limit === undefined) limit = 1; + else limit += 1; + editor = this; + setTimeout(function() {editor.centerOnCursor()}, 100); + return; + } + clientHeight = (this.getScrollerElement().clientHeight / 2) + this.scrollTo(null, (coords.top + coords.bottom)/2 - 10); + });
    @@ -141,34 +155,26 @@
    - {{if filetype=='html':}} + {{if filetype=='html':}}

    {{=T('Key bindings for ZenCoding Plugin')}}

    -
      - {{=shortcut('Ctrl+S', T('Save via Ajax'))}} - {{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}} - {{=shortcut('Shift+Esc', T('Exit Fullscreen'))}} - {{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}} - {{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}} - {{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}} - {{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}} - {{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}} - {{=shortcut('Tab', T('Expand Abbreviation'))}} -
    - {{else:}} + {{else:}}

    {{=T("Key bindings")}}

    + {{pass}}
      {{=shortcut('Ctrl+S', T('Save via Ajax'))}} {{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}} {{=shortcut('Shift+Esc', T('Exit Fullscreen'))}} - {{if filetype=='python':}} - {{=shortcut('Ctrl-Space', T('Autocomplete Python Code'))}} - {{pass}} {{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}} {{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}} {{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}} {{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}} {{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}} -
    + {{=shortcut('Ctrl-/ ', T('Toggle comment'))}} + {{if filetype=='html':}} + {{=shortcut('Tab', T('Expand Abbreviation'))}} + {{elif filetype=='python':}} + {{=shortcut('Ctrl-Space', T('Autocomplete Python Code'))}} {{pass}} +
    diff --git a/applications/admin/views/default/files_menu.html b/applications/admin/views/default/files_menu.html new file mode 100644 index 00000000..85b55f22 --- /dev/null +++ b/applications/admin/views/default/files_menu.html @@ -0,0 +1,3 @@ +{{for files in result_files:}} + {{=files}} +{{pass}} \ No newline at end of file diff --git a/applications/admin/views/default/plugins.html b/applications/admin/views/default/plugins.html index 109c0fac..377f5da1 100644 --- a/applications/admin/views/default/plugins.html +++ b/applications/admin/views/default/plugins.html @@ -1,5 +1,21 @@ {{extend 'layout.html'}} {{=H3("Available plugins")}} {{=P("Source: web2pyslices")}} -{{=plugins}} +{{articles = []}} +{{for article in plugins:}} +{{screenshots = [A(" ", T("Screenshot %s") % (x+1), " ", _href=item) for (x, item) in enumerate(article["package_data"]["screenshots"])]}} +{{articles.append(TR(TD(H5(article["article"]["title"]), +button(URL(c="default", f="install_plugin", args=[app,], vars={"source": article["package_data"]["download"], "plugin": article["article"]["title"]}), T("Install")), +BR(), +IMG(_src="http://www.web2pyslices.com/download/%s" % article["article"]["thumbnail"], _style="margin-top: 1em;"), _style="width: 20em;"), +TD(article["article"]["description"], BR(), +A(T("Plugin page"), _href="http://www.web2pyslices.com/slice/show/%s/" % article["article"]["id"]), " | ", +A(T("Demo"), _href=article["package_data"]["demo"]), " | ", +A(T("Docs"), _href=article["package_data"]["documentation"]), " | ", +A(T("Repository (%s)") % article["package_data"]["repository_brand"], _href=article["package_data"]["repository_page"]), " | ", +A(T("License:"), " ", (article["package_data"]["license_type"] or "").upper(), " ", T("(version %s)") % article["package_data"]["license_version"] if article["package_data"]["license_version"] else "", _href=article["package_data"]["license_url"]), " | " if screenshots else "", *screenshots, _style="width: 40em;")) +) +}} +{{pass}} +{{=TABLE(*articles)}} diff --git a/applications/admin/views/default/todolist.load b/applications/admin/views/default/todolist.load new file mode 100644 index 00000000..9c3e3dcf --- /dev/null +++ b/applications/admin/views/default/todolist.load @@ -0,0 +1,18 @@ +
    + + {{n_todo=reduce(lambda x,y: x + len(y['matches']), todo, 0)}} +

    {{="%s TODO in %s" % (n_todo, app)}}

    + + +
    diff --git a/applications/admin/views/layout.html b/applications/admin/views/layout.html index d1bdbe37..4f3ab932 100644 --- a/applications/admin/views/layout.html +++ b/applications/admin/views/layout.html @@ -55,6 +55,7 @@ + {{block footer}} + {{end}} diff --git a/applications/admin/views/shell/index.html b/applications/admin/views/shell/index.html index cb510f91..c90da324 100644 --- a/applications/admin/views/shell/index.html +++ b/applications/admin/views/shell/index.html @@ -2,40 +2,41 @@ {{block sectionclass}}shell{{end}}
    -
    -
    - -
    -
    - - - -
    -
    -
      -
    • Using the shell may lock the database to other users of this app.
    • -
    • Each db statement is automatically committed.
    • -
    • Creating new tables dynamically is not allowed.
    • -
    • Models are automatically imported in the shell.
    • -
    -
    +
    +
    + +
    +
    + + + +
    +
    +
      +
    • Using the shell may lock the database to other users of this app.
    • +
    • Each db statement is automatically committed.
    • +
    • Creating new tables dynamically is not allowed.
    • +
    • Models are automatically imported in the shell.
    • +
    +
    +
    @@ -113,4 +114,4 @@ jQuery(document).ready(function(){ }); }); - \ No newline at end of file + diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index e714d3cc..e11610cb 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -485,8 +485,10 @@ el.addClass('disabled'); var method = el.prop('type') == 'submit' ? 'val' : 'html'; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; - /*store enabled state*/ - el.data('w2p:enable-with', el[method]()); + /*store enabled state if not already disabled */ + 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 * el.data('w2p_disable_with', el[method]()); @@ -633,7 +635,9 @@ if(disable_with == undefined) { element.data('w2p_disable_with', element[method]()) } - 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); }); @@ -647,7 +651,10 @@ 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')); + if(element.data('w2p:enable-with')) { + element[method](element.data('w2p:enable-with')); + element.removeData('w2p:enable-with'); + } element.prop('disabled', false); }); }, @@ -659,7 +666,18 @@ el.on('ajax:complete', 'form[data-w2p_target]', function (e) { web2py.enableFormElements($(this)); }); - } + }, + /* Invalidate and force reload of a web2py component + */ + invalidate: function(target) { + $('div[data-w2p_remote]', target).each(function () { + var el = $('#' + $(this).attr('id')).get(0); + if (el.timing !== undefined) { // Block triggering regular routines + clearInterval(el.timing); + } + }); + $.web2py.component_handler(target); + }, } /*end of functions */ @@ -679,7 +697,7 @@ /* compatibility code - start */ ajax = jQuery.web2py.ajax; web2py_component = jQuery.web2py.component; -web2py_websocket = jQuery.web2py.websocket; +web2py_websocket = jQuery.web2py.web2py_websocket; web2py_ajax_page = jQuery.web2py.ajax_page; /*needed for IS_STRONG(entropy)*/ web2py_validate_entropy = jQuery.web2py.validate_entropy; diff --git a/applications/examples/views/default/index.html b/applications/examples/views/default/index.html index 2bfec2a6..28852551 100644 --- a/applications/examples/views/default/index.html +++ b/applications/examples/views/default/index.html @@ -43,7 +43,8 @@ random.shuffle(quotes)
    Download Now
    Try it now online
    - Sites Powered by web2py + Sites Powered by web2py

    + Donate Bitcoins
    diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index e714d3cc..e11610cb 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -485,8 +485,10 @@ el.addClass('disabled'); var method = el.prop('type') == 'submit' ? 'val' : 'html'; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working..."; - /*store enabled state*/ - el.data('w2p:enable-with', el[method]()); + /*store enabled state if not already disabled */ + 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 * el.data('w2p_disable_with', el[method]()); @@ -633,7 +635,9 @@ if(disable_with == undefined) { element.data('w2p_disable_with', element[method]()) } - 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); }); @@ -647,7 +651,10 @@ 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')); + if(element.data('w2p:enable-with')) { + element[method](element.data('w2p:enable-with')); + element.removeData('w2p:enable-with'); + } element.prop('disabled', false); }); }, @@ -659,7 +666,18 @@ el.on('ajax:complete', 'form[data-w2p_target]', function (e) { web2py.enableFormElements($(this)); }); - } + }, + /* Invalidate and force reload of a web2py component + */ + invalidate: function(target) { + $('div[data-w2p_remote]', target).each(function () { + var el = $('#' + $(this).attr('id')).get(0); + if (el.timing !== undefined) { // Block triggering regular routines + clearInterval(el.timing); + } + }); + $.web2py.component_handler(target); + }, } /*end of functions */ @@ -679,7 +697,7 @@ /* compatibility code - start */ ajax = jQuery.web2py.ajax; web2py_component = jQuery.web2py.component; -web2py_websocket = jQuery.web2py.websocket; +web2py_websocket = jQuery.web2py.web2py_websocket; web2py_ajax_page = jQuery.web2py.ajax_page; /*needed for IS_STRONG(entropy)*/ web2py_validate_entropy = jQuery.web2py.validate_entropy; diff --git a/examples/app.example.yaml b/examples/app.example.yaml index b56a16e5..ff608af1 100644 --- a/examples/app.example.yaml +++ b/examples/app.example.yaml @@ -65,8 +65,8 @@ skip_files: | (.*\.py[co])| (.*/RCS/.*)| (\..*)| - (applications/(admin|examples)/.*)| - ((admin|examples|welcome)\.(w2p|tar))| + (applications/examples/.*)| + ((examples|welcome)\.(w2p|tar))| (applications/.*?/(cron|databases|errors|cache|sessions)/.*)| ((logs|scripts)/.*)| (anyserver\.py)| diff --git a/gluon/contrib/autolinks.py b/gluon/contrib/autolinks.py index af3d1247..23e9ed10 100644 --- a/gluon/contrib/autolinks.py +++ b/gluon/contrib/autolinks.py @@ -159,6 +159,8 @@ def extension(url): def expand_one(url, cdict): # try ombed but first check in cache + if '@' in url and not '://'in url: + return '%s' % (url, url) if cdict and url in cdict: r = cdict[url] else: diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index 2d3b2aa9..95f9dc56 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -551,7 +551,7 @@ regex_list=re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$') regex_bq_headline=re.compile('^(?:(\.+|\++|\-+)(\.)?\s+)?(-{3}-*)$') regex_tq=re.compile('^(-{3}-*)(?::(?P[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P

    [a-zA-Z][_a-zA-Z\-\d]*)\])?)?$') regex_proto = re.compile(r'(?/=])(?P

    \w+):(?P\w+://[\w\d\-+=?%&/:.]+)', re.M) -regex_auto = re.compile(r'(?/=])(?P\w+://[\w\d\-+_=?%&/:.,;#]+\w)',re.M) +regex_auto = re.compile(r'(?/=])(?P\w+://[\w\d\-+_=?%&/:.,;#]+\w|[\w\-.]+@[\w\-.]+)',re.M) regex_link=re.compile(r'('+LINK+r')|\[\[(?P.+?)\]\]',re.S) regex_link_level2=re.compile(r'^(?P\S.*?)?(?:\s+\[(?P.+?)\])?(?:\s+(?P\S+))?(?:\s+(?P

    popup))?\s*$',re.S) regex_media_level2=re.compile(r'^(?P\S.*?)?(?:\s+\[(?P.+?)\])?(?:\s+(?P\S+))?\s+(?P

    img|IMG|left|right|center|video|audio|blockleft|blockright)(?:\s+(?P\d+px))?\s*$',re.S) @@ -648,7 +648,9 @@ def autolinks_simple(url): image, video or audio tag """ u_url=url.lower() - if u_url.endswith(('.jpg','.jpeg','.gif','.png')): + if '@' in url and not '://' in url: + return '%s' % (url, url) + elif u_url.endswith(('.jpg','.jpeg','.gif','.png')): return '' % url elif u_url.endswith(('.mp4','.mpeg','.mov','.ogv')): return '' % url @@ -673,6 +675,9 @@ def protolinks_simple(proto, url): return 'QR Code'%url return proto+':'+url +def email_simple(email): + return '%s' % (email, email) + def render(text, extra={}, allowed={}, diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index 4d206036..0a224100 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -258,7 +258,7 @@ def run(history, statement, env={}): if not name.startswith('__'): try: history.set_global(name, val) - except TypeError, ex: + except (TypeError, cPickle.PicklingError), ex: UNPICKLABLE_TYPES.append(type(val)) history.add_unpicklable(statement, new_globals.keys()) diff --git a/gluon/contrib/spreadsheet.py b/gluon/contrib/spreadsheet.py index 1ff05e73..7cca9c3d 100644 --- a/gluon/contrib/spreadsheet.py +++ b/gluon/contrib/spreadsheet.py @@ -858,8 +858,8 @@ class Sheet: sorted_headers = [TH(), ] + \ [TH(header[1]) for header in sorted(unsorted_headers)] table.insert(0, TR(*sorted_headers, - **{_class: "%s_fieldnames" % - attributes["_class"]})) + **{'_class': "%s_fieldnames" % + attributes["_class"]})) else: data = SCRIPT(""" // web2py Spreadsheets: no db data.""") diff --git a/gluon/dal.py b/gluon/dal.py index 8dc63f7f..3d7b33d2 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -252,7 +252,8 @@ THREAD_LOCAL = threading.local() REGEX_TYPE = re.compile('^([\w\_\:]+)') REGEX_DBNAME = re.compile('^(\w+)(\:\w+)*') REGEX_W = re.compile('^\w+$') -REGEX_TABLE_DOT_FIELD = re.compile('^(\w+)\.(\w+)$') +REGEX_TABLE_DOT_FIELD = re.compile('^(\w+)\.([^.]+)$') +REGEX_NO_GREEDY_ENTITY_NAME = r'(.+?)' REGEX_UPLOAD_PATTERN = re.compile('(?P[\w\-]+)\.(?P[\w\-]+)\.(?P[\w\-]+)(\.(?P\w+))?\.\w+$') REGEX_CLEANUP_FN = re.compile('[\'"\s;]+') REGEX_UNPACK = re.compile('(?1: - # then it has to be a table level FK - if rtablename not in TFK: - TFK[rtablename] = {} - TFK[rtablename][rfieldname] = field_name - else: - ftype = ftype + \ - types['reference FK'] % dict( - constraint_name = constraint_name, # should be quoted - foreign_key = '%s (%s)' % (rtablename, - rfieldname), - table_name = tablename, - field_name = field._rname or field.name, - on_delete_action=field.ondelete) + except Exception, e: + LOGGER.debug('Error: %s' %e) + raise KeyError('Cannot resolve reference %s in %s definition' % (referenced, table._tablename)) + + # must be PK reference or unique + if getattr(rtable, '_primarykey', None) and rfieldname in rtable._primarykey or \ + rfield.unique: + ftype = types[rfield.type[:9]] % \ + dict(length=rfield.length) + # multicolumn primary key reference? + if not rfield.unique and len(rtable._primarykey)>1: + # then it has to be a table level FK + if rtablename not in TFK: + TFK[rtablename] = {} + TFK[rtablename][rfieldname] = field_name else: - # make a guess here for circular references - if referenced in db: - id_fieldname = db[referenced]._id.name - elif referenced == tablename: - id_fieldname = table._id.name - else: #make a guess - id_fieldname = 'id' - #gotcha: the referenced table must be defined before - #the referencing one to be able to create the table - #Also if it's not recommended, we can still support - #references to tablenames without rname to make - #migrations and model relationship work also if tables - #are not defined in order - real_referenced = ( - (db[referenced]._rname or db[referenced]) - if referenced == tablename or referenced in db - else referenced) - - ftype = types[field_type[:9]] % dict( - index_name = field_name+'__idx', - field_name = field._rname or field.name, - constraint_name = constraint_name, - foreign_key = '%s (%s)' % (real_referenced, - id_fieldname), - on_delete_action=field.ondelete) + ftype = ftype + \ + types['reference FK'] % dict( + constraint_name = constraint_name, # should be quoted + foreign_key = rtable.sqlsafe + ' (' + rfield.sqlsafe_name + ')', + table_name = table.sqlsafe, + field_name = field.sqlsafe_name, + on_delete_action=field.ondelete) + else: + # make a guess here for circular references + if referenced in db: + id_fieldname = db[referenced]._id.sqlsafe_name + elif referenced == tablename: + id_fieldname = table._id.sqlsafe_name + else: #make a guess + id_fieldname = self.QUOTE_TEMPLATE % 'id' + #gotcha: the referenced table must be defined before + #the referencing one to be able to create the table + #Also if it's not recommended, we can still support + #references to tablenames without rname to make + #migrations and model relationship work also if tables + #are not defined in order + if referenced == tablename: + real_referenced = db[referenced].sqlsafe + else: + real_referenced = (referenced in db + and db[referenced].sqlsafe + or referenced) + rfield = db[referenced]._id + ftype = types[field_type[:9]] % dict( + index_name = self.QUOTE_TEMPLATE % (field_name+'__idx'), + field_name = field.sqlsafe_name, + constraint_name = self.QUOTE_TEMPLATE % constraint_name, + foreign_key = '%s (%s)' % (real_referenced, rfield.sqlsafe_name), + on_delete_action=field.ondelete) elif field_type.startswith('list:reference'): ftype = types[field_type[:14]] elif field_type.startswith('decimal'): @@ -995,39 +1026,38 @@ class BaseAdapter(ConnectionPool): # geometry fields are added after the table has been created, not now if not (self.dbengine == 'postgres' and \ field_type.startswith('geom')): - #fetch the rname if it's there - field_rname = "%s" % (field._rname or field_name) - fields.append('%s %s' % (field_rname, ftype)) + fields.append('%s %s' % (field.sqlsafe_name, ftype)) other = ';' # backend-specific extensions to fields if self.dbengine == 'mysql': if not hasattr(table, "_primarykey"): - fields.append('PRIMARY KEY(%s)' % (table._id.name or table._id._rname)) - other = ' ENGINE=InnoDB CHARACTER SET utf8;' + fields.append('PRIMARY KEY (%s)' % (self.QUOTE_TEMPLATE % table._id.name)) + engine = self.adapter_args.get('engine','InnoDB') + other = ' ENGINE=%s CHARACTER SET utf8;' % engine fields = ',\n '.join(fields) for rtablename in TFK: rfields = TFK[rtablename] - pkeys = db[rtablename]._primarykey - fkeys = [ rfields[k] for k in pkeys ] + pkeys = [self.QUOTE_TEMPLATE % pk for pk in db[rtablename]._primarykey] + fkeys = [self.QUOTE_TEMPLATE % rfields[k].name for k in pkeys ] fields = fields + ',\n ' + \ types['reference TFK'] % dict( - table_name = tablename, + table_name = table.sqlsafe, field_name=', '.join(fkeys), - foreign_table = rtablename, + foreign_table = table.sqlsafe, foreign_key = ', '.join(pkeys), on_delete_action = field.ondelete) - #if there's a _rname, let's use that instead - table_rname = table._rname or tablename + + table_rname = table.sqlsafe if getattr(table,'_primarykey',None): query = "CREATE TABLE %s(\n %s,\n %s) %s" % \ - (table_rname, fields, - self.PRIMARY_KEY(', '.join(table._primarykey)),other) + (table.sqlsafe, fields, + self.PRIMARY_KEY(', '.join([self.QUOTE_TEMPLATE % pk for pk in table._primarykey])),other) else: query = "CREATE TABLE %s(\n %s\n)%s" % \ - (table_rname, fields, other) + (table.sqlsafe, fields, other) if self.uri.startswith('sqlite:///') \ or self.uri.startswith('spatialite:///'): @@ -1105,6 +1135,7 @@ class BaseAdapter(ConnectionPool): k,v=item if not isinstance(v,dict): v=dict(type='unknown',sql=v) + if self.ignore_field_case is not True: return k, v return k.lower(),v # make sure all field names are lower case to avoid # migrations because of case cahnge @@ -1132,7 +1163,7 @@ class BaseAdapter(ConnectionPool): query = [ sql_fields[key]['sql'] ] else: query = ['ALTER TABLE %s ADD %s %s;' % \ - (tablename, key, + (table.sqlsafe, key, sql_fields_aux[key]['sql'].replace(', ', new_add))] metadata_change = True elif self.dbengine in ('sqlite', 'spatialite'): @@ -1150,10 +1181,11 @@ class BaseAdapter(ConnectionPool): "'%(table)s', '%(field)s');" % dict(schema=schema, table=tablename, field=key,) ] elif self.dbengine in ('firebird',): - query = ['ALTER TABLE %s DROP %s;' % (tablename, key)] + query = ['ALTER TABLE %s DROP %s;' % + (self.QUOTE_TEMPLATE % tablename, self.QUOTE_TEMPLATE % key)] else: query = ['ALTER TABLE %s DROP COLUMN %s;' % - (tablename, key)] + (self.QUOTE_TEMPLATE % tablename, self.QUOTE_TEMPLATE % key)] metadata_change = True elif sql_fields[key]['sql'] != sql_fields_old[key]['sql'] \ and not (key in table.fields and @@ -1169,12 +1201,15 @@ class BaseAdapter(ConnectionPool): else: drop_expr = 'ALTER TABLE %s DROP COLUMN %s;' key_tmp = key + '__tmp' - query = ['ALTER TABLE %s ADD %s %s;' % (t, key_tmp, tt), - 'UPDATE %s SET %s=%s;' % (t, key_tmp, key), - drop_expr % (t, key), - 'ALTER TABLE %s ADD %s %s;' % (t, key, tt), - 'UPDATE %s SET %s=%s;' % (t, key, key_tmp), - drop_expr % (t, key_tmp)] + query = ['ALTER TABLE %s ADD %s %s;' % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp, tt), + 'UPDATE %s SET %s=%s;' % + (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp, self.QUOTE_TEMPLATE % key), + drop_expr % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key), + 'ALTER TABLE %s ADD %s %s;' % + (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key, tt), + 'UPDATE %s SET %s=%s;' % + (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key, self.QUOTE_TEMPLATE % key_tmp), + drop_expr % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp)] metadata_change = True elif sql_fields[key]['type'] != sql_fields_old[key]['type']: sql_fields_current[key] = sql_fields[key] @@ -1269,8 +1304,7 @@ class BaseAdapter(ConnectionPool): return 'PRIMARY KEY(%s)' % key def _drop(self, table, mode): - table_rname = table._rname or table - return ['DROP TABLE %s;' % table_rname] + return ['DROP TABLE %s;' % table.sqlsafe] def drop(self, table, mode=''): db = table._db @@ -1288,17 +1322,16 @@ class BaseAdapter(ConnectionPool): self.log('success!\n', table) def _insert(self, table, fields): - table_rname = table._rname or table + table_rname = table.sqlsafe if fields: - keys = ','.join(f._rname or f.name for f, v in fields) + keys = ','.join(f.sqlsafe_name for f, v in fields) values = ','.join(self.expand(v, f.type) for f, v in fields) return 'INSERT INTO %s(%s) VALUES (%s);' % (table_rname, keys, values) else: return self._insert_empty(table) def _insert_empty(self, table): - table_rname = table._rname or table - return 'INSERT INTO %s DEFAULT VALUES;' % table_rname + return 'INSERT INTO %s DEFAULT VALUES;' % (table.sqlsafe) def insert(self, table, fields): query = self._insert(table,fields) @@ -1428,7 +1461,7 @@ class BaseAdapter(ConnectionPool): return '(%s)' % ' || '.join(self.expand(x,'string') for x in items) def ADD(self, first, second): - if self.is_numerical_type(first.type): + if self.is_numerical_type(first.type) or isinstance(first.type, gluon.dal.Field): return '(%s + %s)' % (self.expand(first), self.expand(second, first.type)) else: @@ -1451,13 +1484,13 @@ class BaseAdapter(ConnectionPool): self.expand(second, first.type)) def AS(self, first, second): - return '%s AS %s' % (self.expand(first), second) + return '%s AS %s' % (self.expand(first), second) def ON(self, first, second): - table_rname = first._ot and first or first._rname or first._tablename + table_rname = self.table_alias(first) if use_common_filters(second): second = self.common_filter(second,[first._tablename]) - return '%s ON %s' % (self.expand(table_rname), self.expand(second)) + return ('%s ON %s') % (self.expand(table_rname), self.expand(second)) def INVERT(self, first): return '%s DESC' % self.expand(first) @@ -1472,13 +1505,10 @@ class BaseAdapter(ConnectionPool): if isinstance(expression, Field): et = expression.table if not colnames: - table_rname = et._ot and et._tablename or et._rname or et._tablename + table_rname = et._ot and self.QUOTE_TEMPLATE % et._tablename or et._rname or self.QUOTE_TEMPLATE % et._tablename + out = '%s.%s' % (table_rname, expression._rname or (self.QUOTE_TEMPLATE % (expression.name))) else: - table_rname = et._tablename - if not colnames: - out = '%s.%s' % (table_rname, expression._rname or expression.name) - else: - out = '%s.%s' % (table_rname, expression.name) + out = '%s.%s' % (self.QUOTE_TEMPLATE % et._tablename, self.QUOTE_TEMPLATE % expression.name) if field_type == 'string' and not expression.type in ( 'string','text','json','password'): out = self.CAST(out, self.types['text']) @@ -1509,10 +1539,11 @@ class BaseAdapter(ConnectionPool): else: return str(expression) - def table_alias(self,name): - if not isinstance(name, Table): - name = self.db[name]._rname or self.db[name] - return str(name) + def table_alias(self, tbl): + if not isinstance(tbl, Table): + tbl = self.db[tbl] + return tbl.sqlsafe_alias + def alias(self, table, alias): """ @@ -1520,7 +1551,7 @@ class BaseAdapter(ConnectionPool): with alias name. """ other = copy.copy(table) - other['_ot'] = other._ot or other._rname or other._tablename + other['_ot'] = other._ot or other.sqlsafe other['ALL'] = SQLALL(other) other['_tablename'] = alias for fieldname in other.fields: @@ -1532,8 +1563,7 @@ class BaseAdapter(ConnectionPool): return other def _truncate(self, table, mode=''): - tablename = table._rname or table._tablename - return ['TRUNCATE TABLE %s %s;' % (tablename, mode or '')] + return ['TRUNCATE TABLE %s %s;' % (table.sqlsafe, mode or '')] def truncate(self, table, mode= ' '): # Prepare functions "write_to_logfile" and "close_logfile" @@ -1553,10 +1583,10 @@ class BaseAdapter(ConnectionPool): sql_w = ' WHERE ' + self.expand(query) else: sql_w = '' - sql_v = ','.join(['%s=%s' % (field._rname or field.name, + sql_v = ','.join(['%s=%s' % (field.sqlsafe_name, self.expand(value, field.type)) \ for (field, value) in fields]) - tablename = "%s" % (self.db[tablename]._rname or tablename) + tablename = self.db[tablename].sqlsafe return 'UPDATE %s SET %s%s;' % (tablename, sql_v, sql_w) def update(self, tablename, query, fields): @@ -1581,7 +1611,7 @@ class BaseAdapter(ConnectionPool): sql_w = ' WHERE ' + self.expand(query) else: sql_w = '' - tablename = '%s' % (self.db[tablename]._rname or tablename) + tablename = self.db[tablename].sqlsafe return 'DELETE FROM %s%s;' % (tablename, sql_w) def delete(self, tablename, query): @@ -1623,8 +1653,9 @@ class BaseAdapter(ConnectionPool): if isinstance(item,SQLALL): new_fields += item._table elif isinstance(item,str): - if REGEX_TABLE_DOT_FIELD.match(item): - tablename,fieldname = item.split('.') + m = self.REGEX_TABLE_DOT_FIELD.match(item) + if m: + tablename,fieldname = m.groups() append(db[tablename][fieldname]) else: append(Expression(db,lambda item=item:item)) @@ -1645,10 +1676,11 @@ class BaseAdapter(ConnectionPool): tablenames = tables(query) tablenames_for_common_filters = tablenames for field in fields: - if isinstance(field, basestring) \ - and REGEX_TABLE_DOT_FIELD.match(field): - tn,fn = field.split('.') - field = self.db[tn][fn] + if isinstance(field, basestring): + m = self.REGEX_TABLE_DOT_FIELD.match(field) + if m: + tn,fn = m.groups() + field = self.db[tn][fn] for tablename in tables(field): if not tablename in tablenames: tablenames.append(tablename) @@ -1732,7 +1764,7 @@ class BaseAdapter(ConnectionPool): tables_to_merge.keys()]) if joint: sql_t += ' %s %s' % (command, - ','.join([self.table_alias(t) for t in joint])) + ','.join([t for t in joint])) for t in joinon: sql_t += ' %s %s' % (command, t) elif inner_join and left: @@ -1747,7 +1779,7 @@ class BaseAdapter(ConnectionPool): sql_t += ' %s %s' % (icommand, t) if joint: sql_t += ' %s %s' % (command, - ','.join([self.table_alias(t) for t in joint])) + ','.join([t for t in joint])) for t in joinon: sql_t += ' %s %s' % (command, t) else: @@ -1767,9 +1799,9 @@ class BaseAdapter(ConnectionPool): sql_o += ' ORDER BY %s' % self.expand(orderby) if (limitby and not groupby and tablenames and orderby_on_limitby and not orderby): sql_o += ' ORDER BY %s' % ', '.join( - ['%s.%s'%(t,x) for t in tablenames for x in ( + [self.db[t][x].sqlsafe for t in tablenames for x in ( hasattr(self.db[t],'_primarykey') and self.db[t]._primarykey - or [self.db[t]._id._rname or self.db[t]._id.name] + or ['_id'] ) ] ) @@ -1897,8 +1929,10 @@ class BaseAdapter(ConnectionPool): def create_sequence_and_triggers(self, query, table, **args): self.execute(query) + def log_execute(self, *a, **b): + if not self.connection: raise ValueError(a[0]) if not self.connection: return None command = a[0] if hasattr(self,'filter_sql_command'): @@ -1955,8 +1989,17 @@ class BaseAdapter(ConnectionPool): if field_is_type('decimal'): return str(obj) elif field_is_type('reference'): # reference - if fieldtype.find('.')>0: - return repr(obj) + # check for tablename first + referenced = fieldtype[9:].strip() + if referenced in self.db.tables: + return str(long(obj)) + p = referenced.partition('.') + if p[2] != '': + try: + ftype = self.db[p[0]][p[2]].type + return self.represent(obj, ftype) + except (ValueError, KeyError): + return repr(obj) elif isinstance(obj, (Row, Reference)): return str(obj['id']) return str(long(obj)) @@ -2162,14 +2205,15 @@ class BaseAdapter(ConnectionPool): new_rows = [] tmps = [] for colname in colnames: - if not REGEX_TABLE_DOT_FIELD.match(colname): + col_m = self.REGEX_TABLE_DOT_FIELD.match(colname) + if not col_m: tmps.append(None) else: - (tablename, _the_sep_, fieldname) = colname.partition('.') + tablename, fieldname = col_m.groups() table = db[tablename] field = table[fieldname] ft = field.type - tmps.append((tablename,fieldname,table,field,ft)) + tmps.append((tablename, fieldname, table, field, ft)) for (i,row) in enumerate(rows): new_row = Row() for (j,colname) in enumerate(colnames): @@ -2177,9 +2221,8 @@ class BaseAdapter(ConnectionPool): tmp = tmps[j] if tmp: (tablename,fieldname,table,field,ft) = tmp - if tablename in new_row: - colset = new_row[tablename] - else: + colset = new_row.get(tablename, None) + if colset is None: colset = new_row[tablename] = Row() if tablename not in virtualtables: virtualtables.append(tablename) @@ -2287,6 +2330,14 @@ class BaseAdapter(ConnectionPool): return Expression(self.db,'CASE WHEN %s THEN %s ELSE %s END' % \ (self.expand(query),represent(t),represent(f))) + def sqlsafe_table(self, tablename, ot=None): + if ot is not None: + return ('%s AS ' + self.QUOTE_TEMPLATE) % (ot, tablename) + return self.QUOTE_TEMPLATE % tablename + + def sqlsafe_field(self, fieldname): + return self.QUOTE_TEMPLATE % fieldname + ################################################################################### # List of all the available adapters; they all extend BaseAdapter. ################################################################################### @@ -2564,6 +2615,7 @@ class MySQLAdapter(BaseAdapter): 'list:reference': 'LONGTEXT', 'big-id': 'BIGINT AUTO_INCREMENT NOT NULL', 'big-reference': 'BIGINT, INDEX %(index_name)s (%(field_name)s), FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', + 'reference FK': ', CONSTRAINT `FK_%(constraint_name)s` FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', } QUOTE_TEMPLATE = "`%s`" @@ -2590,12 +2642,12 @@ class MySQLAdapter(BaseAdapter): def _drop(self,table,mode): # breaks db integrity but without this mysql does not drop table - table_rname = table._rname or table + table_rname = table.sqlsafe return ['SET FOREIGN_KEY_CHECKS=0;','DROP TABLE %s;' % table_rname, 'SET FOREIGN_KEY_CHECKS=1;'] def _insert_empty(self, table): - return 'INSERT INTO %s VALUES (DEFAULT);' % table + return 'INSERT INTO %s VALUES (DEFAULT);' % (table.sqlsafe) def distributed_transaction_begin(self,key): self.execute('XA START;') @@ -2668,6 +2720,8 @@ class MySQLAdapter(BaseAdapter): class PostgreSQLAdapter(BaseAdapter): drivers = ('psycopg2','pg8000') + QUOTE_TEMPLATE = '"%s"' + support_distributed_transaction = True types = { 'boolean': 'CHAR(1)', @@ -2694,12 +2748,11 @@ class PostgreSQLAdapter(BaseAdapter): 'geography': 'GEOGRAPHY', 'big-id': 'BIGSERIAL PRIMARY KEY', 'big-reference': 'BIGINT REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', - 'reference FK': ', CONSTRAINT FK_%(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', - 'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s', + 'reference FK': ', CONSTRAINT "FK_%(constraint_name)s" FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', + 'reference TFK': ' CONSTRAINT "FK_%(foreign_table)s_PK" FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s', } - QUOTE_TEMPLATE = '"%s"' def varquote(self,name): return varquote_aux(name,'"%s"') @@ -2713,7 +2766,7 @@ class PostgreSQLAdapter(BaseAdapter): return "'%s'" % str(obj).replace("'","''") def sequence_name(self,table): - return '%s_id_seq' % table + return self.QUOTE_TEMPLATE % (table + '_id_seq') def RANDOM(self): return 'RANDOM()' @@ -2802,8 +2855,8 @@ class PostgreSQLAdapter(BaseAdapter): self.execute("SET standard_conforming_strings=on;") self.try_json() - def lastrowid(self,table): - self.execute("""select currval('"%s"')""" % table._sequence_name) + def lastrowid(self,table = None): + self.execute("select lastval()") return int(self.cursor.fetchone()[0]) def try_json(self): @@ -2925,6 +2978,14 @@ class PostgreSQLAdapter(BaseAdapter): """ return 'ST_Within(%s,%s)' %(self.expand(first), self.expand(second, first.type)) + def ST_DWITHIN(self, first, (second, third)): + """ + http://postgis.org/docs/ST_DWithin.html + """ + return 'ST_DWithin(%s,%s,%s)' %(self.expand(first), + self.expand(second, first.type), + self.expand(third, 'double')) + def represent(self, obj, fieldtype): field_is_type = fieldtype.startswith if field_is_type('geo'): @@ -2942,6 +3003,11 @@ class PostgreSQLAdapter(BaseAdapter): return value return BaseAdapter.represent(self, obj, fieldtype) + def _drop(self, table, mode='restrict'): + if mode not in ['restrict', 'cascade', '']: + raise ValueError('Invalid mode: %s' % mode) + return ['DROP TABLE ' + table.sqlsafe + ' ' + str(mode) + ';'] + class NewPostgreSQLAdapter(PostgreSQLAdapter): drivers = ('psycopg2','pg8000') @@ -3074,8 +3140,6 @@ class OracleAdapter(BaseAdapter): 'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s', } - def sequence_name(self,tablename): - return '%s_sequence' % tablename def trigger_name(self,tablename): return '%s_trigger' % tablename @@ -3091,7 +3155,7 @@ class OracleAdapter(BaseAdapter): def _drop(self,table,mode): sequence_name = table._sequence_name - return ['DROP TABLE %s %s;' % (table, mode), 'DROP SEQUENCE %s;' % sequence_name] + return ['DROP TABLE %s %s;' % (table.sqlsafe, mode), 'DROP SEQUENCE %s;' % sequence_name] def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby): if limitby: @@ -3218,11 +3282,18 @@ class OracleAdapter(BaseAdapter): else: return self.cursor.fetchall() + def sqlsafe_table(self, tablename, ot=None): + if ot is not None: + return (self.QUOTE_TEMPLATE + ' ' \ + + self.QUOTE_TEMPLATE) % (ot, tablename) + return self.QUOTE_TEMPLATE % tablename + + class MSSQLAdapter(BaseAdapter): drivers = ('pyodbc',) T_SEP = 'T' - - QUOTE_TEMPLATE = "[%s]" + + QUOTE_TEMPLATE = '"%s"' types = { 'boolean': 'BIT', @@ -3685,7 +3756,7 @@ class FireBirdAdapter(BaseAdapter): } def sequence_name(self,tablename): - return 'genid_%s' % tablename + return ('genid_' + self.QUOTE_TEMPLATE) % tablename def trigger_name(self,tablename): return 'trg_id_%s' % tablename @@ -3714,7 +3785,7 @@ class FireBirdAdapter(BaseAdapter): def _drop(self,table,mode): sequence_name = table._sequence_name - return ['DROP TABLE %s %s;' % (table, mode), 'DROP GENERATOR %s;' % sequence_name] + return ['DROP TABLE %s %s;' % (table.sqlsafe, mode), 'DROP GENERATOR %s;' % sequence_name] def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby): if limitby: @@ -4283,7 +4354,7 @@ class SAPDBAdapter(BaseAdapter): } def sequence_name(self,table): - return '%s_id_Seq' % table + return (self.QUOTE_TEMPLATE + '_id_Seq') % table def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby): if limitby: @@ -4548,6 +4619,7 @@ class GoogleSQLAdapter(UseDatabaseStoredFile,MySQLAdapter): class NoSQLAdapter(BaseAdapter): can_select_for_update = False + QUOTE_TEMPLATE = '%s' @staticmethod def to_unicode(obj): @@ -4737,6 +4809,8 @@ class GoogleDatastoreAdapter(NoSQLAdapter): uploads_in_blob = True types = {} + # reconnect is not required for Datastore dbs + reconnect = lambda *args, **kwargs: None def file_exists(self, filename): pass def file_open(self, filename, mode='rb', lock=True): pass @@ -4839,12 +4913,10 @@ class GoogleDatastoreAdapter(NoSQLAdapter): elif field_type.startswith('reference'): if field.notnull: attr = dict(required=True) - referenced = field_type[10:].strip() - ftype = self.types[field_type[:9]](referenced, **attr) + ftype = self.types[field_type[:9]](**attr) elif field_type.startswith('list:reference'): if field.notnull: attr['required'] = True - referenced = field_type[15:].strip() ftype = self.types[field_type[:14]](**attr) elif field_type.startswith('list:'): ftype = self.types[field_type](**attr) @@ -5168,6 +5240,12 @@ class GoogleDatastoreAdapter(NoSQLAdapter): processor = attributes.get('processor',self.parse) return processor(rows,fields,colnames,False) + def parse_list_integers(self, value, field_type): + return value[:] if self.use_ndb else value + + def parse_list_strings(self, value, field_type): + return value[:] if self.use_ndb else value + def count(self,query,distinct=None,limit=None): if distinct: raise RuntimeError("COUNT DISTINCT not supported") @@ -5442,8 +5520,8 @@ def cleanup(text): """ validates that the given text is clean: only contains [0-9a-zA-Z_] """ - if not REGEX_ALPHANUMERIC.match(text): - raise SyntaxError('invalid table or field name: %s' % text) + #if not REGEX_ALPHANUMERIC.match(text): + # raise SyntaxError('invalid table or field name: %s' % text) return text class MongoDBAdapter(NoSQLAdapter): @@ -5582,9 +5660,6 @@ class MongoDBAdapter(NoSQLAdapter): value = obj else: value = NoSQLAdapter.represent(self, obj, fieldtype) - if isinstance(obj, (list, tuple)) and \ - (not fieldtype == "json" or fieldtype.startswith('list:')): - return value # reference types must be convert to ObjectID if fieldtype =='date': if value == None: @@ -7245,12 +7320,32 @@ class Row(object): __init__ = lambda self,*args,**kwargs: self.__dict__.update(*args,**kwargs) def __getitem__(self, k): + if isinstance(k, Table): + try: + return ogetattr(self, k._tablename) + except (KeyError,AttributeError,TypeError): + pass + elif isinstance(k, Field): + try: + return ogetattr(self, k.name) + except (KeyError,AttributeError,TypeError): + pass + try: + return ogetattr(ogetattr(self, k.tablename), k.name) + except (KeyError,AttributeError,TypeError): + pass + key=str(k) - _extra = self.__dict__.get('_extra', None) + _extra = ogetattr(self, '__dict__').get('_extra', None) if _extra is not None: v = _extra.get(key, DEFAULT) if v != DEFAULT: return v + try: + return ogetattr(self, key) + except (KeyError,AttributeError,TypeError): + pass + m = REGEX_TABLE_DOT_FIELD.match(key) if m: try: @@ -7652,7 +7747,7 @@ class DAL(object): adapter_args=None, attempts=5, auto_import=False, bigint_id=False, debug=False, lazy_tables=False, db_uid=None, do_connect=True, - after_connection=None, tables=None): + after_connection=None, tables=None, ignore_field_case=True): """ Creates a new Database Abstraction Layer instance. @@ -7737,6 +7832,7 @@ class DAL(object): self._decode_credentials = decode_credentials self._attempts = attempts self._do_connect = do_connect + self._ignore_field_case = ignore_field_case if not str(attempts).isdigit() or attempts < 0: attempts = 5 @@ -7768,6 +7864,7 @@ class DAL(object): # copy so multiple DAL() possible self._adapter.types = copy.copy(types) self._adapter.build_parsemap() + self._adapter.ignore_field_case = ignore_field_case if bigint_id: if 'big-id' in types and 'reference' in types: self._adapter.types['id'] = types['big-id'] @@ -7810,11 +7907,11 @@ class DAL(object): def import_table_definitions(self, path, migrate=False, fake_migrate=False, tables=None): - pattern = pjoin(path,self._uri_hash+'_*.table') if tables: for table in tables: self.define_table(**table) else: + pattern = pjoin(path,self._uri_hash+'_*.table') for filename in glob.glob(pattern): tfile = self._adapter.file_open(filename, 'r') try: @@ -8565,13 +8662,8 @@ class Table(object): self._tablename = tablename self._ot = None # args.get('rname') self._rname = args.get('rname') - if not self._rname: - self._sequence_name = args.get('sequence_name') or \ - db and db._adapter.sequence_name(tablename) - else: - tb = self._rname[1:-1] - self._sequence_name = args.get('sequence_name') or \ - db and db._adapter.sequence_name(tb) + self._sequence_name = args.get('sequence_name') or \ + db and db._adapter.sequence_name(self._rname or tablename) self._trigger_name = args.get('trigger_name') or \ db and db._adapter.trigger_name(tablename) self._common_filter = args.get('common_filter') @@ -8652,7 +8744,7 @@ class Table(object): fields.append(Field(fn,'blob',default='', writable=False,readable=False)) - lower_fieldnames = set() + fieldnames_set = set() reserved = dir(Table) + ['fields'] if (db and db.check_reserved): check_reserved = db.check_reserved_keyword @@ -8663,12 +8755,15 @@ class Table(object): for field in fields: field_name = field.name check_reserved(field_name) - fn_lower = field_name.lower() - if fn_lower in lower_fieldnames: + if db and db._ignore_field_case: + fname_item = field_name.lower() + else: + fname_item = field_name + if fname_item in fieldnames_set: raise SyntaxError("duplicate field %s in table %s" \ % (field_name, tablename)) else: - lower_fieldnames.add(fn_lower) + fieldnames_set.add(fname_item) self.fields.append(field_name) self[field_name] = field @@ -8910,8 +9005,23 @@ class Table(object): if 'Oracle' in str(type(self._db._adapter)): return '%s %s' % (ot, self._tablename) return '%s AS %s' % (ot, self._tablename) + return self._tablename + @property + def sqlsafe(self): + rname = self._rname + if rname: return rname + return self._db._adapter.sqlsafe_table(self._tablename) + + @property + def sqlsafe_alias(self): + rname = self._rname + ot = self._ot + if rname and not ot: return rname + return self._db._adapter.sqlsafe_table(self._tablename, self._ot) + + def _drop(self, mode = ''): return self._db._adapter._drop(self, mode) @@ -9566,6 +9676,10 @@ class Expression(object): db = self.db return Query(db, db._adapter.ST_WITHIN, self, value) + def st_dwithin(self, value, distance): + db = self.db + return Query(db, db._adapter.ST_DWITHIN, self, (value, distance)) + # for use in both Query and sortby @@ -9738,7 +9852,12 @@ class Field(Expression): if not isinstance(fieldname, str) or hasattr(Table, fieldname) or \ fieldname[0] == '_' or REGEX_PYTHON_KEYWORDS.match(fieldname): raise SyntaxError('Field: invalid field name: %s' % fieldname) - self.type = type if not isinstance(type, (Table,Field)) else 'reference %s' % type + + if not isinstance(type, (Table,Field)): + self.type = type + else: + self.type = 'reference %s' % type + self.length = length if not length is None else DEFAULTLENGTH.get(self.type,512) self.default = default if default!=DEFAULT else (update or None) self.required = required # is this field required @@ -10004,6 +10123,16 @@ class Field(Expression): except: return '.%s' % self.name + @property + def sqlsafe(self): + if self._table: + return self._table.sqlsafe + '.' + (self._rname or self._db._adapter.sqlsafe_field(self.name)) + return '.%s' % self.name + + @property + def sqlsafe_name(self): + return self._rname or self._db._adapter.sqlsafe_field(self.name) + class Query(object): @@ -10356,7 +10485,7 @@ class Set(object): fields = table._listify(update_fields,update=True) if not fields: raise SyntaxError("No fields to update") - ret = db._adapter.update("%s" % table,self.query,fields) + ret = db._adapter.update("%s" % table._tablename,self.query,fields) ret and [f(self,update_fields) for f in table._after_update] return ret @@ -10869,11 +10998,21 @@ class Rows(object): represent = kwargs.get('represent', False) writer = csv.writer(ofile, delimiter=delimiter, quotechar=quotechar, quoting=quoting) + def unquote_colnames(colnames): + unq_colnames = [] + for col in colnames: + m = self.db._adapter.REGEX_TABLE_DOT_FIELD.match(col) + if not m: + unq_colnames.append(col) + else: + unq_colnames.append('.'.join(m.groups())) + return unq_colnames + colnames = kwargs.get('colnames', self.colnames) write_colnames = kwargs.get('write_colnames',True) # a proper csv starting with the column names if write_colnames: - writer.writerow(colnames) + writer.writerow(unquote_colnames(colnames)) def none_exception(value): """ @@ -10896,10 +11035,11 @@ class Rows(object): for record in self: row = [] for col in colnames: - if not REGEX_TABLE_DOT_FIELD.match(col): + m = self.db._adapter.REGEX_TABLE_DOT_FIELD.match(col) + if not m: row.append(record._extra[col]) else: - (t, f) = col.split('.') + (t, f) = m.groups() field = self.db[t][f] if isinstance(record.get(t, None), (Row,dict)): value = record[t][f] diff --git a/gluon/globals.py b/gluon/globals.py index 58b2c0de..08e09f64 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -816,7 +816,10 @@ class Session(Storage): # if on GAE tickets go also in DB if settings.global_settings.web2py_runtime_gae: request.tickets_db = db - table_migrate = (masterapp == request.application) + if masterapp == request.application: + table_migrate = migrate + else: + table_migrate = False tname = tablename + '_' + masterapp table = db.get(tname, None) Field = db.Field diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index c970ab40..2a417c78 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -21,7 +21,7 @@ from gluon.html import FORM, INPUT, LABEL, OPTION, SELECT, COL, COLGROUP from gluon.html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE from gluon.html import URL, truncate_string, FIELDSET from gluon.dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \ - bar_encode, Reference, REGEX_TABLE_DOT_FIELD, Expression, SQLCustomType + bar_encode, Reference, Expression, SQLCustomType from gluon.storage import Storage from gluon.utils import md5_hash from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \ @@ -45,6 +45,9 @@ except ImportError: table_field = re.compile('[\w_]+\.[\w_]+') widget_class = re.compile('^\w*') +def add_class(a,b): + return a+' '+b if a else b + def represent(field, value, record): f = field.represent if not callable(f): @@ -334,7 +337,7 @@ class RadioWidget(OptionsWidget): attr = cls._attributes(field, {}, **attributes) - attr['_class'] = attr.get('_class', 'web2py_radiowidget') + attr['_class'] = add_class(attr.get('_class'), 'web2py_radiowidget') requires = field.requires if not isinstance(requires, (list, tuple)): @@ -398,7 +401,9 @@ class CheckboxesWidget(OptionsWidget): values = [str(value)] attr = cls._attributes(field, {}, **attributes) - attr['_class'] = attr.get('_class', 'web2py_checkboxeswidget') + attr['_class'] = add_class(attr.get('_class'), 'web2py_checkboxeswidget') + + label = attr.get('label',True) requires = field.requires if not isinstance(requires, (list, tuple)): @@ -439,7 +444,8 @@ class CheckboxesWidget(OptionsWidget): requires=attr.get('requires', None), hideerror=True, _value=k, value=r_value), - LABEL(v, _for='%s%s' % (field.name, k)))) + LABEL(v, _for='%s%s' % (field.name, k)) + if label else '')) opts.append(child(tds)) if opts: @@ -2893,7 +2899,7 @@ class SQLTABLE(TABLE): if not sqlrows: return if not columns: - columns = sqlrows.colnames + columns = ['.'.join(sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD.match(c).groups()) for c in sqlrows.colnames] if headers == 'fieldname:capitalize': headers = {} for c in columns: @@ -3116,7 +3122,7 @@ class ExportClass(object): for record in self.rows: row = [] for col in self.rows.colnames: - if not REGEX_TABLE_DOT_FIELD.match(col): + if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col): row.append(record._extra[col]) else: (t, f) = col.split('.') diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 1b4c5dcd..0ca8b585 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -79,6 +79,9 @@ def tearDownModule(): class TestFields(unittest.TestCase): def testFieldName(self): + return + + # Any table name is supported as long as underlying db does. The following code is ignored. # Check that Fields cannot start with underscores self.assertRaises(SyntaxError, Field, '_abc', 'string') @@ -1394,6 +1397,137 @@ class TestRNameFields(unittest.TestCase): db.person.drop() +class TestQuoting(unittest.TestCase): + # tests for complex table names + def testRun(self): + return + db = DAL(DEFAULT_URI, check_reserved=['all']) + + t0 = db.define_table('A.table.with.dots and spaces', + Field('f', 'string')) + t1 = db.define_table('A.table', + Field('f_other', t0), + Field('words', 'text')) + + blather = 'blah blah and so' + t0[0] = {'f': 'content'} + t1[0] = {'f_other': int(t0[1]['id']), + 'words': blather} + + + r = db(t1['f_other']==t0.id).select() + self.assertEqual(r[0][db['A.table']].words, blather) + + db.define_table('t0', Field('f0')) + db.define_table('t1', Field('f1'), Field('t0', db['t0'])) + db.t0[0]=dict(f0=3) + db.t1[0]=dict(f1=3, t0=1) + + rows=db(db.t0.id==db.t1.t0).select() + self.assertEqual(rows[0].t1.t0, rows[0].t0.id) + if DEFAULT_URI.startswith('mssql'): + #there's no drop cascade in mssql + t1.drop() + t0.drop() + else: + t0.drop('cascade') + t1.drop() + + db.t1.drop() + db.t0.drop() + + # tests for case sensitivity + def testCase(self): + return + db = DAL(DEFAULT_URI, check_reserved=['all'], ignore_field_case=False) + if DEFAULT_URI.startswith('mssql'): + #multiple cascade gotcha + for key in ['reference','reference FK']: + db._adapter.types[key]=db._adapter.types[key].replace( + '%(on_delete_action)s','NO ACTION') + + # test table case + t0 = db.define_table('B', + Field('f', 'string')) + try: + t1 = db.define_table('b', + Field('B', t0), + Field('words', 'text')) + except Exception, e: + # An error is expected when database does not support case + # sensitive entity names. + if DEFAULT_URI.startswith('sqlite:'): + self.assertTrue(isinstance(e, db._adapter.driver.OperationalError)) + return + raise e + + blather = 'blah blah and so' + t0[0] = {'f': 'content'} + t1[0] = {'B': int(t0[1]['id']), + 'words': blather} + + r = db(db.B.id==db.b.B).select() + + self.assertEqual(r[0].b.words, blather) + + t1.drop() + t0.drop() + + # test field case + try: + t0 = db.define_table('table is a test', + Field('a_a'), + Field('a_A')) + except Exception, e: + # some db does not support case sensitive field names mysql is one of them. + if DEFAULT_URI.startswith('mysql:'): + db.rollback() + return + raise e + + t0[0] = dict(a_a = 'a_a', a_A='a_A') + + self.assertEqual(t0[1].a_a, 'a_a') + self.assertEqual(t0[1].a_A, 'a_A') + + t0.drop() + + def testPKFK(self): + + # test primary keys + + db = DAL(DEFAULT_URI, check_reserved=['all'], ignore_field_case=False) + if DEFAULT_URI.startswith('mssql'): + #multiple cascade gotcha + for key in ['reference','reference FK']: + db._adapter.types[key]=db._adapter.types[key].replace( + '%(on_delete_action)s','NO ACTION') + # test table without surrogate key. Length must is limited to + # 100 because of MySQL limitations: it cannot handle more than + # 767 bytes in unique keys. + + t0 = db.define_table('t0', Field('Code', length=100), primarykey=['Code']) + t2 = db.define_table('t2', Field('f'), Field('t0_Code', 'reference t0')) + t3 = db.define_table('t3', Field('f', length=100), Field('t0_Code', t0.Code), primarykey=['f']) + t4 = db.define_table('t4', Field('f', length=100), Field('t0', t0), primarykey=['f']) + + try: + t5 = db.define_table('t5', Field('f', length=100), Field('t0', 'reference no_table_wrong_reference'), primarykey=['f']) + except Exception, e: + self.assertTrue(isinstance(e, KeyError)) + + if DEFAULT_URI.startswith('mssql'): + #there's no drop cascade in mssql + t3.drop() + t4.drop() + t2.drop() + t0.drop() + else: + t0.drop('cascade') + t2.drop() + t3.drop() + t4.drop() + if __name__ == '__main__': unittest.main() - tearDownModule() \ No newline at end of file + tearDownModule() diff --git a/gluon/tools.py b/gluon/tools.py index 874ad220..b0d4d3a5 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -2915,7 +2915,7 @@ class Auth(object): formname='reset_password', dbio=False, onvalidation=onvalidation, hideerror=self.settings.hideerror): - user = table_user(**{userfield:form.vars.email}) + user = table_user(**{userfield:form.vars.get(userfield)}) if not user: session.flash = self.messages['invalid_%s' % userfield] redirect(self.url(args=request.args), @@ -5395,7 +5395,7 @@ class Wiki(object): if (auth.user and check_credentials(current.request, gae_login=False) and not 'wiki_editor' in auth.user_groups.values() and - self.settings.groups is None): + self.settings.groups == auth.user_groups.values()): group = db.auth_group(role='wiki_editor') gid = group.id if group else db.auth_group.insert( role='wiki_editor') @@ -5523,7 +5523,7 @@ class Wiki(object): url = URL(args=('_edit', slug)) return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn")) else: - html = page.html if not force_render else self.get_renderer(page) + html = page.html if not force_render else self.get_renderer()(page) content = XML(self.fix_hostname(html)) return dict(title=page.title, slug=page.slug, diff --git a/gluon/widget.py b/gluon/widget.py index 2a8ddc98..371d1147 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -1224,7 +1224,10 @@ end tell if not options.nobanner: print 'please visit:' print '\t', url - print 'use "kill -SIGTERM %i" to shutdown the web2py server' % os.getpid() + if sys.platform.startswith('win'): + print 'use "taskkill /f /pid %i" to shutdown the web2py server' % os.getpid() + else: + print 'use "kill -SIGTERM %i" to shutdown the web2py server' % os.getpid() # enhance linecache.getline (used by debugger) to look at the source file # if the line was not found (under py2exe & when file was modified)