Merge branch 'master' of git://github.com/web2py/web2py

This commit is contained in:
Fran Boon
2013-12-19 08:35:12 +00:00
30 changed files with 936 additions and 372 deletions
+1 -1
View File
@@ -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
+129 -56
View File
@@ -30,10 +30,18 @@ from gluon.languages import (read_possible_languages, read_dict, write_dict,
read_plural_dict, write_plural_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') session.flash = T('disabled in demo mode')
redirect(URL('site')) 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']: if not is_manager() and request.function in ['change_password', 'upgrade_web2py']:
session.flash = T('disabled in multi user mode') 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): 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: class tmp:
def write(self, data): def write(self, data):
pass pass
def close(self):
pass
return tmp() return tmp()
return open(a, b) 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}) return response.json({'file_hash': file_hash, 'saved_on': saved_on, 'functions': functions, 'controller': controller, 'application': request.args[0], 'highlight': highlight})
else: else:
file_details = dict(app=request.args[0], file_details = dict(app=request.args[0],
lineno=request.vars.lineno or 1,
editor_settings=preferences, editor_settings=preferences,
filename=filename, filename=filename,
realfilename=realfilename, realfilename=realfilename,
@@ -773,6 +784,35 @@ def edit():
else: else:
return response.json(file_details) 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(): def resolve():
""" """
@@ -1053,11 +1093,12 @@ def design():
#Get crontab #Get crontab
cronfolder = apath('%s/cron' % app, r=request) cronfolder = apath('%s/cron' % app, r=request)
if not os.path.exists(cronfolder):
os.mkdir(cronfolder)
crontab = apath('%s/cron/crontab' % app, r=request) crontab = apath('%s/cron/crontab' % app, r=request)
if not os.path.exists(crontab): if not is_gae:
safe_write(crontab, '#crontab') if not os.path.exists(cronfolder):
os.mkdir(cronfolder)
if not os.path.exists(crontab):
safe_write(crontab, '#crontab')
plugins = [] plugins = []
@@ -1209,7 +1250,6 @@ def plugin():
languages=languages, languages=languages,
crontab=crontab) crontab=crontab)
def create_file(): def create_file():
""" Create files handler """ """ Create files handler """
if request.vars and not request.vars.token == session.token: if request.vars and not request.vars.token == session.token:
@@ -1220,6 +1260,8 @@ def create_file():
app = get_app(request.vars.app) app = get_app(request.vars.app)
path = abspath(request.vars.location) path = abspath(request.vars.location)
else: else:
if request.vars.dir:
request.vars.location += request.vars.dir + '/'
app = get_app(name=request.vars.location.split('/')[0]) app = get_app(name=request.vars.location.split('/')[0])
path = apath(request.vars.location, r=request) path = apath(request.vars.location, r=request)
filename = re.sub('[^\w./-]+', '_', request.vars.filename) filename = re.sub('[^\w./-]+', '_', request.vars.filename)
@@ -1347,7 +1389,11 @@ def create_file():
safe_write(full_filename, text) safe_write(full_filename, text)
log_progress(app, 'CREATE', filename) 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):])) dict(filename=full_filename[len(path):]))
vars = {} vars = {}
if request.vars.id: if request.vars.id:
@@ -1356,13 +1402,51 @@ def create_file():
vars['app'] = request.vars.app vars['app'] = request.vars.app
redirect(URL('edit', redirect(URL('edit',
args=[os.path.join(request.vars.location, filename)], vars=vars)) args=[os.path.join(request.vars.location, filename)], vars=vars))
except Exception, e: except Exception, e:
if not isinstance(e, HTTP): if not isinstance(e, HTTP):
session.flash = T('cannot create file') 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(): def upload_file():
""" File uploading handler """ """ File uploading handler """
if request.vars and not request.vars.token == session.token: if request.vars and not request.vars.token == session.token:
@@ -1422,8 +1506,11 @@ def errors():
import hashlib import hashlib
app = get_app() app = get_app()
if is_gae:
method = request.args(1) or 'new' method = 'dbold' if ('old' in
(request.args(1) or '')) else 'dbnew'
else:
method = request.args(1) or 'new'
db_ready = {} db_ready = {}
db_ready['status'] = get_ticket_storage(app) db_ready['status'] = get_ticket_storage(app)
db_ready['errmessage'] = T( db_ready['errmessage'] = T(
@@ -1490,32 +1577,30 @@ def errors():
for fn in tk_db(tk_table.id > 0).select(): for fn in tk_db(tk_table.id > 0).select():
try: try:
error = pickle.loads(fn.ticket_data) 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(tk_table.id == fn.id).delete()
tk_db.commit() 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 = [(x['count'], x) for x in hash2error.values()]
decorated.sort(key=operator.itemgetter(0), reverse=True) decorated.sort(key=operator.itemgetter(0), reverse=True)
return dict(errors=[x[1] for x in decorated], app=app,
return dict(errors=[x[1] for x in decorated], app=app, method=method) method=method, db_ready=db_ready)
elif method == 'dbold': elif method == 'dbold':
tk_db, tk_table = get_ticket_storage(app) tk_db, tk_table = get_ticket_storage(app)
@@ -1523,16 +1608,18 @@ def errors():
if item[:7] == 'delete_': if item[:7] == 'delete_':
tk_db(tk_table.ticket_id == item[7:]).delete() tk_db(tk_table.ticket_id == item[7:]).delete()
tk_db.commit() 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_] tickets = [row.ticket_id for row in tickets_]
times = dict( times = dict([(row.ticket_id, row.created_datetime) for
[(row.ticket_id, row.created_datetime) for row in tickets_]) row in tickets_])
return dict(app=app, tickets=tickets, method=method,
return dict(app=app, tickets=tickets, method=method, times=times) times=times, db_ready=db_ready)
else: else:
for item in request.vars: 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 # Remove anything else as requested
if item[:7] == 'delete_' and (not item == "delete_all}"): if item[:7] == 'delete_' and (not item == "delete_all}"):
os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request)) 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): if os.path.exists(ticket_file):
db_string = open(ticket_file).read() db_string = open(ticket_file).read()
db_string = db_string.strip().replace('\r', '').replace('\n', '') 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: else:
return False return False
tickets_table = 'web2py_ticket' tickets_table = 'web2py_ticket'
@@ -1804,24 +1894,7 @@ def plugins():
"public/api.json/action/list/content/Package?package" + "public/api.json/action/list/content/Package?package" +
"_type=plugin&search_index=false").read() "_type=plugin&search_index=false").read()
session.plugins = loads_json(rawlist) session.plugins = loads_json(rawlist)
plugins = TABLE( return dict(plugins=session.plugins["results"], app=request.args(0))
*[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))
def install_plugin(): def install_plugin():
app = request.args(0) app = request.args(0)
+2 -1
View File
@@ -3,6 +3,7 @@ import cStringIO
import gluon.contrib.shell import gluon.contrib.shell
import code import code
import thread import thread
import cgi
from gluon.shell import env from gluon.shell import env
if DEMO_MODE or MULTI_USER_MODE: if DEMO_MODE or MULTI_USER_MODE:
@@ -40,7 +41,7 @@ def callback():
k = len(session['commands:' + app]) - 1 k = len(session['commands:' + app]) - 1
#output = PRE(output) #output = PRE(output)
#return TABLE(TR('In[%i]:'%k,PRE(command)),TR('Out[%i]:'%k,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(): def reset():
+16 -2
View File
@@ -1,4 +1,4 @@
# coding: utf8 # -*- coding: utf-8 -*-
{ {
'!langcode!': 'es', '!langcode!': 'es',
'!langname!': 'Español', '!langname!': 'Español',
@@ -9,6 +9,7 @@
'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S',
'(requires internet access, experimental)': '(requiere acceso a internet, experimental)', '(requires internet access, experimental)': '(requiere acceso a internet, experimental)',
'(something like "it-it")': '(algo como "it-it")', '(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\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\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files',
@@ -122,6 +123,7 @@
'delete plugin': 'eliminar plugin', 'delete plugin': 'eliminar plugin',
'Delete this file (you will be asked to confirm deletion)': 'Elimine este fichero (se le pedirá confirmación)', 'Delete this file (you will be asked to confirm deletion)': 'Elimine este fichero (se le pedirá confirmación)',
'Delete:': 'Elimine:', 'Delete:': 'Elimine:',
'Demo': 'Demo',
'Deploy': 'Deploy', 'Deploy': 'Deploy',
'Deploy on Google App Engine': 'Instale en Google App Engine', 'Deploy on Google App Engine': 'Instale en Google App Engine',
'Deploy to OpenShift': 'Instale en OpenShift', 'Deploy to OpenShift': 'Instale en OpenShift',
@@ -134,6 +136,7 @@
'direction: ltr': 'direction: ltr', 'direction: ltr': 'direction: ltr',
'Disable': 'Deshabilitar', 'Disable': 'Deshabilitar',
'docs': 'docs', 'docs': 'docs',
'Docs': 'Docs',
'Done!': 'Done!', 'Done!': 'Done!',
'done!': 'listo!', 'done!': 'listo!',
'Download': 'Descargar', 'Download': 'Descargar',
@@ -157,6 +160,7 @@
'Editing myemail': 'Editing myemail', 'Editing myemail': 'Editing myemail',
'Editing rbare': 'Editing rbare', 'Editing rbare': 'Editing rbare',
'Editing ul': 'Editing ul', 'Editing ul': 'Editing ul',
'Enable': 'Enable',
'Enterprise Web Framework': 'Armazón Empresarial para Internet', 'Enterprise Web Framework': 'Armazón Empresarial para Internet',
'Error': 'Error', 'Error': 'Error',
'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"', 'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"',
@@ -190,6 +194,8 @@
'First name': 'Nombre', 'First name': 'Nombre',
'Frames': 'Frames', 'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].', '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', 'Globals##debug': 'Globals',
'graph model': 'graph model', 'graph model': 'graph model',
'Group ID': 'ID de Grupo', 'Group ID': 'ID de Grupo',
@@ -213,6 +219,7 @@
'internal error': 'error interno', 'internal error': 'error interno',
'Internal State': 'Estado Interno', 'Internal State': 'Estado Interno',
'Invalid action': 'Acción inválida', 'Invalid action': 'Acción inválida',
'Invalid application name': 'Invalid application name',
'Invalid email': 'Correo inválido', 'Invalid email': 'Correo inválido',
'invalid password': 'contraseña inválida', 'invalid password': 'contraseña inválida',
'invalid password.': 'invalid password.', 'invalid password.': 'invalid password.',
@@ -230,6 +237,7 @@
'Last name': 'Apellido', 'Last name': 'Apellido',
'Last saved on:': 'Guardado en:', 'Last saved on:': 'Guardado en:',
'License for': 'Licencia para', 'License for': 'Licencia para',
'License:': 'License:',
'lists by ticket': 'lists by ticket', 'lists by ticket': 'lists by ticket',
'loading...': 'cargando...', 'loading...': 'cargando...',
'locals': 'locals', 'locals': 'locals',
@@ -250,6 +258,7 @@
'new application "%s" created': 'nueva aplicación "%s" creada', 'new application "%s" created': 'nueva aplicación "%s" creada',
'New application wizard': 'Asistente para nueva aplicación', 'New application wizard': 'Asistente para nueva aplicación',
'new plugin installed': 'nuevo plugin instalado', '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.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.dialog.w2p': 'New plugin installed: web2py.plugin.dialog.w2p',
'New plugin installed: web2py.plugin.math2py.w2p': 'New plugin installed: web2py.plugin.math2py.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', 'Removed Breakpoint on %s at line %s': 'Eliminado punto de ruptura en %s en la línea %s',
'Replace': 'Reemplazar', 'Replace': 'Reemplazar',
'Replace All': 'Reemplazar todos', 'Replace All': 'Reemplazar todos',
'Repository (%s)': 'Repository (%s)',
'Repository: %s': 'Repository: %s',
'request': 'request', 'request': 'request',
'Resolve Conflict file': 'archivo Resolución de Conflicto', 'Resolve Conflict file': 'archivo Resolución de Conflicto',
'response': 'response', 'response': 'response',
@@ -329,6 +340,8 @@
'Save file: %s': 'Guardar: %s', 'Save file: %s': 'Guardar: %s',
'Save via Ajax': 'Guardar via Ajax', 'Save via Ajax': 'Guardar via Ajax',
'Saved file hash:': 'Hash del archivo guardado:', 'Saved file hash:': 'Hash del archivo guardado:',
'Screenshot %s': 'Screenshot %s',
'Screenshots': 'Screenshots',
'selected': 'seleccionado(s)', 'selected': 'seleccionado(s)',
'session': 'session', 'session': 'session',
'session expired': 'sesión expirada', 'session expired': 'sesión expirada',
@@ -336,6 +349,7 @@
'shell': 'shell', 'shell': 'shell',
'Site': 'sitio', 'Site': 'sitio',
'some files could not be removed': 'algunos archivos no pudieron ser removidos', 'some files could not be removed': 'algunos archivos no pudieron ser removidos',
'source : filesystem': 'source : filesystem',
'Start searching': 'Iniciar búsqueda', 'Start searching': 'Iniciar búsqueda',
'Start wizard': 'Iniciar asistente', 'Start wizard': 'Iniciar asistente',
'state': 'estado', 'state': 'estado',
@@ -355,8 +369,8 @@
'test': 'probar', 'test': 'probar',
'Testing application': 'Probando aplicación', '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 "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 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 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', 'The presentations layer, views are also known as templates': 'La capa de presentación, las vistas también son llamadas plantillas',
+1 -1
View File
@@ -144,7 +144,7 @@ if session.is_mobile == 'true':
elif session.is_mobile == 'false': elif session.is_mobile == 'false':
is_mobile = False is_mobile = False
else: else:
is_mobile = request.user_agent().is_mobile is_mobile = request.user_agent().get('is_mobile',False)
if DEMO_MODE: if DEMO_MODE:
session.authorized = True session.authorized = True
+1 -1
View File
@@ -24,7 +24,7 @@ def button_enable(href, app):
return A(label, _class='button btn', _id=id, callback=href, target=id) return A(label, _class='button btn', _id=id, callback=href, target=id)
def sp_button(href, label): 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) ret = A_button(SPAN(label), _href=href)
else: else:
ret = A(SPAN(label), _class='button special btn btn-inverse', _href=href) ret = A(SPAN(label), _class='button special btn btn-inverse', _href=href)
@@ -1,7 +1,13 @@
/* TODO rename this file as web2py-editor.css */
/* Fullscreen */ /* Fullscreen */
.CodeMirror-fullscreen { .CodeMirror-fullscreen {
z-index: 1030; z-index: 1030;
} }
.CodeMirror {
border-top: 1px solid #ddd;
/*border-left: 1px solid #ddd;*/
border-bottom: 1px solid #ddd;
}
/* BREAKPOINTS */ /* BREAKPOINTS */
@@ -36,3 +42,15 @@
/*.nav-tabs>li { /*.nav-tabs>li {
min-width: 100px; 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;
}
+4 -4
View File
@@ -248,7 +248,7 @@ function keepalive(url) {
}); });
} }
function load_file(url) { function load_file(url, lineno) {
$.ajax({ $.ajax({
type: "GET", type: "GET",
contentType: 'application/json', contentType: 'application/json',
@@ -263,13 +263,13 @@ function load_file(url) {
var tab_header = '<li><a title="'+ json['filename'] +'" href="#' + json['id'] + '" data-toggle="tab"><button type="button" class="close">&times;</button>' + json['realfilename'] + '</a></li>'; var tab_header = '<li><a title="'+ json['filename'] +'" href="#' + json['id'] + '" data-toggle="tab"><button type="button" class="close">&times;</button>' + json['realfilename'] + '</a></li>';
var tab_body = '<div id="' + json['id'] + '" class="tab-pane fade in " >' + json['plain_html'] + '</div>'; var tab_body = '<div id="' + json['id'] + '" class="tab-pane fade in " >' + json['plain_html'] + '</div>';
if(json['force'] === false) { if(json['force'] === false) {
$('#filesTab').append($(tab_header)); $('#myTabContent').append($(tab_body)); // First load the body
$('#myTabContent').append($(tab_body)); $('#filesTab').append($(tab_header)); // Then load the header which trigger the shown event
} else { } else {
$('#' + json['id']).html($(tab_body)); $('#' + json['id']).html($(tab_body));
} }
} }
$("a[href='#" + json['id'] + "']").click(); $("a[href='#" + json['id'] + "']").trigger('click', lineno);
} }
}, },
error: function (x) { error: function (x) {
+24 -6
View File
@@ -485,8 +485,10 @@
el.addClass('disabled'); el.addClass('disabled');
var method = el.prop('type') == 'submit' ? 'val' : 'html'; 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..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state*/ /*store enabled state if not already disabled */
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 /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
* el.data('w2p_disable_with', el[method]()); * el.data('w2p_disable_with', el[method]());
@@ -633,7 +635,9 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) 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[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
}); });
@@ -647,7 +651,10 @@
form.find(web2py.enableSelector).each(function () { form.find(web2py.enableSelector).each(function () {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) 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); element.prop('disabled', false);
}); });
}, },
@@ -659,7 +666,18 @@
el.on('ajax:complete', 'form[data-w2p_target]', function (e) { el.on('ajax:complete', 'form[data-w2p_target]', function (e) {
web2py.enableFormElements($(this)); 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 */ /*end of functions */
@@ -679,7 +697,7 @@
/* compatibility code - start */ /* compatibility code - start */
ajax = jQuery.web2py.ajax; ajax = jQuery.web2py.ajax;
web2py_component = jQuery.web2py.component; web2py_component = jQuery.web2py.component;
web2py_websocket = jQuery.web2py.websocket; web2py_websocket = jQuery.web2py.web2py_websocket;
web2py_ajax_page = jQuery.web2py.ajax_page; web2py_ajax_page = jQuery.web2py.ajax_page;
/*needed for IS_STRONG(entropy)*/ /*needed for IS_STRONG(entropy)*/
web2py_validate_entropy = jQuery.web2py.validate_entropy; web2py_validate_entropy = jQuery.web2py.validate_entropy;
+88 -24
View File
@@ -1,4 +1,32 @@
{{extend 'layout.html'}} {{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('&nbsp;'),LABEL('/', _style='display:inline-block;'),XML('&nbsp;'),
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
}}
<!-- begin "edit" block --> <!-- begin "edit" block -->
{{ {{
def shortcut(combo, description): def shortcut(combo, description):
@@ -45,6 +73,7 @@
<script src="{{=cm}}/addon/fold/xml-fold.js"></script> <script src="{{=cm}}/addon/fold/xml-fold.js"></script>
<script src="{{=cm}}/addon/fold/comment-fold.js"></script> <script src="{{=cm}}/addon/fold/comment-fold.js"></script>
<script src="{{=cm}}/addon/fold/indent-fold.js"></script> <script src="{{=cm}}/addon/fold/indent-fold.js"></script>
<script src="{{=cm}}/addon/comment/comment.js"></script>
<link rel="stylesheet" href="{{=cm}}/addon/fold/foldgutter.css"> <link rel="stylesheet" href="{{=cm}}/addon/fold/foldgutter.css">
<script src="{{=cm}}/emmet.min.js"></script> <script src="{{=cm}}/emmet.min.js"></script>
<script src="{{=js_url}}/ajax_editor.js"></script> <script src="{{=js_url}}/ajax_editor.js"></script>
@@ -52,12 +81,17 @@
<link rel="stylesheet" href="{{=css_url}}/web2py-codemirror.css"> <link rel="stylesheet" href="{{=css_url}}/web2py-codemirror.css">
<script type="text/javascript"> <script type="text/javascript">
var current_font_incr = 0; // Default font-size, 0 means isn't set var current_font_incr = 0; // Default font-size, 0 means isn't set
$(document).on('shown click', 'a[data-toggle="tab"]', function (e) { $(document).on('shown click', 'a[data-toggle="tab"]', function (e, lineno) {
var tab_id = $(this).attr('href'); var tab_id = $(this).attr('href');
var editor = $(tab_id + " textarea").data('editor'); var editor = $(tab_id + " textarea").data('editor');
if (editor) { if (editor) {
editor.setSize('100%', $(tab_id).height()); editor_height = $(window).height() - $(tab_id + " .well-small").offset().top - $(tab_id + " .well-small").outerHeight(true) - $('.navbar-fixed-bottom').outerHeight() - 1;
editor.setSize('100%', editor_height);
editor.refresh(); editor.refresh();
if (lineno !== undefined) {
editor.setCursor(lineno);
editor.centerOnCursor();
}
} }
var n_li = $('#filesTab li').length; var n_li = $('#filesTab li').length;
$.each($('#filesTab li'), function(index, element) { $.each($('#filesTab li'), function(index, element) {
@@ -104,7 +138,8 @@ $(document).on('click', '#restore', function (e) {
$(document).on('click', 'a.editor_filelink, a#editor_settingslink', function (e) { $(document).on('click', 'a.editor_filelink, a#editor_settingslink', function (e) {
e.preventDefault(); e.preventDefault();
var url = $(this).attr("href"); var url = $(this).attr("href");
load_file(url); var lineno = $(this).data("lineno");
load_file(url, lineno);
}); });
/* This method updates all editors already instantiated with the selected preferences*/ /* This method updates all editors already instantiated with the selected preferences*/
@@ -164,7 +199,7 @@ $(document).on('click', 'a.font_button', function (e) {
}); });
{{if len(request.args) > 1:}} {{if len(request.args) > 1:}}
load_file('{{=URL(f='edit', args=request.args, vars=request.get_vars)}}'); load_file('{{=URL(f='edit', args=request.args, vars=request.get_vars)}}', {{=request.vars.lineno or 1}});
{{pass}} {{pass}}
</script> </script>
@@ -197,27 +232,22 @@ $(document).on('click', 'a.font_button', function (e) {
<div class="files-menu"> <div class="files-menu">
<ul class="nav nav-list" rel="pagebookmark" id="filelist"> <ul class="nav nav-list" rel="pagebookmark" id="filelist">
<li><input type="text" placeholder="{{=T('Rapid Search')}}" class="input-block-level typeahead-tw search-query"></li> <li><input type="text" placeholder="{{=T('Rapid Search')}}" class="input-block-level typeahead-tw search-query"></li>
{{dirs=[{'name':'models', 'reg':'.*\.py$'},
{'name':'controllers', 'reg':'.*\.py$'},
{'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'},
{'name':'modules', 'reg':'.*\.py$'}, ]}}
{{auto_complete_list=[]}} {{auto_complete_list=[]}}
{{for dir in dirs:}} <br />
<li class="nav-header component" onclick="collapse('{{="%s_files" % dir['name']}}');">{{=dir['name']}}</li> <div>
<li id="{{="%s_files" % dir['name']}}"> <button onclick="jQuery('#form').slideToggle()" class="btn btn-mini">{{=T('Create')}}</button>
<ul class="nav nav-list small-font"> <br />
{{for f in listfiles(app, dir['name'], regexp=dir['reg'] ):}} <div id="form">
{{id="%s__" % dir['name'] + f.replace('.','__')}} <div>
{{current_file = request.args(len(request.args) - 1)}} {{=file_create_form('%s/' % app, '')}}
<li class="{{#='active' if current_file==f else ''}}" style="overflow:hidden"> </div>
{{a_tag=editfile(dir['name'], f, dict(id=id))}} </div>
{{=a_tag}} <script>jQuery('#form').slideToggle()</script>
{{auto_complete_list.append({'value':f, 'tokens':f.split('/'), 'a_tag':a_tag})}} </div>
</li> <br />
{{pass}} <div id="files_menu">
</ul> {{=LOAD('default', 'files_menu', vars={'app':app}, ajax=True)}}
</li> </div>
{{pass}}
</ul> </ul>
</div> </div>
</div> </div>
@@ -227,13 +257,47 @@ $(document).on('click', 'a.font_button', function (e) {
<div id="myTabContent" class="tab-content"> <div id="myTabContent" class="tab-content">
</div> </div>
</div> </div>
<section id="windows_divs" class="tab-content ">
<div id="window_todo" class="tab-pane container-fluid">
{{=LOAD('default', 'todolist.load', vars={'app':app}, ajax=True, timeout=60000, times="infinity")}}
</div>
</section>
</div> </div>
{{block footer}}
<div id="" class="navbar navbar-inverse navbar-fixed-bottom">
<div class="navbar-inner">
<ul id="windows_hooks" class="nav">
<li class="">
<a href="#window_todo">TODO</a>
</li>
</ul>
</div>
</div>
<script>
$(document).on('click', '#window_todo li a', function (e) {
$(this).find('i').toggleClass('icon-chevron-right');
$(this).find('i').toggleClass('icon-chevron-down');
});
$('#windows_hooks li a').click(function (e) {
e.preventDefault();
if ( $(this).parent('li').hasClass('active') ) {
$(this).parent('li').removeClass('active');
$($(this).attr('href')).removeClass('active');
} else {
$(this).tab('show');
}
});
</script>
{{end}}
<script> <script>
$(document).ready(function() { $(document).ready(function() {
var filesMenu = $('#files'); var filesMenu = $('#files');
var ow = filesMenu.outerWidth(); var ow = filesMenu.outerWidth();
filesMenu.width(ow); filesMenu.width(ow);
$('#files').css('left', '-'+ow+'px'); $('#files').css('left', '-'+ow+'px');
$.web2py.trap_form('url', 'form');
$('#form form').addClass('no_trap'); // Let to reuse the same form
}); });
</script> </script>
<!-- Typeahead scripts here so the page load faster --> <!-- Typeahead scripts here so the page load faster -->
+51 -45
View File
@@ -74,15 +74,15 @@
request.env['wsgi_url_scheme'], request.env['http_host'], request.env['wsgi_url_scheme'], request.env['http_host'],
URL(c='debug', f='toggle_breakpoint')))}}, sel); URL(c='debug', f='toggle_breakpoint')))}}, sel);
}); });
function makeMarker() { function makeMarker() {
var marker = document.createElement("div"); var marker = document.createElement("div");
marker.style.color = "#822"; marker.style.color = "#822";
marker.innerHTML = "●"; marker.innerHTML = "●";
marker.className = "breakpoint"; marker.className = "breakpoint";
return marker; return marker;
} }
{{if filetype in ('html', 'javascript', 'css'):}} {{if filetype in ('html', 'javascript', 'css'):}}
// must be here or break emmet/zencoding // must be here or break emmet/zencoding
CodeMirror.defaults.extraKeys["Ctrl-S"] = CodeMirror.defaults.extraKeys["Ctrl-S"] =
function(instance) { function(instance) {
@@ -94,26 +94,27 @@
CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) { CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
} }
{{pass}} {{pass}}
{{if filetype=='python':}} {{if filetype=='python':}}
// must be here or break emmet/zencoding for python // must be here or break emmet/zencoding for python
CodeMirror.defaults.extraKeys["Ctrl-S"] = CodeMirror.defaults.extraKeys["Ctrl-S"] =
function(instance) { function(instance) {
doClickSave();}; doClickSave();};
CodeMirror.defaults.extraKeys["Ctrl-Space"] = "autocomplete"; CodeMirror.defaults.extraKeys["Ctrl-Space"] = "autocomplete";
CodeMirror.defaults.extraKeys["Tab"] = "indentMore"; CodeMirror.defaults.extraKeys["Tab"] = "indentMore";
CodeMirror.defaults.extraKeys["Shift-Tab"] = "indentLess"; CodeMirror.defaults.extraKeys["Shift-Tab"] = "indentLess";
CodeMirror.defaults.extraKeys["Ctrl-F11"] = function(cm) { CodeMirror.defaults.extraKeys["Ctrl-F11"] = function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen")); cm.setOption("fullScreen", !cm.getOption("fullScreen"));
}, },
CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) { CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
} }
//for autocomplete //for autocomplete
CodeMirror.commands.autocomplete = function(cm) { CodeMirror.commands.autocomplete = function(cm) {
CodeMirror.showHint(cm, CodeMirror.pythonHint); CodeMirror.showHint(cm, CodeMirror.pythonHint);
} }
{{pass}} {{pass}}
CodeMirror.defaults.extraKeys["Ctrl-/"] = "toggleComment";
store_changes_function = function(instance, changeObj) { store_changes_function = function(instance, changeObj) {
jQuery(instance).data('saved', false); jQuery(instance).data('saved', false);
instance.off("change", store_changes_function); instance.off("change", store_changes_function);
@@ -128,6 +129,19 @@
request.env['wsgi_url_scheme'], request.env['http_host'], request.env['wsgi_url_scheme'], request.env['http_host'],
URL(c='debug', f='list_breakpoints')))}}, editor); 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);
});
</script> </script>
<div class="editor-bar-bottom" style="margin-top:9px;"> <div class="editor-bar-bottom" style="margin-top:9px;">
@@ -141,34 +155,26 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="help alert alert-block alert-info"> <div class="help alert alert-block alert-info">
{{if filetype=='html':}} {{if filetype=='html':}}
<h3>{{=T('Key bindings for ZenCoding Plugin')}}</h3> <h3>{{=T('Key bindings for ZenCoding Plugin')}}</h3>
<ul class="keybindings unstyled"> {{else:}}
{{=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'))}}
</ul>
{{else:}}
<h3>{{=T("Key bindings")}}</h3> <h3>{{=T("Key bindings")}}</h3>
{{pass}}
<ul class="keybindings unstyled"> <ul class="keybindings unstyled">
{{=shortcut('Ctrl+S', T('Save via Ajax'))}} {{=shortcut('Ctrl+S', T('Save via Ajax'))}}
{{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}} {{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}}
{{=shortcut('Shift+Esc', T('Exit 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-F / Cmd-F', T('Start searching'))}}
{{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}} {{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}}
{{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}} {{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}}
{{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}} {{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}}
{{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}} {{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}}
</ul> {{=shortcut('Ctrl-/ ', T('Toggle comment'))}}
{{if filetype=='html':}}
{{=shortcut('Tab', T('Expand Abbreviation'))}}
{{elif filetype=='python':}}
{{=shortcut('Ctrl-Space', T('Autocomplete Python Code'))}}
{{pass}} {{pass}}
</ul>
</div> </div>
</div> </div>
@@ -0,0 +1,3 @@
{{for files in result_files:}}
{{=files}}
{{pass}}
+17 -1
View File
@@ -1,5 +1,21 @@
{{extend 'layout.html'}} {{extend 'layout.html'}}
{{=H3("Available plugins")}} {{=H3("Available plugins")}}
{{=P("Source: web2pyslices")}} {{=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)}}
@@ -0,0 +1,18 @@
<div>
<!--div class="page-header"-->
{{n_todo=reduce(lambda x,y: x + len(y['matches']), todo, 0)}}
<h4>{{="%s TODO in %s" % (n_todo, app)}}</h4>
<!--/div-->
<ul class="nav nav-list cm-s-web2py">
{{for pos_file, file in enumerate(todo):}}
<li onclick="collapse('{{=pos_file}}_matches');"><a ><i class='icon-chevron-right'></i>{{=file['filename']}} <span class='small'>({{=len(file['matches'])}} TODO)</span></a></li>
<li id="{{=pos_file}}_matches" style="display: none;"><ul class="nav nav-list">
{{for m in file['matches']:}}
<li><a href="{{=URL('edit', args=[app,file['dir'], file['filename']], vars={'lineno':m['lineno']}, extension="")}}" data-lineno="{{=m['lineno']-1}}" class="editor_filelink">
<span class='cm-comment'>{{=m['text']}}</span>
</a></li>
{{pass}}
</ul></li>
{{pass}}
</ul>
</div>
+2
View File
@@ -55,6 +55,7 @@
<!-- FOOTER <!-- FOOTER
============== --> ============== -->
{{block footer}}
<footer id="footer" class="fixed"> <footer id="footer" class="fixed">
<p><span>{{=T('Powered by')}} {{=A('web2py', _href='http://www.web2py.com')}}&trade; {{=T('created by')}} Massimo Di Pierro &copy;2007-{{=request.now.year}} <p><span>{{=T('Powered by')}} {{=A('web2py', _href='http://www.web2py.com')}}&trade; {{=T('created by')}} Massimo Di Pierro &copy;2007-{{=request.now.year}}
{{if hasattr(T,'get_possible_languages_info'):}} {{if hasattr(T,'get_possible_languages_info'):}}
@@ -68,6 +69,7 @@
</span>{{pass}} </span>{{pass}}
</p> </p>
</footer><!-- /#footer --> </footer><!-- /#footer -->
{{end}}
<!-- BS JAVASCRIPT <!-- BS JAVASCRIPT
====================== --> ====================== -->
+32 -31
View File
@@ -2,40 +2,41 @@
{{block sectionclass}}shell{{end}} {{block sectionclass}}shell{{end}}
<!-- begin "shell" block --> <!-- begin "shell" block -->
<div id="wrapper"> <div id="wrapper">
<div class="row-fluid"> <div class="row-fluid">
<div class="output-wrapper span8"> <div class="output-wrapper span8">
<textarea id="output" readonly="readonly">web2py Shell {{=request.env.web2py_version}}</textarea> <textarea id="output" readonly="readonly">web2py Shell {{=request.env.web2py_version}}
</div> </textarea>
</div> </div>
</div>
<div class="row-fluid"> <div class="row-fluid">
<form id="form" action="{{=URL(r=request,f='callback',args=app)}}" method="get" class="span8"> <form id="form" action="{{=URL(r=request,f='callback',args=app)}}" method="get" class="span8">
<div id="shellwrapper"> <div id="shellwrapper">
<div class="prompt-wrapper"> <div class="prompt-wrapper">
<div class="prompt-container"> <div class="prompt-container">
<textarea class="prompt" name="statement" id="statement"></textarea> <textarea class="prompt" name="statement" id="statement"></textarea>
</div> </div>
<a href="#" rel="tooltip" data-placement="right" data-original-title="{{=T('Type some Python code in here and hit Return (Enter) to execute it.')}}"> <a href="#" rel="tooltip" data-placement="right" data-original-title="{{=T('Type some Python code in here and hit Return (Enter) to execute it.')}}">
{{=helpicon()}} {{=helpicon()}}
<span>Type some Python code in here and hit Return (Enter) to execute it.</span> <span>Type some Python code in here and hit Return (Enter) to execute it.</span>
</a> </a>
</div>
<div id="caret"><span>&gt;&gt;&gt;</span></div>
<div id="autoscroll">autoscroll</div>
</div>
</form>
</div> </div>
<div id="caret"><span>&gt;&gt;&gt;</span></div>
<div id="autoscroll">autoscroll</div>
</div>
</form>
</div>
<div class="row-fluid clearfix"> <div class="row-fluid clearfix">
<div class="help alert alert-info span6"> <div class="help alert alert-info span6">
<ul> <ul>
<li>Using the shell may lock the database to other users of this app.</li> <li>Using the shell may lock the database to other users of this app.</li>
<li>Each db statement is automatically committed.</li> <li>Each db statement is automatically committed.</li>
<li>Creating new tables dynamically is not allowed.</li> <li>Creating new tables dynamically is not allowed.</li>
<li>Models are automatically imported in the shell.</li> <li>Models are automatically imported in the shell.</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" src="{{=URL('static', 'js/autoscroll.js')}}"></script> <script type="text/javascript" src="{{=URL('static', 'js/autoscroll.js')}}"></script>
+24 -6
View File
@@ -485,8 +485,10 @@
el.addClass('disabled'); el.addClass('disabled');
var method = el.prop('type') == 'submit' ? 'val' : 'html'; 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..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state*/ /*store enabled state if not already disabled */
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 /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
* el.data('w2p_disable_with', el[method]()); * el.data('w2p_disable_with', el[method]());
@@ -633,7 +635,9 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) 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[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
}); });
@@ -647,7 +651,10 @@
form.find(web2py.enableSelector).each(function () { form.find(web2py.enableSelector).each(function () {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) 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); element.prop('disabled', false);
}); });
}, },
@@ -659,7 +666,18 @@
el.on('ajax:complete', 'form[data-w2p_target]', function (e) { el.on('ajax:complete', 'form[data-w2p_target]', function (e) {
web2py.enableFormElements($(this)); 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 */ /*end of functions */
@@ -679,7 +697,7 @@
/* compatibility code - start */ /* compatibility code - start */
ajax = jQuery.web2py.ajax; ajax = jQuery.web2py.ajax;
web2py_component = jQuery.web2py.component; web2py_component = jQuery.web2py.component;
web2py_websocket = jQuery.web2py.websocket; web2py_websocket = jQuery.web2py.web2py_websocket;
web2py_ajax_page = jQuery.web2py.ajax_page; web2py_ajax_page = jQuery.web2py.ajax_page;
/*needed for IS_STRONG(entropy)*/ /*needed for IS_STRONG(entropy)*/
web2py_validate_entropy = jQuery.web2py.validate_entropy; web2py_validate_entropy = jQuery.web2py.validate_entropy;
@@ -43,7 +43,8 @@ random.shuffle(quotes)
<a href="http://www.infoworld.com/slideshow/24605/infoworlds-2012-technology-of-the-year-award-winners-183313#slide23"><img src="{{=URL('static','images/infoworld2012.jpeg')}}" width="200px"/></a><br/> <a href="http://www.infoworld.com/slideshow/24605/infoworlds-2012-technology-of-the-year-award-winners-183313#slide23"><img src="{{=URL('static','images/infoworld2012.jpeg')}}" width="200px"/></a><br/>
<a class="btn btn-danger" href="{{=URL('download')}}" style="margin-top:10px; width:180px; color:white">Download Now</a><br/> <a class="btn btn-danger" href="{{=URL('download')}}" style="margin-top:10px; width:180px; color:white">Download Now</a><br/>
<a class="btn btn-danger" href="https://www.pythonanywhere.com/try-web2py" style="margin-top:10px; width:180px; color:white">Try it now online</a><br/> <a class="btn btn-danger" href="https://www.pythonanywhere.com/try-web2py" style="margin-top:10px; width:180px; color:white">Try it now online</a><br/>
<a class="btn btn-danger" href="http://web2py.com/poweredby" style="margin-top:10px; width:180px; color:white">Sites Powered by web2py</a> <a class="btn btn-danger" href="http://web2py.com/poweredby" style="margin-top:10px; width:180px; color:white">Sites Powered by web2py</a><br/><br/>
<a class="coinbase-button" data-code="df71ec5c2d5bc3b1c18139ab645f352b" data-button-style="donation_large" href="https://coinbase.com/checkouts/df71ec5c2d5bc3b1c18139ab645f352b">Donate Bitcoins</a><script src="https://coinbase.com/assets/button.js" type="text/javascript"></script>
</div> </div>
</div> </div>
</div> </div>
+24 -6
View File
@@ -485,8 +485,10 @@
el.addClass('disabled'); el.addClass('disabled');
var method = el.prop('type') == 'submit' ? 'val' : 'html'; 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..."; var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state*/ /*store enabled state if not already disabled */
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 /*if you don't want to see "working..." on buttons, replace the following
* two lines with this one * two lines with this one
* el.data('w2p_disable_with', el[method]()); * el.data('w2p_disable_with', el[method]());
@@ -633,7 +635,9 @@
if(disable_with == undefined) { if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]()) 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[method](element.data('w2p_disable_with'));
element.prop('disabled', true); element.prop('disabled', true);
}); });
@@ -647,7 +651,10 @@
form.find(web2py.enableSelector).each(function () { form.find(web2py.enableSelector).each(function () {
var element = $(this), var element = $(this),
method = element.is('button') ? 'html' : 'val'; method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) 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); element.prop('disabled', false);
}); });
}, },
@@ -659,7 +666,18 @@
el.on('ajax:complete', 'form[data-w2p_target]', function (e) { el.on('ajax:complete', 'form[data-w2p_target]', function (e) {
web2py.enableFormElements($(this)); 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 */ /*end of functions */
@@ -679,7 +697,7 @@
/* compatibility code - start */ /* compatibility code - start */
ajax = jQuery.web2py.ajax; ajax = jQuery.web2py.ajax;
web2py_component = jQuery.web2py.component; web2py_component = jQuery.web2py.component;
web2py_websocket = jQuery.web2py.websocket; web2py_websocket = jQuery.web2py.web2py_websocket;
web2py_ajax_page = jQuery.web2py.ajax_page; web2py_ajax_page = jQuery.web2py.ajax_page;
/*needed for IS_STRONG(entropy)*/ /*needed for IS_STRONG(entropy)*/
web2py_validate_entropy = jQuery.web2py.validate_entropy; web2py_validate_entropy = jQuery.web2py.validate_entropy;
+2 -2
View File
@@ -65,8 +65,8 @@ skip_files: |
(.*\.py[co])| (.*\.py[co])|
(.*/RCS/.*)| (.*/RCS/.*)|
(\..*)| (\..*)|
(applications/(admin|examples)/.*)| (applications/examples/.*)|
((admin|examples|welcome)\.(w2p|tar))| ((examples|welcome)\.(w2p|tar))|
(applications/.*?/(cron|databases|errors|cache|sessions)/.*)| (applications/.*?/(cron|databases|errors|cache|sessions)/.*)|
((logs|scripts)/.*)| ((logs|scripts)/.*)|
(anyserver\.py)| (anyserver\.py)|
+2
View File
@@ -159,6 +159,8 @@ def extension(url):
def expand_one(url, cdict): def expand_one(url, cdict):
# try ombed but first check in cache # try ombed but first check in cache
if '@' in url and not '://'in url:
return '<a href="mailto:%s">%s</a>' % (url, url)
if cdict and url in cdict: if cdict and url in cdict:
r = cdict[url] r = cdict[url]
else: else:
+7 -2
View File
@@ -551,7 +551,7 @@ regex_list=re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$')
regex_bq_headline=re.compile('^(?:(\.+|\++|\-+)(\.)?\s+)?(-{3}-*)$') regex_bq_headline=re.compile('^(?:(\.+|\++|\-+)(\.)?\s+)?(-{3}-*)$')
regex_tq=re.compile('^(-{3}-*)(?::(?P<c>[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P<p>[a-zA-Z][_a-zA-Z\-\d]*)\])?)?$') regex_tq=re.compile('^(-{3}-*)(?::(?P<c>[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P<p>[a-zA-Z][_a-zA-Z\-\d]*)\])?)?$')
regex_proto = re.compile(r'(?<!["\w>/=])(?P<p>\w+):(?P<k>\w+://[\w\d\-+=?%&/:.]+)', re.M) regex_proto = re.compile(r'(?<!["\w>/=])(?P<p>\w+):(?P<k>\w+://[\w\d\-+=?%&/:.]+)', re.M)
regex_auto = re.compile(r'(?<!["\w>/=])(?P<k>\w+://[\w\d\-+_=?%&/:.,;#]+\w)',re.M) regex_auto = re.compile(r'(?<!["\w>/=])(?P<k>\w+://[\w\d\-+_=?%&/:.,;#]+\w|[\w\-.]+@[\w\-.]+)',re.M)
regex_link=re.compile(r'('+LINK+r')|\[\[(?P<s>.+?)\]\]',re.S) regex_link=re.compile(r'('+LINK+r')|\[\[(?P<s>.+?)\]\]',re.S)
regex_link_level2=re.compile(r'^(?P<t>\S.*?)?(?:\s+\[(?P<a>.+?)\])?(?:\s+(?P<k>\S+))?(?:\s+(?P<p>popup))?\s*$',re.S) regex_link_level2=re.compile(r'^(?P<t>\S.*?)?(?:\s+\[(?P<a>.+?)\])?(?:\s+(?P<k>\S+))?(?:\s+(?P<p>popup))?\s*$',re.S)
regex_media_level2=re.compile(r'^(?P<t>\S.*?)?(?:\s+\[(?P<a>.+?)\])?(?:\s+(?P<k>\S+))?\s+(?P<p>img|IMG|left|right|center|video|audio|blockleft|blockright)(?:\s+(?P<w>\d+px))?\s*$',re.S) regex_media_level2=re.compile(r'^(?P<t>\S.*?)?(?:\s+\[(?P<a>.+?)\])?(?:\s+(?P<k>\S+))?\s+(?P<p>img|IMG|left|right|center|video|audio|blockleft|blockright)(?:\s+(?P<w>\d+px))?\s*$',re.S)
@@ -648,7 +648,9 @@ def autolinks_simple(url):
image, video or audio tag image, video or audio tag
""" """
u_url=url.lower() u_url=url.lower()
if u_url.endswith(('.jpg','.jpeg','.gif','.png')): if '@' in url and not '://' in url:
return '<a href="mailto:%s">%s</a>' % (url, url)
elif u_url.endswith(('.jpg','.jpeg','.gif','.png')):
return '<img src="%s" controls />' % url return '<img src="%s" controls />' % url
elif u_url.endswith(('.mp4','.mpeg','.mov','.ogv')): elif u_url.endswith(('.mp4','.mpeg','.mov','.ogv')):
return '<video src="%s" controls></video>' % url return '<video src="%s" controls></video>' % url
@@ -673,6 +675,9 @@ def protolinks_simple(proto, url):
return '<img style="width:100px" src="http://chart.apis.google.com/chart?cht=qr&chs=100x100&chl=%s&choe=UTF-8&chld=H" alt="QR Code" title="QR Code" />'%url return '<img style="width:100px" src="http://chart.apis.google.com/chart?cht=qr&chs=100x100&chl=%s&choe=UTF-8&chld=H" alt="QR Code" title="QR Code" />'%url
return proto+':'+url return proto+':'+url
def email_simple(email):
return '<a href="mailto:%s">%s</a>' % (email, email)
def render(text, def render(text,
extra={}, extra={},
allowed={}, allowed={},
+1 -1
View File
@@ -258,7 +258,7 @@ def run(history, statement, env={}):
if not name.startswith('__'): if not name.startswith('__'):
try: try:
history.set_global(name, val) history.set_global(name, val)
except TypeError, ex: except (TypeError, cPickle.PicklingError), ex:
UNPICKLABLE_TYPES.append(type(val)) UNPICKLABLE_TYPES.append(type(val))
history.add_unpicklable(statement, new_globals.keys()) history.add_unpicklable(statement, new_globals.keys())
+2 -2
View File
@@ -858,8 +858,8 @@ class Sheet:
sorted_headers = [TH(), ] + \ sorted_headers = [TH(), ] + \
[TH(header[1]) for header in sorted(unsorted_headers)] [TH(header[1]) for header in sorted(unsorted_headers)]
table.insert(0, TR(*sorted_headers, table.insert(0, TR(*sorted_headers,
**{_class: "%s_fieldnames" % **{'_class': "%s_fieldnames" %
attributes["_class"]})) attributes["_class"]}))
else: else:
data = SCRIPT(""" // web2py Spreadsheets: no db data.""") data = SCRIPT(""" // web2py Spreadsheets: no db data.""")
+302 -162
View File
@@ -252,7 +252,8 @@ THREAD_LOCAL = threading.local()
REGEX_TYPE = re.compile('^([\w\_\:]+)') REGEX_TYPE = re.compile('^([\w\_\:]+)')
REGEX_DBNAME = re.compile('^(\w+)(\:\w+)*') REGEX_DBNAME = re.compile('^(\w+)(\:\w+)*')
REGEX_W = re.compile('^\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<table>[\w\-]+)\.(?P<field>[\w\-]+)\.(?P<uuidkey>[\w\-]+)(\.(?P<name>\w+))?\.\w+$') REGEX_UPLOAD_PATTERN = re.compile('(?P<table>[\w\-]+)\.(?P<field>[\w\-]+)\.(?P<uuidkey>[\w\-]+)(\.(?P<name>\w+))?\.\w+$')
REGEX_CLEANUP_FN = re.compile('[\'"\s;]+') REGEX_CLEANUP_FN = re.compile('[\'"\s;]+')
REGEX_UNPACK = re.compile('(?<!\|)\|(?!\|)') REGEX_UNPACK = re.compile('(?<!\|)\|(?!\|)')
@@ -670,12 +671,27 @@ class ConnectionPool(object):
break break
self.after_connection_hook() self.after_connection_hook()
###################################################################################
# metaclass to prepare adapter classes static values
###################################################################################
class AdapterMeta(type):
def __new__(cls, clsname, bases, dct):
classobj = super(AdapterMeta, cls).__new__(cls, clsname, bases, dct)
classobj.REGEX_TABLE_DOT_FIELD = re.compile(r'^' + \
classobj.QUOTE_TEMPLATE % REGEX_NO_GREEDY_ENTITY_NAME + \
r'\.' + \
classobj.QUOTE_TEMPLATE % REGEX_NO_GREEDY_ENTITY_NAME + \
r'$')
return classobj
################################################################################### ###################################################################################
# this is a generic adapter that does nothing; all others are derived from this one # this is a generic adapter that does nothing; all others are derived from this one
################################################################################### ###################################################################################
class BaseAdapter(ConnectionPool): class BaseAdapter(ConnectionPool):
__metaclass__ = AdapterMeta
native_json = False native_json = False
driver = None driver = None
driver_name = None driver_name = None
@@ -687,12 +703,14 @@ class BaseAdapter(ConnectionPool):
can_select_for_update = True can_select_for_update = True
dbpath = None dbpath = None
folder = None folder = None
connector = lambda *args, **kwargs: None # __init__ should override this
TRUE = 'T' TRUE = 'T'
FALSE = 'F' FALSE = 'F'
T_SEP = ' ' T_SEP = ' '
QUOTE_TEMPLATE = '"%s"' QUOTE_TEMPLATE = '"%s"'
types = { types = {
'boolean': 'CHAR(1)', 'boolean': 'CHAR(1)',
'string': 'CHAR(%(length)s)', 'string': 'CHAR(%(length)s)',
@@ -717,6 +735,7 @@ class BaseAdapter(ConnectionPool):
# the two below are only used when DAL(...bigint_id=True) and replace 'id','reference' # the two below are only used when DAL(...bigint_id=True) and replace 'id','reference'
'big-id': 'BIGINT PRIMARY KEY AUTOINCREMENT', 'big-id': 'BIGINT PRIMARY KEY AUTOINCREMENT',
'big-reference': 'BIGINT REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', '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',
} }
def isOperationalError(self,exception): def isOperationalError(self,exception):
@@ -834,8 +853,9 @@ class BaseAdapter(ConnectionPool):
self.connection = Dummy() self.connection = Dummy()
self.cursor = Dummy() self.cursor = Dummy()
def sequence_name(self,tablename): def sequence_name(self,tablename):
return '%s_sequence' % tablename return self.QUOTE_TEMPLATE % ('%s_sequence' % tablename)
def trigger_name(self,tablename): def trigger_name(self,tablename):
return '%s_sequence' % tablename return '%s_sequence' % tablename
@@ -868,61 +888,72 @@ class BaseAdapter(ConnectionPool):
if referenced == '.': if referenced == '.':
referenced = tablename referenced = tablename
constraint_name = self.constraint_name(tablename, field_name) constraint_name = self.constraint_name(tablename, field_name)
if not '.' in referenced \ # if not '.' in referenced \
and referenced != tablename \ # and referenced != tablename \
and hasattr(table,'_primarykey'): # and hasattr(table,'_primarykey'):
ftype = types['integer'] # ftype = types['integer']
else: #else:
if hasattr(table,'_primarykey'): try:
rtable = db[referenced]
rfield = rtable._id
rfieldname = rfield.name
rtablename = referenced
except (KeyError, ValueError, AttributeError), e:
LOGGER.debug('Error: %s' % e)
try:
rtablename,rfieldname = referenced.split('.') rtablename,rfieldname = referenced.split('.')
rtable = db[rtablename] rtable = db[rtablename]
rfield = rtable[rfieldname] rfield = rtable[rfieldname]
# must be PK reference or unique except Exception, e:
if rfieldname in rtable._primarykey or \ LOGGER.debug('Error: %s' %e)
rfield.unique: raise KeyError('Cannot resolve reference %s in %s definition' % (referenced, table._tablename))
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:
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)
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( # must be PK reference or unique
index_name = field_name+'__idx', if getattr(rtable, '_primarykey', None) and rfieldname in rtable._primarykey or \
field_name = field._rname or field.name, rfield.unique:
constraint_name = constraint_name, ftype = types[rfield.type[:9]] % \
foreign_key = '%s (%s)' % (real_referenced, dict(length=rfield.length)
id_fieldname), # multicolumn primary key reference?
on_delete_action=field.ondelete) 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:
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'): elif field_type.startswith('list:reference'):
ftype = types[field_type[:14]] ftype = types[field_type[:14]]
elif field_type.startswith('decimal'): elif field_type.startswith('decimal'):
@@ -995,39 +1026,38 @@ class BaseAdapter(ConnectionPool):
# geometry fields are added after the table has been created, not now # geometry fields are added after the table has been created, not now
if not (self.dbengine == 'postgres' and \ if not (self.dbengine == 'postgres' and \
field_type.startswith('geom')): field_type.startswith('geom')):
#fetch the rname if it's there fields.append('%s %s' % (field.sqlsafe_name, ftype))
field_rname = "%s" % (field._rname or field_name)
fields.append('%s %s' % (field_rname, ftype))
other = ';' other = ';'
# backend-specific extensions to fields # backend-specific extensions to fields
if self.dbengine == 'mysql': if self.dbengine == 'mysql':
if not hasattr(table, "_primarykey"): if not hasattr(table, "_primarykey"):
fields.append('PRIMARY KEY(%s)' % (table._id.name or table._id._rname)) fields.append('PRIMARY KEY (%s)' % (self.QUOTE_TEMPLATE % table._id.name))
other = ' ENGINE=InnoDB CHARACTER SET utf8;' engine = self.adapter_args.get('engine','InnoDB')
other = ' ENGINE=%s CHARACTER SET utf8;' % engine
fields = ',\n '.join(fields) fields = ',\n '.join(fields)
for rtablename in TFK: for rtablename in TFK:
rfields = TFK[rtablename] rfields = TFK[rtablename]
pkeys = db[rtablename]._primarykey pkeys = [self.QUOTE_TEMPLATE % pk for pk in db[rtablename]._primarykey]
fkeys = [ rfields[k] for k in pkeys ] fkeys = [self.QUOTE_TEMPLATE % rfields[k].name for k in pkeys ]
fields = fields + ',\n ' + \ fields = fields + ',\n ' + \
types['reference TFK'] % dict( types['reference TFK'] % dict(
table_name = tablename, table_name = table.sqlsafe,
field_name=', '.join(fkeys), field_name=', '.join(fkeys),
foreign_table = rtablename, foreign_table = table.sqlsafe,
foreign_key = ', '.join(pkeys), foreign_key = ', '.join(pkeys),
on_delete_action = field.ondelete) 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): if getattr(table,'_primarykey',None):
query = "CREATE TABLE %s(\n %s,\n %s) %s" % \ query = "CREATE TABLE %s(\n %s,\n %s) %s" % \
(table_rname, fields, (table.sqlsafe, fields,
self.PRIMARY_KEY(', '.join(table._primarykey)),other) self.PRIMARY_KEY(', '.join([self.QUOTE_TEMPLATE % pk for pk in table._primarykey])),other)
else: else:
query = "CREATE TABLE %s(\n %s\n)%s" % \ query = "CREATE TABLE %s(\n %s\n)%s" % \
(table_rname, fields, other) (table.sqlsafe, fields, other)
if self.uri.startswith('sqlite:///') \ if self.uri.startswith('sqlite:///') \
or self.uri.startswith('spatialite:///'): or self.uri.startswith('spatialite:///'):
@@ -1105,6 +1135,7 @@ class BaseAdapter(ConnectionPool):
k,v=item k,v=item
if not isinstance(v,dict): if not isinstance(v,dict):
v=dict(type='unknown',sql=v) v=dict(type='unknown',sql=v)
if self.ignore_field_case is not True: return k, v
return k.lower(),v return k.lower(),v
# make sure all field names are lower case to avoid # make sure all field names are lower case to avoid
# migrations because of case cahnge # migrations because of case cahnge
@@ -1132,7 +1163,7 @@ class BaseAdapter(ConnectionPool):
query = [ sql_fields[key]['sql'] ] query = [ sql_fields[key]['sql'] ]
else: else:
query = ['ALTER TABLE %s ADD %s %s;' % \ query = ['ALTER TABLE %s ADD %s %s;' % \
(tablename, key, (table.sqlsafe, key,
sql_fields_aux[key]['sql'].replace(', ', new_add))] sql_fields_aux[key]['sql'].replace(', ', new_add))]
metadata_change = True metadata_change = True
elif self.dbengine in ('sqlite', 'spatialite'): elif self.dbengine in ('sqlite', 'spatialite'):
@@ -1150,10 +1181,11 @@ class BaseAdapter(ConnectionPool):
"'%(table)s', '%(field)s');" % "'%(table)s', '%(field)s');" %
dict(schema=schema, table=tablename, field=key,) ] dict(schema=schema, table=tablename, field=key,) ]
elif self.dbengine in ('firebird',): 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: else:
query = ['ALTER TABLE %s DROP COLUMN %s;' % query = ['ALTER TABLE %s DROP COLUMN %s;' %
(tablename, key)] (self.QUOTE_TEMPLATE % tablename, self.QUOTE_TEMPLATE % key)]
metadata_change = True metadata_change = True
elif sql_fields[key]['sql'] != sql_fields_old[key]['sql'] \ elif sql_fields[key]['sql'] != sql_fields_old[key]['sql'] \
and not (key in table.fields and and not (key in table.fields and
@@ -1169,12 +1201,15 @@ class BaseAdapter(ConnectionPool):
else: else:
drop_expr = 'ALTER TABLE %s DROP COLUMN %s;' drop_expr = 'ALTER TABLE %s DROP COLUMN %s;'
key_tmp = key + '__tmp' key_tmp = key + '__tmp'
query = ['ALTER TABLE %s ADD %s %s;' % (t, key_tmp, tt), query = ['ALTER TABLE %s ADD %s %s;' % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp, tt),
'UPDATE %s SET %s=%s;' % (t, key_tmp, key), 'UPDATE %s SET %s=%s;' %
drop_expr % (t, key), (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp, self.QUOTE_TEMPLATE % key),
'ALTER TABLE %s ADD %s %s;' % (t, key, tt), drop_expr % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key),
'UPDATE %s SET %s=%s;' % (t, key, key_tmp), 'ALTER TABLE %s ADD %s %s;' %
drop_expr % (t, key_tmp)] (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 metadata_change = True
elif sql_fields[key]['type'] != sql_fields_old[key]['type']: elif sql_fields[key]['type'] != sql_fields_old[key]['type']:
sql_fields_current[key] = sql_fields[key] sql_fields_current[key] = sql_fields[key]
@@ -1269,8 +1304,7 @@ class BaseAdapter(ConnectionPool):
return 'PRIMARY KEY(%s)' % key return 'PRIMARY KEY(%s)' % key
def _drop(self, table, mode): def _drop(self, table, mode):
table_rname = table._rname or table return ['DROP TABLE %s;' % table.sqlsafe]
return ['DROP TABLE %s;' % table_rname]
def drop(self, table, mode=''): def drop(self, table, mode=''):
db = table._db db = table._db
@@ -1288,17 +1322,16 @@ class BaseAdapter(ConnectionPool):
self.log('success!\n', table) self.log('success!\n', table)
def _insert(self, table, fields): def _insert(self, table, fields):
table_rname = table._rname or table table_rname = table.sqlsafe
if fields: 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) values = ','.join(self.expand(v, f.type) for f, v in fields)
return 'INSERT INTO %s(%s) VALUES (%s);' % (table_rname, keys, values) return 'INSERT INTO %s(%s) VALUES (%s);' % (table_rname, keys, values)
else: else:
return self._insert_empty(table) return self._insert_empty(table)
def _insert_empty(self, table): def _insert_empty(self, table):
table_rname = table._rname or table return 'INSERT INTO %s DEFAULT VALUES;' % (table.sqlsafe)
return 'INSERT INTO %s DEFAULT VALUES;' % table_rname
def insert(self, table, fields): def insert(self, table, fields):
query = self._insert(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) return '(%s)' % ' || '.join(self.expand(x,'string') for x in items)
def ADD(self, first, second): 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), return '(%s + %s)' % (self.expand(first),
self.expand(second, first.type)) self.expand(second, first.type))
else: else:
@@ -1451,13 +1484,13 @@ class BaseAdapter(ConnectionPool):
self.expand(second, first.type)) self.expand(second, first.type))
def AS(self, first, second): 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): 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): if use_common_filters(second):
second = self.common_filter(second,[first._tablename]) 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): def INVERT(self, first):
return '%s DESC' % self.expand(first) return '%s DESC' % self.expand(first)
@@ -1472,13 +1505,10 @@ class BaseAdapter(ConnectionPool):
if isinstance(expression, Field): if isinstance(expression, Field):
et = expression.table et = expression.table
if not colnames: 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: else:
table_rname = et._tablename out = '%s.%s' % (self.QUOTE_TEMPLATE % et._tablename, self.QUOTE_TEMPLATE % expression.name)
if not colnames:
out = '%s.%s' % (table_rname, expression._rname or expression.name)
else:
out = '%s.%s' % (table_rname, expression.name)
if field_type == 'string' and not expression.type in ( if field_type == 'string' and not expression.type in (
'string','text','json','password'): 'string','text','json','password'):
out = self.CAST(out, self.types['text']) out = self.CAST(out, self.types['text'])
@@ -1509,10 +1539,11 @@ class BaseAdapter(ConnectionPool):
else: else:
return str(expression) return str(expression)
def table_alias(self,name): def table_alias(self, tbl):
if not isinstance(name, Table): if not isinstance(tbl, Table):
name = self.db[name]._rname or self.db[name] tbl = self.db[tbl]
return str(name) return tbl.sqlsafe_alias
def alias(self, table, alias): def alias(self, table, alias):
""" """
@@ -1520,7 +1551,7 @@ class BaseAdapter(ConnectionPool):
with alias name. with alias name.
""" """
other = copy.copy(table) 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['ALL'] = SQLALL(other)
other['_tablename'] = alias other['_tablename'] = alias
for fieldname in other.fields: for fieldname in other.fields:
@@ -1532,8 +1563,7 @@ class BaseAdapter(ConnectionPool):
return other return other
def _truncate(self, table, mode=''): def _truncate(self, table, mode=''):
tablename = table._rname or table._tablename return ['TRUNCATE TABLE %s %s;' % (table.sqlsafe, mode or '')]
return ['TRUNCATE TABLE %s %s;' % (tablename, mode or '')]
def truncate(self, table, mode= ' '): def truncate(self, table, mode= ' '):
# Prepare functions "write_to_logfile" and "close_logfile" # Prepare functions "write_to_logfile" and "close_logfile"
@@ -1553,10 +1583,10 @@ class BaseAdapter(ConnectionPool):
sql_w = ' WHERE ' + self.expand(query) sql_w = ' WHERE ' + self.expand(query)
else: else:
sql_w = '' 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)) \ self.expand(value, field.type)) \
for (field, value) in fields]) 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) return 'UPDATE %s SET %s%s;' % (tablename, sql_v, sql_w)
def update(self, tablename, query, fields): def update(self, tablename, query, fields):
@@ -1581,7 +1611,7 @@ class BaseAdapter(ConnectionPool):
sql_w = ' WHERE ' + self.expand(query) sql_w = ' WHERE ' + self.expand(query)
else: else:
sql_w = '' sql_w = ''
tablename = '%s' % (self.db[tablename]._rname or tablename) tablename = self.db[tablename].sqlsafe
return 'DELETE FROM %s%s;' % (tablename, sql_w) return 'DELETE FROM %s%s;' % (tablename, sql_w)
def delete(self, tablename, query): def delete(self, tablename, query):
@@ -1623,8 +1653,9 @@ class BaseAdapter(ConnectionPool):
if isinstance(item,SQLALL): if isinstance(item,SQLALL):
new_fields += item._table new_fields += item._table
elif isinstance(item,str): elif isinstance(item,str):
if REGEX_TABLE_DOT_FIELD.match(item): m = self.REGEX_TABLE_DOT_FIELD.match(item)
tablename,fieldname = item.split('.') if m:
tablename,fieldname = m.groups()
append(db[tablename][fieldname]) append(db[tablename][fieldname])
else: else:
append(Expression(db,lambda item=item:item)) append(Expression(db,lambda item=item:item))
@@ -1645,10 +1676,11 @@ class BaseAdapter(ConnectionPool):
tablenames = tables(query) tablenames = tables(query)
tablenames_for_common_filters = tablenames tablenames_for_common_filters = tablenames
for field in fields: for field in fields:
if isinstance(field, basestring) \ if isinstance(field, basestring):
and REGEX_TABLE_DOT_FIELD.match(field): m = self.REGEX_TABLE_DOT_FIELD.match(field)
tn,fn = field.split('.') if m:
field = self.db[tn][fn] tn,fn = m.groups()
field = self.db[tn][fn]
for tablename in tables(field): for tablename in tables(field):
if not tablename in tablenames: if not tablename in tablenames:
tablenames.append(tablename) tablenames.append(tablename)
@@ -1732,7 +1764,7 @@ class BaseAdapter(ConnectionPool):
tables_to_merge.keys()]) tables_to_merge.keys()])
if joint: if joint:
sql_t += ' %s %s' % (command, sql_t += ' %s %s' % (command,
','.join([self.table_alias(t) for t in joint])) ','.join([t for t in joint]))
for t in joinon: for t in joinon:
sql_t += ' %s %s' % (command, t) sql_t += ' %s %s' % (command, t)
elif inner_join and left: elif inner_join and left:
@@ -1747,7 +1779,7 @@ class BaseAdapter(ConnectionPool):
sql_t += ' %s %s' % (icommand, t) sql_t += ' %s %s' % (icommand, t)
if joint: if joint:
sql_t += ' %s %s' % (command, sql_t += ' %s %s' % (command,
','.join([self.table_alias(t) for t in joint])) ','.join([t for t in joint]))
for t in joinon: for t in joinon:
sql_t += ' %s %s' % (command, t) sql_t += ' %s %s' % (command, t)
else: else:
@@ -1767,9 +1799,9 @@ class BaseAdapter(ConnectionPool):
sql_o += ' ORDER BY %s' % self.expand(orderby) sql_o += ' ORDER BY %s' % self.expand(orderby)
if (limitby and not groupby and tablenames and orderby_on_limitby and not orderby): if (limitby and not groupby and tablenames and orderby_on_limitby and not orderby):
sql_o += ' ORDER BY %s' % ', '.join( 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 hasattr(self.db[t],'_primarykey') and self.db[t]._primarykey
or [self.db[t]._id._rname or self.db[t]._id.name] or ['_id']
) )
] ]
) )
@@ -1898,7 +1930,9 @@ class BaseAdapter(ConnectionPool):
def create_sequence_and_triggers(self, query, table, **args): def create_sequence_and_triggers(self, query, table, **args):
self.execute(query) self.execute(query)
def log_execute(self, *a, **b): def log_execute(self, *a, **b):
if not self.connection: raise ValueError(a[0])
if not self.connection: return None if not self.connection: return None
command = a[0] command = a[0]
if hasattr(self,'filter_sql_command'): if hasattr(self,'filter_sql_command'):
@@ -1955,8 +1989,17 @@ class BaseAdapter(ConnectionPool):
if field_is_type('decimal'): if field_is_type('decimal'):
return str(obj) return str(obj)
elif field_is_type('reference'): # reference elif field_is_type('reference'): # reference
if fieldtype.find('.')>0: # check for tablename first
return repr(obj) 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)): elif isinstance(obj, (Row, Reference)):
return str(obj['id']) return str(obj['id'])
return str(long(obj)) return str(long(obj))
@@ -2162,14 +2205,15 @@ class BaseAdapter(ConnectionPool):
new_rows = [] new_rows = []
tmps = [] tmps = []
for colname in colnames: 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) tmps.append(None)
else: else:
(tablename, _the_sep_, fieldname) = colname.partition('.') tablename, fieldname = col_m.groups()
table = db[tablename] table = db[tablename]
field = table[fieldname] field = table[fieldname]
ft = field.type ft = field.type
tmps.append((tablename,fieldname,table,field,ft)) tmps.append((tablename, fieldname, table, field, ft))
for (i,row) in enumerate(rows): for (i,row) in enumerate(rows):
new_row = Row() new_row = Row()
for (j,colname) in enumerate(colnames): for (j,colname) in enumerate(colnames):
@@ -2177,9 +2221,8 @@ class BaseAdapter(ConnectionPool):
tmp = tmps[j] tmp = tmps[j]
if tmp: if tmp:
(tablename,fieldname,table,field,ft) = tmp (tablename,fieldname,table,field,ft) = tmp
if tablename in new_row: colset = new_row.get(tablename, None)
colset = new_row[tablename] if colset is None:
else:
colset = new_row[tablename] = Row() colset = new_row[tablename] = Row()
if tablename not in virtualtables: if tablename not in virtualtables:
virtualtables.append(tablename) virtualtables.append(tablename)
@@ -2287,6 +2330,14 @@ class BaseAdapter(ConnectionPool):
return Expression(self.db,'CASE WHEN %s THEN %s ELSE %s END' % \ return Expression(self.db,'CASE WHEN %s THEN %s ELSE %s END' % \
(self.expand(query),represent(t),represent(f))) (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. # List of all the available adapters; they all extend BaseAdapter.
################################################################################### ###################################################################################
@@ -2564,6 +2615,7 @@ class MySQLAdapter(BaseAdapter):
'list:reference': 'LONGTEXT', 'list:reference': 'LONGTEXT',
'big-id': 'BIGINT AUTO_INCREMENT NOT NULL', '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', '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`" QUOTE_TEMPLATE = "`%s`"
@@ -2590,12 +2642,12 @@ class MySQLAdapter(BaseAdapter):
def _drop(self,table,mode): def _drop(self,table,mode):
# breaks db integrity but without this mysql does not drop table # 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, return ['SET FOREIGN_KEY_CHECKS=0;','DROP TABLE %s;' % table_rname,
'SET FOREIGN_KEY_CHECKS=1;'] 'SET FOREIGN_KEY_CHECKS=1;']
def _insert_empty(self, table): 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): def distributed_transaction_begin(self,key):
self.execute('XA START;') self.execute('XA START;')
@@ -2668,6 +2720,8 @@ class MySQLAdapter(BaseAdapter):
class PostgreSQLAdapter(BaseAdapter): class PostgreSQLAdapter(BaseAdapter):
drivers = ('psycopg2','pg8000') drivers = ('psycopg2','pg8000')
QUOTE_TEMPLATE = '"%s"'
support_distributed_transaction = True support_distributed_transaction = True
types = { types = {
'boolean': 'CHAR(1)', 'boolean': 'CHAR(1)',
@@ -2694,12 +2748,11 @@ class PostgreSQLAdapter(BaseAdapter):
'geography': 'GEOGRAPHY', 'geography': 'GEOGRAPHY',
'big-id': 'BIGSERIAL PRIMARY KEY', 'big-id': 'BIGSERIAL PRIMARY KEY',
'big-reference': 'BIGINT REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s', '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 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 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): def varquote(self,name):
return varquote_aux(name,'"%s"') return varquote_aux(name,'"%s"')
@@ -2713,7 +2766,7 @@ class PostgreSQLAdapter(BaseAdapter):
return "'%s'" % str(obj).replace("'","''") return "'%s'" % str(obj).replace("'","''")
def sequence_name(self,table): def sequence_name(self,table):
return '%s_id_seq' % table return self.QUOTE_TEMPLATE % (table + '_id_seq')
def RANDOM(self): def RANDOM(self):
return 'RANDOM()' return 'RANDOM()'
@@ -2802,8 +2855,8 @@ class PostgreSQLAdapter(BaseAdapter):
self.execute("SET standard_conforming_strings=on;") self.execute("SET standard_conforming_strings=on;")
self.try_json() self.try_json()
def lastrowid(self,table): def lastrowid(self,table = None):
self.execute("""select currval('"%s"')""" % table._sequence_name) self.execute("select lastval()")
return int(self.cursor.fetchone()[0]) return int(self.cursor.fetchone()[0])
def try_json(self): def try_json(self):
@@ -2925,6 +2978,14 @@ class PostgreSQLAdapter(BaseAdapter):
""" """
return 'ST_Within(%s,%s)' %(self.expand(first), self.expand(second, first.type)) 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): def represent(self, obj, fieldtype):
field_is_type = fieldtype.startswith field_is_type = fieldtype.startswith
if field_is_type('geo'): if field_is_type('geo'):
@@ -2942,6 +3003,11 @@ class PostgreSQLAdapter(BaseAdapter):
return value return value
return BaseAdapter.represent(self, obj, fieldtype) 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): class NewPostgreSQLAdapter(PostgreSQLAdapter):
drivers = ('psycopg2','pg8000') 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', '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): def trigger_name(self,tablename):
return '%s_trigger' % tablename return '%s_trigger' % tablename
@@ -3091,7 +3155,7 @@ class OracleAdapter(BaseAdapter):
def _drop(self,table,mode): def _drop(self,table,mode):
sequence_name = table._sequence_name 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): def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby):
if limitby: if limitby:
@@ -3218,11 +3282,18 @@ class OracleAdapter(BaseAdapter):
else: else:
return self.cursor.fetchall() 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): class MSSQLAdapter(BaseAdapter):
drivers = ('pyodbc',) drivers = ('pyodbc',)
T_SEP = 'T' T_SEP = 'T'
QUOTE_TEMPLATE = "[%s]" QUOTE_TEMPLATE = '"%s"'
types = { types = {
'boolean': 'BIT', 'boolean': 'BIT',
@@ -3685,7 +3756,7 @@ class FireBirdAdapter(BaseAdapter):
} }
def sequence_name(self,tablename): def sequence_name(self,tablename):
return 'genid_%s' % tablename return ('genid_' + self.QUOTE_TEMPLATE) % tablename
def trigger_name(self,tablename): def trigger_name(self,tablename):
return 'trg_id_%s' % tablename return 'trg_id_%s' % tablename
@@ -3714,7 +3785,7 @@ class FireBirdAdapter(BaseAdapter):
def _drop(self,table,mode): def _drop(self,table,mode):
sequence_name = table._sequence_name 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): def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby):
if limitby: if limitby:
@@ -4283,7 +4354,7 @@ class SAPDBAdapter(BaseAdapter):
} }
def sequence_name(self,table): 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): def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby):
if limitby: if limitby:
@@ -4548,6 +4619,7 @@ class GoogleSQLAdapter(UseDatabaseStoredFile,MySQLAdapter):
class NoSQLAdapter(BaseAdapter): class NoSQLAdapter(BaseAdapter):
can_select_for_update = False can_select_for_update = False
QUOTE_TEMPLATE = '%s'
@staticmethod @staticmethod
def to_unicode(obj): def to_unicode(obj):
@@ -4737,6 +4809,8 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
uploads_in_blob = True uploads_in_blob = True
types = {} types = {}
# reconnect is not required for Datastore dbs
reconnect = lambda *args, **kwargs: None
def file_exists(self, filename): pass def file_exists(self, filename): pass
def file_open(self, filename, mode='rb', lock=True): pass def file_open(self, filename, mode='rb', lock=True): pass
@@ -4839,12 +4913,10 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
elif field_type.startswith('reference'): elif field_type.startswith('reference'):
if field.notnull: if field.notnull:
attr = dict(required=True) attr = dict(required=True)
referenced = field_type[10:].strip() ftype = self.types[field_type[:9]](**attr)
ftype = self.types[field_type[:9]](referenced, **attr)
elif field_type.startswith('list:reference'): elif field_type.startswith('list:reference'):
if field.notnull: if field.notnull:
attr['required'] = True attr['required'] = True
referenced = field_type[15:].strip()
ftype = self.types[field_type[:14]](**attr) ftype = self.types[field_type[:14]](**attr)
elif field_type.startswith('list:'): elif field_type.startswith('list:'):
ftype = self.types[field_type](**attr) ftype = self.types[field_type](**attr)
@@ -5168,6 +5240,12 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
processor = attributes.get('processor',self.parse) processor = attributes.get('processor',self.parse)
return processor(rows,fields,colnames,False) 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): def count(self,query,distinct=None,limit=None):
if distinct: if distinct:
raise RuntimeError("COUNT DISTINCT not supported") 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_] validates that the given text is clean: only contains [0-9a-zA-Z_]
""" """
if not REGEX_ALPHANUMERIC.match(text): #if not REGEX_ALPHANUMERIC.match(text):
raise SyntaxError('invalid table or field name: %s' % text) # raise SyntaxError('invalid table or field name: %s' % text)
return text return text
class MongoDBAdapter(NoSQLAdapter): class MongoDBAdapter(NoSQLAdapter):
@@ -5582,9 +5660,6 @@ class MongoDBAdapter(NoSQLAdapter):
value = obj value = obj
else: else:
value = NoSQLAdapter.represent(self, obj, fieldtype) 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 # reference types must be convert to ObjectID
if fieldtype =='date': if fieldtype =='date':
if value == None: if value == None:
@@ -7245,12 +7320,32 @@ class Row(object):
__init__ = lambda self,*args,**kwargs: self.__dict__.update(*args,**kwargs) __init__ = lambda self,*args,**kwargs: self.__dict__.update(*args,**kwargs)
def __getitem__(self, k): 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) key=str(k)
_extra = self.__dict__.get('_extra', None) _extra = ogetattr(self, '__dict__').get('_extra', None)
if _extra is not None: if _extra is not None:
v = _extra.get(key, DEFAULT) v = _extra.get(key, DEFAULT)
if v != DEFAULT: if v != DEFAULT:
return v return v
try:
return ogetattr(self, key)
except (KeyError,AttributeError,TypeError):
pass
m = REGEX_TABLE_DOT_FIELD.match(key) m = REGEX_TABLE_DOT_FIELD.match(key)
if m: if m:
try: try:
@@ -7652,7 +7747,7 @@ class DAL(object):
adapter_args=None, attempts=5, auto_import=False, adapter_args=None, attempts=5, auto_import=False,
bigint_id=False, debug=False, lazy_tables=False, bigint_id=False, debug=False, lazy_tables=False,
db_uid=None, do_connect=True, 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. Creates a new Database Abstraction Layer instance.
@@ -7737,6 +7832,7 @@ class DAL(object):
self._decode_credentials = decode_credentials self._decode_credentials = decode_credentials
self._attempts = attempts self._attempts = attempts
self._do_connect = do_connect self._do_connect = do_connect
self._ignore_field_case = ignore_field_case
if not str(attempts).isdigit() or attempts < 0: if not str(attempts).isdigit() or attempts < 0:
attempts = 5 attempts = 5
@@ -7768,6 +7864,7 @@ class DAL(object):
# copy so multiple DAL() possible # copy so multiple DAL() possible
self._adapter.types = copy.copy(types) self._adapter.types = copy.copy(types)
self._adapter.build_parsemap() self._adapter.build_parsemap()
self._adapter.ignore_field_case = ignore_field_case
if bigint_id: if bigint_id:
if 'big-id' in types and 'reference' in types: if 'big-id' in types and 'reference' in types:
self._adapter.types['id'] = types['big-id'] self._adapter.types['id'] = types['big-id']
@@ -7810,11 +7907,11 @@ class DAL(object):
def import_table_definitions(self, path, migrate=False, def import_table_definitions(self, path, migrate=False,
fake_migrate=False, tables=None): fake_migrate=False, tables=None):
pattern = pjoin(path,self._uri_hash+'_*.table')
if tables: if tables:
for table in tables: for table in tables:
self.define_table(**table) self.define_table(**table)
else: else:
pattern = pjoin(path,self._uri_hash+'_*.table')
for filename in glob.glob(pattern): for filename in glob.glob(pattern):
tfile = self._adapter.file_open(filename, 'r') tfile = self._adapter.file_open(filename, 'r')
try: try:
@@ -8565,13 +8662,8 @@ class Table(object):
self._tablename = tablename self._tablename = tablename
self._ot = None # args.get('rname') self._ot = None # args.get('rname')
self._rname = args.get('rname') self._rname = args.get('rname')
if not self._rname: self._sequence_name = args.get('sequence_name') or \
self._sequence_name = args.get('sequence_name') or \ db and db._adapter.sequence_name(self._rname or tablename)
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._trigger_name = args.get('trigger_name') or \ self._trigger_name = args.get('trigger_name') or \
db and db._adapter.trigger_name(tablename) db and db._adapter.trigger_name(tablename)
self._common_filter = args.get('common_filter') self._common_filter = args.get('common_filter')
@@ -8652,7 +8744,7 @@ class Table(object):
fields.append(Field(fn,'blob',default='', fields.append(Field(fn,'blob',default='',
writable=False,readable=False)) writable=False,readable=False))
lower_fieldnames = set() fieldnames_set = set()
reserved = dir(Table) + ['fields'] reserved = dir(Table) + ['fields']
if (db and db.check_reserved): if (db and db.check_reserved):
check_reserved = db.check_reserved_keyword check_reserved = db.check_reserved_keyword
@@ -8663,12 +8755,15 @@ class Table(object):
for field in fields: for field in fields:
field_name = field.name field_name = field.name
check_reserved(field_name) check_reserved(field_name)
fn_lower = field_name.lower() if db and db._ignore_field_case:
if fn_lower in lower_fieldnames: fname_item = field_name.lower()
else:
fname_item = field_name
if fname_item in fieldnames_set:
raise SyntaxError("duplicate field %s in table %s" \ raise SyntaxError("duplicate field %s in table %s" \
% (field_name, tablename)) % (field_name, tablename))
else: else:
lower_fieldnames.add(fn_lower) fieldnames_set.add(fname_item)
self.fields.append(field_name) self.fields.append(field_name)
self[field_name] = field self[field_name] = field
@@ -8910,8 +9005,23 @@ class Table(object):
if 'Oracle' in str(type(self._db._adapter)): if 'Oracle' in str(type(self._db._adapter)):
return '%s %s' % (ot, self._tablename) return '%s %s' % (ot, self._tablename)
return '%s AS %s' % (ot, self._tablename) return '%s AS %s' % (ot, self._tablename)
return 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 = ''): def _drop(self, mode = ''):
return self._db._adapter._drop(self, mode) return self._db._adapter._drop(self, mode)
@@ -9566,6 +9676,10 @@ class Expression(object):
db = self.db db = self.db
return Query(db, db._adapter.ST_WITHIN, self, value) 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 # for use in both Query and sortby
@@ -9738,7 +9852,12 @@ class Field(Expression):
if not isinstance(fieldname, str) or hasattr(Table, fieldname) or \ if not isinstance(fieldname, str) or hasattr(Table, fieldname) or \
fieldname[0] == '_' or REGEX_PYTHON_KEYWORDS.match(fieldname): fieldname[0] == '_' or REGEX_PYTHON_KEYWORDS.match(fieldname):
raise SyntaxError('Field: invalid field name: %s' % 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.length = length if not length is None else DEFAULTLENGTH.get(self.type,512)
self.default = default if default!=DEFAULT else (update or None) self.default = default if default!=DEFAULT else (update or None)
self.required = required # is this field required self.required = required # is this field required
@@ -10004,6 +10123,16 @@ class Field(Expression):
except: except:
return '<no table>.%s' % self.name return '<no table>.%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 '<no table>.%s' % self.name
@property
def sqlsafe_name(self):
return self._rname or self._db._adapter.sqlsafe_field(self.name)
class Query(object): class Query(object):
@@ -10356,7 +10485,7 @@ class Set(object):
fields = table._listify(update_fields,update=True) fields = table._listify(update_fields,update=True)
if not fields: if not fields:
raise SyntaxError("No fields to update") 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] ret and [f(self,update_fields) for f in table._after_update]
return ret return ret
@@ -10869,11 +10998,21 @@ class Rows(object):
represent = kwargs.get('represent', False) represent = kwargs.get('represent', False)
writer = csv.writer(ofile, delimiter=delimiter, writer = csv.writer(ofile, delimiter=delimiter,
quotechar=quotechar, quoting=quoting) 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) colnames = kwargs.get('colnames', self.colnames)
write_colnames = kwargs.get('write_colnames',True) write_colnames = kwargs.get('write_colnames',True)
# a proper csv starting with the column names # a proper csv starting with the column names
if write_colnames: if write_colnames:
writer.writerow(colnames) writer.writerow(unquote_colnames(colnames))
def none_exception(value): def none_exception(value):
""" """
@@ -10896,10 +11035,11 @@ class Rows(object):
for record in self: for record in self:
row = [] row = []
for col in colnames: 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]) row.append(record._extra[col])
else: else:
(t, f) = col.split('.') (t, f) = m.groups()
field = self.db[t][f] field = self.db[t][f]
if isinstance(record.get(t, None), (Row,dict)): if isinstance(record.get(t, None), (Row,dict)):
value = record[t][f] value = record[t][f]
+4 -1
View File
@@ -816,7 +816,10 @@ class Session(Storage):
# if on GAE tickets go also in DB # if on GAE tickets go also in DB
if settings.global_settings.web2py_runtime_gae: if settings.global_settings.web2py_runtime_gae:
request.tickets_db = db request.tickets_db = db
table_migrate = (masterapp == request.application) if masterapp == request.application:
table_migrate = migrate
else:
table_migrate = False
tname = tablename + '_' + masterapp tname = tablename + '_' + masterapp
table = db.get(tname, None) table = db.get(tname, None)
Field = db.Field Field = db.Field
+12 -6
View File
@@ -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 TABLE, THEAD, TBODY, TR, TD, TH, STYLE
from gluon.html import URL, truncate_string, FIELDSET from gluon.html import URL, truncate_string, FIELDSET
from gluon.dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \ 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.storage import Storage
from gluon.utils import md5_hash from gluon.utils import md5_hash
from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \ 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_]+') table_field = re.compile('[\w_]+\.[\w_]+')
widget_class = re.compile('^\w*') widget_class = re.compile('^\w*')
def add_class(a,b):
return a+' '+b if a else b
def represent(field, value, record): def represent(field, value, record):
f = field.represent f = field.represent
if not callable(f): if not callable(f):
@@ -334,7 +337,7 @@ class RadioWidget(OptionsWidget):
attr = cls._attributes(field, {}, **attributes) attr = cls._attributes(field, {}, **attributes)
attr['_class'] = attr.get('_class', 'web2py_radiowidget') attr['_class'] = add_class(attr.get('_class'), 'web2py_radiowidget')
requires = field.requires requires = field.requires
if not isinstance(requires, (list, tuple)): if not isinstance(requires, (list, tuple)):
@@ -398,7 +401,9 @@ class CheckboxesWidget(OptionsWidget):
values = [str(value)] values = [str(value)]
attr = cls._attributes(field, {}, **attributes) 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 requires = field.requires
if not isinstance(requires, (list, tuple)): if not isinstance(requires, (list, tuple)):
@@ -439,7 +444,8 @@ class CheckboxesWidget(OptionsWidget):
requires=attr.get('requires', None), requires=attr.get('requires', None),
hideerror=True, _value=k, hideerror=True, _value=k,
value=r_value), 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)) opts.append(child(tds))
if opts: if opts:
@@ -2893,7 +2899,7 @@ class SQLTABLE(TABLE):
if not sqlrows: if not sqlrows:
return return
if not columns: 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': if headers == 'fieldname:capitalize':
headers = {} headers = {}
for c in columns: for c in columns:
@@ -3116,7 +3122,7 @@ class ExportClass(object):
for record in self.rows: for record in self.rows:
row = [] row = []
for col in self.rows.colnames: 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]) row.append(record._extra[col])
else: else:
(t, f) = col.split('.') (t, f) = col.split('.')
+134
View File
@@ -79,6 +79,9 @@ def tearDownModule():
class TestFields(unittest.TestCase): class TestFields(unittest.TestCase):
def testFieldName(self): 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 # Check that Fields cannot start with underscores
self.assertRaises(SyntaxError, Field, '_abc', 'string') self.assertRaises(SyntaxError, Field, '_abc', 'string')
@@ -1394,6 +1397,137 @@ class TestRNameFields(unittest.TestCase):
db.person.drop() 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__': if __name__ == '__main__':
unittest.main() unittest.main()
tearDownModule() tearDownModule()
+3 -3
View File
@@ -2915,7 +2915,7 @@ class Auth(object):
formname='reset_password', dbio=False, formname='reset_password', dbio=False,
onvalidation=onvalidation, onvalidation=onvalidation,
hideerror=self.settings.hideerror): hideerror=self.settings.hideerror):
user = table_user(**{userfield:form.vars.email}) user = table_user(**{userfield:form.vars.get(userfield)})
if not user: if not user:
session.flash = self.messages['invalid_%s' % userfield] session.flash = self.messages['invalid_%s' % userfield]
redirect(self.url(args=request.args), redirect(self.url(args=request.args),
@@ -5395,7 +5395,7 @@ class Wiki(object):
if (auth.user and if (auth.user and
check_credentials(current.request, gae_login=False) and check_credentials(current.request, gae_login=False) and
not 'wiki_editor' in auth.user_groups.values() 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') group = db.auth_group(role='wiki_editor')
gid = group.id if group else db.auth_group.insert( gid = group.id if group else db.auth_group.insert(
role='wiki_editor') role='wiki_editor')
@@ -5523,7 +5523,7 @@ class Wiki(object):
url = URL(args=('_edit', slug)) url = URL(args=('_edit', slug))
return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn")) return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn"))
else: 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)) content = XML(self.fix_hostname(html))
return dict(title=page.title, return dict(title=page.title,
slug=page.slug, slug=page.slug,
+4 -1
View File
@@ -1224,7 +1224,10 @@ end tell
if not options.nobanner: if not options.nobanner:
print 'please visit:' print 'please visit:'
print '\t', url 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 # enhance linecache.getline (used by debugger) to look at the source file
# if the line was not found (under py2exe & when file was modified) # if the line was not found (under py2exe & when file was modified)