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)
if DEMO_MODE and request.function in ['change_password', 'pack', 'pack_custom','pack_plugin', 'upgrade_web2py', 'uninstall', 'cleanup', 'compile_app', 'remove_compiled_app', 'delete', 'delete_plugin', 'create_file', 'upload_file', 'update_languages', 'reload_routes', 'git_push', 'git_pull']:
if DEMO_MODE and request.function in ['change_password', 'pack',
'pack_custom','pack_plugin', 'upgrade_web2py', 'uninstall',
'cleanup', 'compile_app', 'remove_compiled_app', 'delete',
'delete_plugin', 'create_file', 'upload_file', 'update_languages',
'reload_routes', 'git_push', 'git_pull', 'install_plugin']:
session.flash = T('disabled in demo mode')
redirect(URL('site'))
if is_gae and request.function in ('edit', 'edit_language',
'edit_plurals', 'update_languages', 'create_file', 'install_plugin'):
session.flash = T('disabled in GAE mode')
redirect(URL('site'))
if not is_manager() and request.function in ['change_password', 'upgrade_web2py']:
session.flash = T('disabled in multi user mode')
@@ -63,10 +71,12 @@ def log_progress(app, mode='EDIT', filename=None, progress=0):
def safe_open(a, b):
if DEMO_MODE and ('w' in b or 'a' in b):
if (DEMO_MODE or is_gae) and ('w' in b or 'a' in b):
class tmp:
def write(self, data):
pass
def close(self):
pass
return tmp()
return open(a, b)
@@ -750,6 +760,7 @@ def edit():
return response.json({'file_hash': file_hash, 'saved_on': saved_on, 'functions': functions, 'controller': controller, 'application': request.args[0], 'highlight': highlight})
else:
file_details = dict(app=request.args[0],
lineno=request.vars.lineno or 1,
editor_settings=preferences,
filename=filename,
realfilename=realfilename,
@@ -773,6 +784,35 @@ def edit():
else:
return response.json(file_details)
def todolist():
""" Returns all TODO of the requested app
"""
app = request.vars.app or ''
app_path = apath('%(app)s' % {'app':app}, r=request)
dirs=['models', 'controllers', 'modules', 'private' ]
def listfiles(app, dir, regexp='.*\.py$'):
files = sorted( listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp))
files = [x.replace(os.path.sep, '/') for x in files if not x.endswith('.bak')]
return files
pattern = '#\s*(todo)+\s+(.*)'
regex = re.compile(pattern, re.IGNORECASE)
output = []
for d in dirs:
for f in listfiles(app, d):
matches = []
filename= apath(os.path.join(app, d, f), r=request)
with open(filename, 'r') as f_s:
src = f_s.read()
for m in regex.finditer(src):
start = m.start()
lineno = src.count('\n', 0, start) + 1
matches.append({'text':m.group(0), 'lineno':lineno})
if len(matches) != 0:
output.append({'filename':f,'matches':matches, 'dir':d})
return {'todo':output, 'app': app}
def resolve():
"""
@@ -1053,11 +1093,12 @@ def design():
#Get crontab
cronfolder = apath('%s/cron' % app, r=request)
if not os.path.exists(cronfolder):
os.mkdir(cronfolder)
crontab = apath('%s/cron/crontab' % app, r=request)
if not os.path.exists(crontab):
safe_write(crontab, '#crontab')
if not is_gae:
if not os.path.exists(cronfolder):
os.mkdir(cronfolder)
if not os.path.exists(crontab):
safe_write(crontab, '#crontab')
plugins = []
@@ -1209,7 +1250,6 @@ def plugin():
languages=languages,
crontab=crontab)
def create_file():
""" Create files handler """
if request.vars and not request.vars.token == session.token:
@@ -1220,6 +1260,8 @@ def create_file():
app = get_app(request.vars.app)
path = abspath(request.vars.location)
else:
if request.vars.dir:
request.vars.location += request.vars.dir + '/'
app = get_app(name=request.vars.location.split('/')[0])
path = apath(request.vars.location, r=request)
filename = re.sub('[^\w./-]+', '_', request.vars.filename)
@@ -1347,7 +1389,11 @@ def create_file():
safe_write(full_filename, text)
log_progress(app, 'CREATE', filename)
session.flash = T('file "%(filename)s" created',
if request.vars.dir:
result = T('file "%(filename)s" created',
dict(filename=full_filename[len(path):]))
else:
session.flash = T('file "%(filename)s" created',
dict(filename=full_filename[len(path):]))
vars = {}
if request.vars.id:
@@ -1356,13 +1402,51 @@ def create_file():
vars['app'] = request.vars.app
redirect(URL('edit',
args=[os.path.join(request.vars.location, filename)], vars=vars))
except Exception, e:
if not isinstance(e, HTTP):
session.flash = T('cannot create file')
redirect(request.vars.sender + anchor)
if request.vars.dir:
response.flash = result
response.headers['web2py-component-content'] = 'append'
response.headers['web2py-component-command'] = """
$.web2py.invalidate('#files_menu');
load_file('%s');
$.web2py.enableElement($('#form form').find($.web2py.formInputClickSelector));
""" % URL('edit', args=[app,request.vars.dir,filename])
return ''
else:
redirect(request.vars.sender + anchor)
def listfiles(app, dir, regexp='.*\.py$'):
files = sorted(
listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp))
files = [x.replace('\\', '/') for x in files if not x.endswith('.bak')]
return files
def editfile(path,file,vars={}, app = None):
args=(path,file) if 'app' in vars else (app,path,file)
url = URL('edit', args=args, vars=vars)
return A(file, _class='editor_filelink', _href=url, _style='word-wrap: nowrap;')
def files_menu():
app = request.vars.app or 'welcome'
dirs=[{'name':'models', 'reg':'.*\.py$'},
{'name':'controllers', 'reg':'.*\.py$'},
{'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'},
{'name':'modules', 'reg':'.*\.py$'},
{'name':'static', 'reg': '[^\.#].*'}]
result_files = []
for dir in dirs:
result_files.append(TAG[''](LI(dir['name'], _class="nav-header component", _onclick="collapse('" + dir['name'] + "_files');"),
LI(UL(*[LI(editfile(dir['name'], f, dict(id=dir['name'] + f.replace('.','__')), app), _style="overflow:hidden", _id=dir['name']+"__"+f.replace('.','__'))
for f in listfiles(app, dir['name'], regexp=dir['reg'])],
_class="nav nav-list small-font"),
_id=dir['name'] + '_files', _style="display: none;")))
return dict(result_files = result_files)
def upload_file():
""" File uploading handler """
if request.vars and not request.vars.token == session.token:
@@ -1422,8 +1506,11 @@ def errors():
import hashlib
app = get_app()
method = request.args(1) or 'new'
if is_gae:
method = 'dbold' if ('old' in
(request.args(1) or '')) else 'dbnew'
else:
method = request.args(1) or 'new'
db_ready = {}
db_ready['status'] = get_ticket_storage(app)
db_ready['errmessage'] = T(
@@ -1490,32 +1577,30 @@ def errors():
for fn in tk_db(tk_table.id > 0).select():
try:
error = pickle.loads(fn.ticket_data)
except AttributeError:
hash = hashlib.md5(error['traceback']).hexdigest()
if hash in delete_hashes:
tk_db(tk_table.id == fn.id).delete()
tk_db.commit()
else:
try:
hash2error[hash]['count'] += 1
except KeyError:
error_lines = error['traceback'].split("\n")
last_line = error_lines[-2]
error_causer = os.path.split(error['layer'])[1]
hash2error[hash] = dict(count=1,
pickel=error, causer=error_causer,
last_line=last_line, hash=hash,
ticket=fn.ticket_id)
except AttributeError, e:
tk_db(tk_table.id == fn.id).delete()
tk_db.commit()
hash = hashlib.md5(error['traceback']).hexdigest()
if hash in delete_hashes:
tk_db(tk_table.id == fn.id).delete()
tk_db.commit()
else:
try:
hash2error['hash']['count'] += 1
except KeyError:
error_lines = error['traceback'].split("\n")
last_line = error_lines[-2]
error_causer = os.path.split(error['layer'])[1]
hash2error[hash] = dict(count=1, pickel=error,
causer=error_causer,
last_line=last_line,
hash=hash, ticket=fn.ticket_id)
decorated = [(x['count'], x) for x in hash2error.values()]
decorated.sort(key=operator.itemgetter(0), reverse=True)
return dict(errors=[x[1] for x in decorated], app=app, method=method)
return dict(errors=[x[1] for x in decorated], app=app,
method=method, db_ready=db_ready)
elif method == 'dbold':
tk_db, tk_table = get_ticket_storage(app)
@@ -1523,16 +1608,18 @@ def errors():
if item[:7] == 'delete_':
tk_db(tk_table.ticket_id == item[7:]).delete()
tk_db.commit()
tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id, tk_table.created_datetime, orderby=~tk_table.created_datetime)
tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id,
tk_table.created_datetime,
orderby=~tk_table.created_datetime)
tickets = [row.ticket_id for row in tickets_]
times = dict(
[(row.ticket_id, row.created_datetime) for row in tickets_])
return dict(app=app, tickets=tickets, method=method, times=times)
times = dict([(row.ticket_id, row.created_datetime) for
row in tickets_])
return dict(app=app, tickets=tickets, method=method,
times=times, db_ready=db_ready)
else:
for item in request.vars:
# delete_all} rows doesn't contain any ticket
# delete_all rows doesn't contain any ticket
# Remove anything else as requested
if item[:7] == 'delete_' and (not item == "delete_all}"):
os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request))
@@ -1552,6 +1639,9 @@ def get_ticket_storage(app):
if os.path.exists(ticket_file):
db_string = open(ticket_file).read()
db_string = db_string.strip().replace('\r', '').replace('\n', '')
elif is_gae:
# use Datastore as fallback if there is no ticket_file
db_string = "google:datastore"
else:
return False
tickets_table = 'web2py_ticket'
@@ -1804,24 +1894,7 @@ def plugins():
"public/api.json/action/list/content/Package?package" +
"_type=plugin&search_index=false").read()
session.plugins = loads_json(rawlist)
plugins = TABLE(
*[TR(TD(H5(article["article"]["title"]),
A(T("Install"),
_href=URL(c="default",
f="install_plugin",
args=[app,],
vars={"source":
article["package_data"]["download"],
"plugin": article["article"]["title"]}
))),
TD(article["article"]["description"], BR(),
A(T("Plugin page"),
_href="http://www.web2pyslices.com/slice/show/%s/" % \
article["article"]["id"])),
TD(IMG(_src="http://www.web2pyslices.com/download/%s" % \
article["article"]["thumbnail"])))
for article in session.plugins["results"]])
return dict(plugins=plugins, app=request.args(0))
return dict(plugins=session.plugins["results"], app=request.args(0))
def install_plugin():
app = request.args(0)
+2 -1
View File
@@ -3,6 +3,7 @@ import cStringIO
import gluon.contrib.shell
import code
import thread
import cgi
from gluon.shell import env
if DEMO_MODE or MULTI_USER_MODE:
@@ -40,7 +41,7 @@ def callback():
k = len(session['commands:' + app]) - 1
#output = PRE(output)
#return TABLE(TR('In[%i]:'%k,PRE(command)),TR('Out[%i]:'%k,output))
return 'In [%i] : %s%s\n' % (k + 1, command, output)
return cgi.escape('In [%i] : %s%s\n' % (k + 1, command, output))
def reset():
+16 -2
View File
@@ -1,4 +1,4 @@
# coding: utf8
# -*- coding: utf-8 -*-
{
'!langcode!': 'es',
'!langname!': 'Español',
@@ -9,6 +9,7 @@
'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S',
'(requires internet access, experimental)': '(requiere acceso a internet, experimental)',
'(something like "it-it")': '(algo como "it-it")',
'(version %s)': '(version %s)',
'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(file **gluon/contrib/plural_rules/%s.py** is not found)',
'@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page',
'@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files',
@@ -122,6 +123,7 @@
'delete plugin': 'eliminar plugin',
'Delete this file (you will be asked to confirm deletion)': 'Elimine este fichero (se le pedirá confirmación)',
'Delete:': 'Elimine:',
'Demo': 'Demo',
'Deploy': 'Deploy',
'Deploy on Google App Engine': 'Instale en Google App Engine',
'Deploy to OpenShift': 'Instale en OpenShift',
@@ -134,6 +136,7 @@
'direction: ltr': 'direction: ltr',
'Disable': 'Deshabilitar',
'docs': 'docs',
'Docs': 'Docs',
'Done!': 'Done!',
'done!': 'listo!',
'Download': 'Descargar',
@@ -157,6 +160,7 @@
'Editing myemail': 'Editing myemail',
'Editing rbare': 'Editing rbare',
'Editing ul': 'Editing ul',
'Enable': 'Enable',
'Enterprise Web Framework': 'Armazón Empresarial para Internet',
'Error': 'Error',
'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"',
@@ -190,6 +194,8 @@
'First name': 'Nombre',
'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].',
'Git Pull': 'Git Pull',
'Git Push': 'Git Push',
'Globals##debug': 'Globals',
'graph model': 'graph model',
'Group ID': 'ID de Grupo',
@@ -213,6 +219,7 @@
'internal error': 'error interno',
'Internal State': 'Estado Interno',
'Invalid action': 'Acción inválida',
'Invalid application name': 'Invalid application name',
'Invalid email': 'Correo inválido',
'invalid password': 'contraseña inválida',
'invalid password.': 'invalid password.',
@@ -230,6 +237,7 @@
'Last name': 'Apellido',
'Last saved on:': 'Guardado en:',
'License for': 'Licencia para',
'License:': 'License:',
'lists by ticket': 'lists by ticket',
'loading...': 'cargando...',
'locals': 'locals',
@@ -250,6 +258,7 @@
'new application "%s" created': 'nueva aplicación "%s" creada',
'New application wizard': 'Asistente para nueva aplicación',
'new plugin installed': 'nuevo plugin instalado',
'New plugin installed: %s': 'New plugin installed: %s',
'New plugin installed: web2py.plugin.attachment.w2p': 'New plugin installed: web2py.plugin.attachment.w2p',
'New plugin installed: web2py.plugin.dialog.w2p': 'New plugin installed: web2py.plugin.dialog.w2p',
'New plugin installed: web2py.plugin.math2py.w2p': 'New plugin installed: web2py.plugin.math2py.w2p',
@@ -310,6 +319,8 @@
'Removed Breakpoint on %s at line %s': 'Eliminado punto de ruptura en %s en la línea %s',
'Replace': 'Reemplazar',
'Replace All': 'Reemplazar todos',
'Repository (%s)': 'Repository (%s)',
'Repository: %s': 'Repository: %s',
'request': 'request',
'Resolve Conflict file': 'archivo Resolución de Conflicto',
'response': 'response',
@@ -329,6 +340,8 @@
'Save file: %s': 'Guardar: %s',
'Save via Ajax': 'Guardar via Ajax',
'Saved file hash:': 'Hash del archivo guardado:',
'Screenshot %s': 'Screenshot %s',
'Screenshots': 'Screenshots',
'selected': 'seleccionado(s)',
'session': 'session',
'session expired': 'sesión expirada',
@@ -336,6 +349,7 @@
'shell': 'shell',
'Site': 'sitio',
'some files could not be removed': 'algunos archivos no pudieron ser removidos',
'source : filesystem': 'source : filesystem',
'Start searching': 'Iniciar búsqueda',
'Start wizard': 'Iniciar asistente',
'state': 'estado',
@@ -355,8 +369,8 @@
'test': 'probar',
'Testing application': 'Probando aplicación',
'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "consulta" es una condición como "db.tabla1.campo1==\'valor\'". Algo como "db.tabla1.campo1==db.tabla2.campo2" resulta en un JOIN SQL.',
'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador',
'The application logic, each URL path is mapped in one exposed function in the controller': 'La lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador',
'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador',
'the data representation, define database tables and sets': 'la representación de datos, define tablas y conjuntos de base de datos',
'The data representation, define database tables and sets': 'La representación de datos, define tablas y conjuntos de base de datos',
'The presentations layer, views are also known as templates': 'La capa de presentación, las vistas también son llamadas plantillas',
+1 -1
View File
@@ -144,7 +144,7 @@ if session.is_mobile == 'true':
elif session.is_mobile == 'false':
is_mobile = False
else:
is_mobile = request.user_agent().is_mobile
is_mobile = request.user_agent().get('is_mobile',False)
if DEMO_MODE:
session.authorized = True
+2 -2
View File
@@ -24,7 +24,7 @@ def button_enable(href, app):
return A(label, _class='button btn', _id=id, callback=href, target=id)
def sp_button(href, label):
if request.user_agent().is_mobile:
if request.user_agent().get('is_mobile'):
ret = A_button(SPAN(label), _href=href)
else:
ret = A(SPAN(label), _class='button special btn btn-inverse', _href=href)
@@ -37,4 +37,4 @@ def searchbox(elementid):
return SPAN(LABEL(IMG(_id="search_start", _src=URL('static', 'images/search.png'), _alt=T('filter')),
_class='icon', _for=elementid), ' ',
INPUT(_id=elementid, _type='text', _size=12, _class="input-medium"),
_class="searchbox")
_class="searchbox")
@@ -1,7 +1,13 @@
/* TODO rename this file as web2py-editor.css */
/* Fullscreen */
.CodeMirror-fullscreen {
z-index: 1030;
}
.CodeMirror {
border-top: 1px solid #ddd;
/*border-left: 1px solid #ddd;*/
border-bottom: 1px solid #ddd;
}
/* BREAKPOINTS */
@@ -36,3 +42,15 @@
/*.nav-tabs>li {
min-width: 100px;
}*/
#windows_divs > div {
position: fixed;
height: 30%;
left: 0;
background: white;
right: 0;
bottom: 41px;
z-index: 1030;
overflow: inherit;
border-top: 1px solid #ddd;
}
+4 -4
View File
@@ -248,7 +248,7 @@ function keepalive(url) {
});
}
function load_file(url) {
function load_file(url, lineno) {
$.ajax({
type: "GET",
contentType: 'application/json',
@@ -263,13 +263,13 @@ function load_file(url) {
var tab_header = '<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>';
if(json['force'] === false) {
$('#filesTab').append($(tab_header));
$('#myTabContent').append($(tab_body));
$('#myTabContent').append($(tab_body)); // First load the body
$('#filesTab').append($(tab_header)); // Then load the header which trigger the shown event
} else {
$('#' + json['id']).html($(tab_body));
}
}
$("a[href='#" + json['id'] + "']").click();
$("a[href='#" + json['id'] + "']").trigger('click', lineno);
}
},
error: function (x) {
+24 -6
View File
@@ -485,8 +485,10 @@
el.addClass('disabled');
var method = el.prop('type') == 'submit' ? 'val' : 'html';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state*/
el.data('w2p:enable-with', el[method]());
/*store enabled state if not already disabled */
if (el.data('w2p:enable-with') === undefined) {
el.data('w2p:enable-with', el[method]());
}
/*if you don't want to see "working..." on buttons, replace the following
* two lines with this one
* el.data('w2p_disable_with', el[method]());
@@ -633,7 +635,9 @@
if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]())
}
element.data('w2p:enable-with', element[method]());
if (element.data('w2p:enable-with') === undefined) {
element.data('w2p:enable-with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true);
});
@@ -647,7 +651,10 @@
form.find(web2py.enableSelector).each(function () {
var element = $(this),
method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) element[method](element.data('w2p:enable-with'));
if(element.data('w2p:enable-with')) {
element[method](element.data('w2p:enable-with'));
element.removeData('w2p:enable-with');
}
element.prop('disabled', false);
});
},
@@ -659,7 +666,18 @@
el.on('ajax:complete', 'form[data-w2p_target]', function (e) {
web2py.enableFormElements($(this));
});
}
},
/* Invalidate and force reload of a web2py component
*/
invalidate: function(target) {
$('div[data-w2p_remote]', target).each(function () {
var el = $('#' + $(this).attr('id')).get(0);
if (el.timing !== undefined) { // Block triggering regular routines
clearInterval(el.timing);
}
});
$.web2py.component_handler(target);
},
}
/*end of functions */
@@ -679,7 +697,7 @@
/* compatibility code - start */
ajax = jQuery.web2py.ajax;
web2py_component = jQuery.web2py.component;
web2py_websocket = jQuery.web2py.websocket;
web2py_websocket = jQuery.web2py.web2py_websocket;
web2py_ajax_page = jQuery.web2py.ajax_page;
/*needed for IS_STRONG(entropy)*/
web2py_validate_entropy = jQuery.web2py.validate_entropy;
+88 -24
View File
@@ -1,4 +1,32 @@
{{extend 'layout.html'}}
{{
dirs=[{'name':'models', 'reg':'.*\.py$'},
{'name':'controllers', 'reg':'.*\.py$'},
{'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'},
{'name':'modules', 'reg':'.*\.py$'},
{'name':'static', 'reg': '[^\.#].*'}]
def file_create_form(location, anchor=None, helptext=""):
form=FORM(
LABEL(T("create file with filename:")),
SELECT(_name='dir', _style='width:100px;',
*[OPTION(dir['name'], _value=dir['name']) for dir in dirs]),
XML('&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 -->
{{
def shortcut(combo, description):
@@ -45,6 +73,7 @@
<script src="{{=cm}}/addon/fold/xml-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/comment/comment.js"></script>
<link rel="stylesheet" href="{{=cm}}/addon/fold/foldgutter.css">
<script src="{{=cm}}/emmet.min.js"></script>
<script src="{{=js_url}}/ajax_editor.js"></script>
@@ -52,12 +81,17 @@
<link rel="stylesheet" href="{{=css_url}}/web2py-codemirror.css">
<script type="text/javascript">
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 editor = $(tab_id + " textarea").data('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();
if (lineno !== undefined) {
editor.setCursor(lineno);
editor.centerOnCursor();
}
}
var n_li = $('#filesTab li').length;
$.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) {
e.preventDefault();
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*/
@@ -164,7 +199,7 @@ $(document).on('click', 'a.font_button', function (e) {
});
{{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}}
</script>
@@ -197,27 +232,22 @@ $(document).on('click', 'a.font_button', function (e) {
<div class="files-menu">
<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>
{{dirs=[{'name':'models', 'reg':'.*\.py$'},
{'name':'controllers', 'reg':'.*\.py$'},
{'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'},
{'name':'modules', 'reg':'.*\.py$'}, ]}}
{{auto_complete_list=[]}}
{{for dir in dirs:}}
<li class="nav-header component" onclick="collapse('{{="%s_files" % dir['name']}}');">{{=dir['name']}}</li>
<li id="{{="%s_files" % dir['name']}}">
<ul class="nav nav-list small-font">
{{for f in listfiles(app, dir['name'], regexp=dir['reg'] ):}}
{{id="%s__" % dir['name'] + f.replace('.','__')}}
{{current_file = request.args(len(request.args) - 1)}}
<li class="{{#='active' if current_file==f else ''}}" style="overflow:hidden">
{{a_tag=editfile(dir['name'], f, dict(id=id))}}
{{=a_tag}}
{{auto_complete_list.append({'value':f, 'tokens':f.split('/'), 'a_tag':a_tag})}}
</li>
{{pass}}
</ul>
</li>
{{pass}}
<br />
<div>
<button onclick="jQuery('#form').slideToggle()" class="btn btn-mini">{{=T('Create')}}</button>
<br />
<div id="form">
<div>
{{=file_create_form('%s/' % app, '')}}
</div>
</div>
<script>jQuery('#form').slideToggle()</script>
</div>
<br />
<div id="files_menu">
{{=LOAD('default', 'files_menu', vars={'app':app}, ajax=True)}}
</div>
</ul>
</div>
</div>
@@ -227,13 +257,47 @@ $(document).on('click', 'a.font_button', function (e) {
<div id="myTabContent" class="tab-content">
</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>
{{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>
$(document).ready(function() {
var filesMenu = $('#files');
var ow = filesMenu.outerWidth();
filesMenu.width(ow);
$('#files').css('left', '-'+ow+'px');
$.web2py.trap_form('url', 'form');
$('#form form').addClass('no_trap'); // Let to reuse the same form
});
</script>
<!-- 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'],
URL(c='debug', f='toggle_breakpoint')))}}, sel);
});
function makeMarker() {
var marker = document.createElement("div");
marker.style.color = "#822";
marker.innerHTML = "●";
marker.className = "breakpoint";
return marker;
}
function makeMarker() {
var marker = document.createElement("div");
marker.style.color = "#822";
marker.innerHTML = "●";
marker.className = "breakpoint";
return marker;
}
{{if filetype in ('html', 'javascript', 'css'):}}
{{if filetype in ('html', 'javascript', 'css'):}}
// must be here or break emmet/zencoding
CodeMirror.defaults.extraKeys["Ctrl-S"] =
function(instance) {
@@ -94,26 +94,27 @@
CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
{{pass}}
{{if filetype=='python':}}
// must be here or break emmet/zencoding for python
CodeMirror.defaults.extraKeys["Ctrl-S"] =
function(instance) {
doClickSave();};
CodeMirror.defaults.extraKeys["Ctrl-Space"] = "autocomplete";
CodeMirror.defaults.extraKeys["Tab"] = "indentMore";
CodeMirror.defaults.extraKeys["Shift-Tab"] = "indentLess";
CodeMirror.defaults.extraKeys["Ctrl-F11"] = function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
//for autocomplete
CodeMirror.commands.autocomplete = function(cm) {
CodeMirror.showHint(cm, CodeMirror.pythonHint);
}
{{pass}}
{{pass}}
{{if filetype=='python':}}
// must be here or break emmet/zencoding for python
CodeMirror.defaults.extraKeys["Ctrl-S"] =
function(instance) {
doClickSave();};
CodeMirror.defaults.extraKeys["Ctrl-Space"] = "autocomplete";
CodeMirror.defaults.extraKeys["Tab"] = "indentMore";
CodeMirror.defaults.extraKeys["Shift-Tab"] = "indentLess";
CodeMirror.defaults.extraKeys["Ctrl-F11"] = function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
CodeMirror.defaults.extraKeys["Shift-Esc"] = function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
//for autocomplete
CodeMirror.commands.autocomplete = function(cm) {
CodeMirror.showHint(cm, CodeMirror.pythonHint);
}
{{pass}}
CodeMirror.defaults.extraKeys["Ctrl-/"] = "toggleComment";
store_changes_function = function(instance, changeObj) {
jQuery(instance).data('saved', false);
instance.off("change", store_changes_function);
@@ -128,6 +129,19 @@
request.env['wsgi_url_scheme'], request.env['http_host'],
URL(c='debug', f='list_breakpoints')))}}, editor);
// TODO move it in a separated file
CodeMirror.defineExtension("centerOnCursor", function(limit) {
var coords = this.cursorCoords(null, "local");
if (this.getScrollerElement().clientHeight === 0 && limit !== 10) {
if (limit === undefined) limit = 1;
else limit += 1;
editor = this;
setTimeout(function() {editor.centerOnCursor()}, 100);
return;
}
clientHeight = (this.getScrollerElement().clientHeight / 2)
this.scrollTo(null, (coords.top + coords.bottom)/2 - 10);
});
</script>
<div class="editor-bar-bottom" style="margin-top:9px;">
@@ -141,34 +155,26 @@
<div class="row-fluid">
<div class="help alert alert-block alert-info">
{{if filetype=='html':}}
{{if filetype=='html':}}
<h3>{{=T('Key bindings for ZenCoding Plugin')}}</h3>
<ul class="keybindings unstyled">
{{=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:}}
{{else:}}
<h3>{{=T("Key bindings")}}</h3>
{{pass}}
<ul class="keybindings unstyled">
{{=shortcut('Ctrl+S', T('Save via Ajax'))}}
{{=shortcut('Ctrl+F11', T('Toggle Fullscreen'))}}
{{=shortcut('Shift+Esc', T('Exit Fullscreen'))}}
{{if filetype=='python':}}
{{=shortcut('Ctrl-Space', T('Autocomplete Python Code'))}}
{{pass}}
{{=shortcut('Ctrl-F / Cmd-F', T('Start searching'))}}
{{=shortcut('Ctrl-G / Cmd-G', T('Find Next'))}}
{{=shortcut('Shift-Ctrl-G / Shift-Cmd-G', T('Find Previous'))}}
{{=shortcut('Shift-Ctrl-F / Cmd-Option-F', T('Replace'))}}
{{=shortcut('Shift-Ctrl-R / Shift-Cmd-Option-F', T('Replace All'))}}
</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}}
</ul>
</div>
</div>
@@ -0,0 +1,3 @@
{{for files in result_files:}}
{{=files}}
{{pass}}
+17 -1
View File
@@ -1,5 +1,21 @@
{{extend 'layout.html'}}
{{=H3("Available plugins")}}
{{=P("Source: web2pyslices")}}
{{=plugins}}
{{articles = []}}
{{for article in plugins:}}
{{screenshots = [A(" ", T("Screenshot %s") % (x+1), " ", _href=item) for (x, item) in enumerate(article["package_data"]["screenshots"])]}}
{{articles.append(TR(TD(H5(article["article"]["title"]),
button(URL(c="default", f="install_plugin", args=[app,], vars={"source": article["package_data"]["download"], "plugin": article["article"]["title"]}), T("Install")),
BR(),
IMG(_src="http://www.web2pyslices.com/download/%s" % article["article"]["thumbnail"], _style="margin-top: 1em;"), _style="width: 20em;"),
TD(article["article"]["description"], BR(),
A(T("Plugin page"), _href="http://www.web2pyslices.com/slice/show/%s/" % article["article"]["id"]), " | ",
A(T("Demo"), _href=article["package_data"]["demo"]), " | ",
A(T("Docs"), _href=article["package_data"]["documentation"]), " | ",
A(T("Repository (%s)") % article["package_data"]["repository_brand"], _href=article["package_data"]["repository_page"]), " | ",
A(T("License:"), " ", (article["package_data"]["license_type"] or "").upper(), " ", T("(version %s)") % article["package_data"]["license_version"] if article["package_data"]["license_version"] else "", _href=article["package_data"]["license_url"]), " | " if screenshots else "", *screenshots, _style="width: 40em;"))
)
}}
{{pass}}
{{=TABLE(*articles)}}
@@ -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
============== -->
{{block footer}}
<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}}
{{if hasattr(T,'get_possible_languages_info'):}}
@@ -68,6 +69,7 @@
</span>{{pass}}
</p>
</footer><!-- /#footer -->
{{end}}
<!-- BS JAVASCRIPT
====================== -->
+35 -34
View File
@@ -2,40 +2,41 @@
{{block sectionclass}}shell{{end}}
<!-- begin "shell" block -->
<div id="wrapper">
<div class="row-fluid">
<div class="output-wrapper span8">
<textarea id="output" readonly="readonly">web2py Shell {{=request.env.web2py_version}}</textarea>
</div>
</div>
<div class="row-fluid">
<form id="form" action="{{=URL(r=request,f='callback',args=app)}}" method="get" class="span8">
<div id="shellwrapper">
<div class="prompt-wrapper">
<div class="prompt-container">
<textarea class="prompt" name="statement" id="statement"></textarea>
</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.')}}">
{{=helpicon()}}
<span>Type some Python code in here and hit Return (Enter) to execute it.</span>
</a>
</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="help alert alert-info span6">
<ul>
<li>Using the shell may lock the database to other users of this app.</li>
<li>Each db statement is automatically committed.</li>
<li>Creating new tables dynamically is not allowed.</li>
<li>Models are automatically imported in the shell.</li>
</ul>
</div>
<div class="row-fluid">
<div class="output-wrapper span8">
<textarea id="output" readonly="readonly">web2py Shell {{=request.env.web2py_version}}
</textarea>
</div>
</div>
<div class="row-fluid">
<form id="form" action="{{=URL(r=request,f='callback',args=app)}}" method="get" class="span8">
<div id="shellwrapper">
<div class="prompt-wrapper">
<div class="prompt-container">
<textarea class="prompt" name="statement" id="statement"></textarea>
</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.')}}">
{{=helpicon()}}
<span>Type some Python code in here and hit Return (Enter) to execute it.</span>
</a>
</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="help alert alert-info span6">
<ul>
<li>Using the shell may lock the database to other users of this app.</li>
<li>Each db statement is automatically committed.</li>
<li>Creating new tables dynamically is not allowed.</li>
<li>Models are automatically imported in the shell.</li>
</ul>
</div>
</div>
</div>
<script type="text/javascript" src="{{=URL('static', 'js/autoscroll.js')}}"></script>
@@ -113,4 +114,4 @@ jQuery(document).ready(function(){
});
});
</script>
<!-- end "shell" block -->
<!-- end "shell" block -->
+24 -6
View File
@@ -485,8 +485,10 @@
el.addClass('disabled');
var method = el.prop('type') == 'submit' ? 'val' : 'html';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state*/
el.data('w2p:enable-with', el[method]());
/*store enabled state if not already disabled */
if (el.data('w2p:enable-with') === undefined) {
el.data('w2p:enable-with', el[method]());
}
/*if you don't want to see "working..." on buttons, replace the following
* two lines with this one
* el.data('w2p_disable_with', el[method]());
@@ -633,7 +635,9 @@
if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]())
}
element.data('w2p:enable-with', element[method]());
if (element.data('w2p:enable-with') === undefined) {
element.data('w2p:enable-with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true);
});
@@ -647,7 +651,10 @@
form.find(web2py.enableSelector).each(function () {
var element = $(this),
method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) element[method](element.data('w2p:enable-with'));
if(element.data('w2p:enable-with')) {
element[method](element.data('w2p:enable-with'));
element.removeData('w2p:enable-with');
}
element.prop('disabled', false);
});
},
@@ -659,7 +666,18 @@
el.on('ajax:complete', 'form[data-w2p_target]', function (e) {
web2py.enableFormElements($(this));
});
}
},
/* Invalidate and force reload of a web2py component
*/
invalidate: function(target) {
$('div[data-w2p_remote]', target).each(function () {
var el = $('#' + $(this).attr('id')).get(0);
if (el.timing !== undefined) { // Block triggering regular routines
clearInterval(el.timing);
}
});
$.web2py.component_handler(target);
},
}
/*end of functions */
@@ -679,7 +697,7 @@
/* compatibility code - start */
ajax = jQuery.web2py.ajax;
web2py_component = jQuery.web2py.component;
web2py_websocket = jQuery.web2py.websocket;
web2py_websocket = jQuery.web2py.web2py_websocket;
web2py_ajax_page = jQuery.web2py.ajax_page;
/*needed for IS_STRONG(entropy)*/
web2py_validate_entropy = jQuery.web2py.validate_entropy;
@@ -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 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="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>
+24 -6
View File
@@ -485,8 +485,10 @@
el.addClass('disabled');
var method = el.prop('type') == 'submit' ? 'val' : 'html';
var disable_with_message = (typeof w2p_ajax_disable_with_message != 'undefined') ? w2p_ajax_disable_with_message : "Working...";
/*store enabled state*/
el.data('w2p:enable-with', el[method]());
/*store enabled state if not already disabled */
if (el.data('w2p:enable-with') === undefined) {
el.data('w2p:enable-with', el[method]());
}
/*if you don't want to see "working..." on buttons, replace the following
* two lines with this one
* el.data('w2p_disable_with', el[method]());
@@ -633,7 +635,9 @@
if(disable_with == undefined) {
element.data('w2p_disable_with', element[method]())
}
element.data('w2p:enable-with', element[method]());
if (element.data('w2p:enable-with') === undefined) {
element.data('w2p:enable-with', element[method]());
}
element[method](element.data('w2p_disable_with'));
element.prop('disabled', true);
});
@@ -647,7 +651,10 @@
form.find(web2py.enableSelector).each(function () {
var element = $(this),
method = element.is('button') ? 'html' : 'val';
if(element.data('w2p:enable-with')) element[method](element.data('w2p:enable-with'));
if(element.data('w2p:enable-with')) {
element[method](element.data('w2p:enable-with'));
element.removeData('w2p:enable-with');
}
element.prop('disabled', false);
});
},
@@ -659,7 +666,18 @@
el.on('ajax:complete', 'form[data-w2p_target]', function (e) {
web2py.enableFormElements($(this));
});
}
},
/* Invalidate and force reload of a web2py component
*/
invalidate: function(target) {
$('div[data-w2p_remote]', target).each(function () {
var el = $('#' + $(this).attr('id')).get(0);
if (el.timing !== undefined) { // Block triggering regular routines
clearInterval(el.timing);
}
});
$.web2py.component_handler(target);
},
}
/*end of functions */
@@ -679,7 +697,7 @@
/* compatibility code - start */
ajax = jQuery.web2py.ajax;
web2py_component = jQuery.web2py.component;
web2py_websocket = jQuery.web2py.websocket;
web2py_websocket = jQuery.web2py.web2py_websocket;
web2py_ajax_page = jQuery.web2py.ajax_page;
/*needed for IS_STRONG(entropy)*/
web2py_validate_entropy = jQuery.web2py.validate_entropy;
+2 -2
View File
@@ -65,8 +65,8 @@ skip_files: |
(.*\.py[co])|
(.*/RCS/.*)|
(\..*)|
(applications/(admin|examples)/.*)|
((admin|examples|welcome)\.(w2p|tar))|
(applications/examples/.*)|
((examples|welcome)\.(w2p|tar))|
(applications/.*?/(cron|databases|errors|cache|sessions)/.*)|
((logs|scripts)/.*)|
(anyserver\.py)|
+2
View File
@@ -159,6 +159,8 @@ def extension(url):
def expand_one(url, cdict):
# try ombed but first check in cache
if '@' in url and not '://'in url:
return '<a href="mailto:%s">%s</a>' % (url, url)
if cdict and url in cdict:
r = cdict[url]
else:
+7 -2
View File
@@ -551,7 +551,7 @@ regex_list=re.compile('^(?:(?:(#{1,6})|(?:(\.+|\++|\-+)(\.)?))\s*)?(.*)$')
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_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_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)
@@ -648,7 +648,9 @@ def autolinks_simple(url):
image, video or audio tag
"""
u_url=url.lower()
if u_url.endswith(('.jpg','.jpeg','.gif','.png')):
if '@' in url and not '://' in url:
return '<a href="mailto:%s">%s</a>' % (url, url)
elif u_url.endswith(('.jpg','.jpeg','.gif','.png')):
return '<img src="%s" controls />' % url
elif u_url.endswith(('.mp4','.mpeg','.mov','.ogv')):
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 proto+':'+url
def email_simple(email):
return '<a href="mailto:%s">%s</a>' % (email, email)
def render(text,
extra={},
allowed={},
+1 -1
View File
@@ -258,7 +258,7 @@ def run(history, statement, env={}):
if not name.startswith('__'):
try:
history.set_global(name, val)
except TypeError, ex:
except (TypeError, cPickle.PicklingError), ex:
UNPICKLABLE_TYPES.append(type(val))
history.add_unpicklable(statement, new_globals.keys())
+2 -2
View File
@@ -858,8 +858,8 @@ class Sheet:
sorted_headers = [TH(), ] + \
[TH(header[1]) for header in sorted(unsorted_headers)]
table.insert(0, TR(*sorted_headers,
**{_class: "%s_fieldnames" %
attributes["_class"]}))
**{'_class': "%s_fieldnames" %
attributes["_class"]}))
else:
data = SCRIPT(""" // web2py Spreadsheets: no db data.""")
+303 -163
View File
@@ -252,7 +252,8 @@ THREAD_LOCAL = threading.local()
REGEX_TYPE = re.compile('^([\w\_\:]+)')
REGEX_DBNAME = re.compile('^(\w+)(\:\w+)*')
REGEX_W = re.compile('^\w+$')
REGEX_TABLE_DOT_FIELD = re.compile('^(\w+)\.(\w+)$')
REGEX_TABLE_DOT_FIELD = re.compile('^(\w+)\.([^.]+)$')
REGEX_NO_GREEDY_ENTITY_NAME = r'(.+?)'
REGEX_UPLOAD_PATTERN = re.compile('(?P<table>[\w\-]+)\.(?P<field>[\w\-]+)\.(?P<uuidkey>[\w\-]+)(\.(?P<name>\w+))?\.\w+$')
REGEX_CLEANUP_FN = re.compile('[\'"\s;]+')
REGEX_UNPACK = re.compile('(?<!\|)\|(?!\|)')
@@ -670,12 +671,27 @@ class ConnectionPool(object):
break
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
###################################################################################
class BaseAdapter(ConnectionPool):
__metaclass__ = AdapterMeta
native_json = False
driver = None
driver_name = None
@@ -687,12 +703,14 @@ class BaseAdapter(ConnectionPool):
can_select_for_update = True
dbpath = None
folder = None
connector = lambda *args, **kwargs: None # __init__ should override this
TRUE = 'T'
FALSE = 'F'
T_SEP = ' '
QUOTE_TEMPLATE = '"%s"'
types = {
'boolean': 'CHAR(1)',
'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'
'big-id': 'BIGINT PRIMARY KEY AUTOINCREMENT',
'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):
@@ -834,8 +853,9 @@ class BaseAdapter(ConnectionPool):
self.connection = Dummy()
self.cursor = Dummy()
def sequence_name(self,tablename):
return '%s_sequence' % tablename
return self.QUOTE_TEMPLATE % ('%s_sequence' % tablename)
def trigger_name(self,tablename):
return '%s_sequence' % tablename
@@ -868,61 +888,72 @@ class BaseAdapter(ConnectionPool):
if referenced == '.':
referenced = tablename
constraint_name = self.constraint_name(tablename, field_name)
if not '.' in referenced \
and referenced != tablename \
and hasattr(table,'_primarykey'):
ftype = types['integer']
else:
if hasattr(table,'_primarykey'):
# if not '.' in referenced \
# and referenced != tablename \
# and hasattr(table,'_primarykey'):
# ftype = types['integer']
#else:
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('.')
rtable = db[rtablename]
rfield = rtable[rfieldname]
# must be PK reference or unique
if rfieldname in rtable._primarykey or \
rfield.unique:
ftype = types[rfield.type[:9]] % \
dict(length=rfield.length)
# multicolumn primary key reference?
if not rfield.unique and len(rtable._primarykey)>1:
# then it has to be a table level FK
if rtablename not in TFK:
TFK[rtablename] = {}
TFK[rtablename][rfieldname] = field_name
else:
ftype = ftype + \
types['reference FK'] % dict(
constraint_name = constraint_name, # should be quoted
foreign_key = '%s (%s)' % (rtablename,
rfieldname),
table_name = tablename,
field_name = field._rname or field.name,
on_delete_action=field.ondelete)
except Exception, e:
LOGGER.debug('Error: %s' %e)
raise KeyError('Cannot resolve reference %s in %s definition' % (referenced, table._tablename))
# must be PK reference or unique
if getattr(rtable, '_primarykey', None) and rfieldname in rtable._primarykey or \
rfield.unique:
ftype = types[rfield.type[:9]] % \
dict(length=rfield.length)
# multicolumn primary key reference?
if not rfield.unique and len(rtable._primarykey)>1:
# then it has to be a table level FK
if rtablename not in TFK:
TFK[rtablename] = {}
TFK[rtablename][rfieldname] = field_name
else:
# make a guess here for circular references
if referenced in db:
id_fieldname = db[referenced]._id.name
elif referenced == tablename:
id_fieldname = table._id.name
else: #make a guess
id_fieldname = 'id'
#gotcha: the referenced table must be defined before
#the referencing one to be able to create the table
#Also if it's not recommended, we can still support
#references to tablenames without rname to make
#migrations and model relationship work also if tables
#are not defined in order
real_referenced = (
(db[referenced]._rname or db[referenced])
if referenced == tablename or referenced in db
else referenced)
ftype = types[field_type[:9]] % dict(
index_name = field_name+'__idx',
field_name = field._rname or field.name,
constraint_name = constraint_name,
foreign_key = '%s (%s)' % (real_referenced,
id_fieldname),
on_delete_action=field.ondelete)
ftype = ftype + \
types['reference FK'] % dict(
constraint_name = constraint_name, # should be quoted
foreign_key = rtable.sqlsafe + ' (' + rfield.sqlsafe_name + ')',
table_name = table.sqlsafe,
field_name = field.sqlsafe_name,
on_delete_action=field.ondelete)
else:
# make a guess here for circular references
if referenced in db:
id_fieldname = db[referenced]._id.sqlsafe_name
elif referenced == tablename:
id_fieldname = table._id.sqlsafe_name
else: #make a guess
id_fieldname = self.QUOTE_TEMPLATE % 'id'
#gotcha: the referenced table must be defined before
#the referencing one to be able to create the table
#Also if it's not recommended, we can still support
#references to tablenames without rname to make
#migrations and model relationship work also if tables
#are not defined in order
if referenced == tablename:
real_referenced = db[referenced].sqlsafe
else:
real_referenced = (referenced in db
and db[referenced].sqlsafe
or referenced)
rfield = db[referenced]._id
ftype = types[field_type[:9]] % dict(
index_name = self.QUOTE_TEMPLATE % (field_name+'__idx'),
field_name = field.sqlsafe_name,
constraint_name = self.QUOTE_TEMPLATE % constraint_name,
foreign_key = '%s (%s)' % (real_referenced, rfield.sqlsafe_name),
on_delete_action=field.ondelete)
elif field_type.startswith('list:reference'):
ftype = types[field_type[:14]]
elif field_type.startswith('decimal'):
@@ -995,39 +1026,38 @@ class BaseAdapter(ConnectionPool):
# geometry fields are added after the table has been created, not now
if not (self.dbengine == 'postgres' and \
field_type.startswith('geom')):
#fetch the rname if it's there
field_rname = "%s" % (field._rname or field_name)
fields.append('%s %s' % (field_rname, ftype))
fields.append('%s %s' % (field.sqlsafe_name, ftype))
other = ';'
# backend-specific extensions to fields
if self.dbengine == 'mysql':
if not hasattr(table, "_primarykey"):
fields.append('PRIMARY KEY(%s)' % (table._id.name or table._id._rname))
other = ' ENGINE=InnoDB CHARACTER SET utf8;'
fields.append('PRIMARY KEY (%s)' % (self.QUOTE_TEMPLATE % table._id.name))
engine = self.adapter_args.get('engine','InnoDB')
other = ' ENGINE=%s CHARACTER SET utf8;' % engine
fields = ',\n '.join(fields)
for rtablename in TFK:
rfields = TFK[rtablename]
pkeys = db[rtablename]._primarykey
fkeys = [ rfields[k] for k in pkeys ]
pkeys = [self.QUOTE_TEMPLATE % pk for pk in db[rtablename]._primarykey]
fkeys = [self.QUOTE_TEMPLATE % rfields[k].name for k in pkeys ]
fields = fields + ',\n ' + \
types['reference TFK'] % dict(
table_name = tablename,
table_name = table.sqlsafe,
field_name=', '.join(fkeys),
foreign_table = rtablename,
foreign_table = table.sqlsafe,
foreign_key = ', '.join(pkeys),
on_delete_action = field.ondelete)
#if there's a _rname, let's use that instead
table_rname = table._rname or tablename
table_rname = table.sqlsafe
if getattr(table,'_primarykey',None):
query = "CREATE TABLE %s(\n %s,\n %s) %s" % \
(table_rname, fields,
self.PRIMARY_KEY(', '.join(table._primarykey)),other)
(table.sqlsafe, fields,
self.PRIMARY_KEY(', '.join([self.QUOTE_TEMPLATE % pk for pk in table._primarykey])),other)
else:
query = "CREATE TABLE %s(\n %s\n)%s" % \
(table_rname, fields, other)
(table.sqlsafe, fields, other)
if self.uri.startswith('sqlite:///') \
or self.uri.startswith('spatialite:///'):
@@ -1105,6 +1135,7 @@ class BaseAdapter(ConnectionPool):
k,v=item
if not isinstance(v,dict):
v=dict(type='unknown',sql=v)
if self.ignore_field_case is not True: return k, v
return k.lower(),v
# make sure all field names are lower case to avoid
# migrations because of case cahnge
@@ -1132,7 +1163,7 @@ class BaseAdapter(ConnectionPool):
query = [ sql_fields[key]['sql'] ]
else:
query = ['ALTER TABLE %s ADD %s %s;' % \
(tablename, key,
(table.sqlsafe, key,
sql_fields_aux[key]['sql'].replace(', ', new_add))]
metadata_change = True
elif self.dbengine in ('sqlite', 'spatialite'):
@@ -1150,10 +1181,11 @@ class BaseAdapter(ConnectionPool):
"'%(table)s', '%(field)s');" %
dict(schema=schema, table=tablename, field=key,) ]
elif self.dbengine in ('firebird',):
query = ['ALTER TABLE %s DROP %s;' % (tablename, key)]
query = ['ALTER TABLE %s DROP %s;' %
(self.QUOTE_TEMPLATE % tablename, self.QUOTE_TEMPLATE % key)]
else:
query = ['ALTER TABLE %s DROP COLUMN %s;' %
(tablename, key)]
(self.QUOTE_TEMPLATE % tablename, self.QUOTE_TEMPLATE % key)]
metadata_change = True
elif sql_fields[key]['sql'] != sql_fields_old[key]['sql'] \
and not (key in table.fields and
@@ -1169,12 +1201,15 @@ class BaseAdapter(ConnectionPool):
else:
drop_expr = 'ALTER TABLE %s DROP COLUMN %s;'
key_tmp = key + '__tmp'
query = ['ALTER TABLE %s ADD %s %s;' % (t, key_tmp, tt),
'UPDATE %s SET %s=%s;' % (t, key_tmp, key),
drop_expr % (t, key),
'ALTER TABLE %s ADD %s %s;' % (t, key, tt),
'UPDATE %s SET %s=%s;' % (t, key, key_tmp),
drop_expr % (t, key_tmp)]
query = ['ALTER TABLE %s ADD %s %s;' % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp, tt),
'UPDATE %s SET %s=%s;' %
(self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp, self.QUOTE_TEMPLATE % key),
drop_expr % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key),
'ALTER TABLE %s ADD %s %s;' %
(self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key, tt),
'UPDATE %s SET %s=%s;' %
(self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key, self.QUOTE_TEMPLATE % key_tmp),
drop_expr % (self.QUOTE_TEMPLATE % t, self.QUOTE_TEMPLATE % key_tmp)]
metadata_change = True
elif sql_fields[key]['type'] != sql_fields_old[key]['type']:
sql_fields_current[key] = sql_fields[key]
@@ -1269,8 +1304,7 @@ class BaseAdapter(ConnectionPool):
return 'PRIMARY KEY(%s)' % key
def _drop(self, table, mode):
table_rname = table._rname or table
return ['DROP TABLE %s;' % table_rname]
return ['DROP TABLE %s;' % table.sqlsafe]
def drop(self, table, mode=''):
db = table._db
@@ -1288,17 +1322,16 @@ class BaseAdapter(ConnectionPool):
self.log('success!\n', table)
def _insert(self, table, fields):
table_rname = table._rname or table
table_rname = table.sqlsafe
if fields:
keys = ','.join(f._rname or f.name for f, v in fields)
keys = ','.join(f.sqlsafe_name for f, v in fields)
values = ','.join(self.expand(v, f.type) for f, v in fields)
return 'INSERT INTO %s(%s) VALUES (%s);' % (table_rname, keys, values)
else:
return self._insert_empty(table)
def _insert_empty(self, table):
table_rname = table._rname or table
return 'INSERT INTO %s DEFAULT VALUES;' % table_rname
return 'INSERT INTO %s DEFAULT VALUES;' % (table.sqlsafe)
def insert(self, table, fields):
query = self._insert(table,fields)
@@ -1428,7 +1461,7 @@ class BaseAdapter(ConnectionPool):
return '(%s)' % ' || '.join(self.expand(x,'string') for x in items)
def ADD(self, first, second):
if self.is_numerical_type(first.type):
if self.is_numerical_type(first.type) or isinstance(first.type, gluon.dal.Field):
return '(%s + %s)' % (self.expand(first),
self.expand(second, first.type))
else:
@@ -1451,13 +1484,13 @@ class BaseAdapter(ConnectionPool):
self.expand(second, first.type))
def AS(self, first, second):
return '%s AS %s' % (self.expand(first), second)
return '%s AS %s' % (self.expand(first), second)
def ON(self, first, second):
table_rname = first._ot and first or first._rname or first._tablename
table_rname = self.table_alias(first)
if use_common_filters(second):
second = self.common_filter(second,[first._tablename])
return '%s ON %s' % (self.expand(table_rname), self.expand(second))
return ('%s ON %s') % (self.expand(table_rname), self.expand(second))
def INVERT(self, first):
return '%s DESC' % self.expand(first)
@@ -1472,13 +1505,10 @@ class BaseAdapter(ConnectionPool):
if isinstance(expression, Field):
et = expression.table
if not colnames:
table_rname = et._ot and et._tablename or et._rname or et._tablename
table_rname = et._ot and self.QUOTE_TEMPLATE % et._tablename or et._rname or self.QUOTE_TEMPLATE % et._tablename
out = '%s.%s' % (table_rname, expression._rname or (self.QUOTE_TEMPLATE % (expression.name)))
else:
table_rname = et._tablename
if not colnames:
out = '%s.%s' % (table_rname, expression._rname or expression.name)
else:
out = '%s.%s' % (table_rname, expression.name)
out = '%s.%s' % (self.QUOTE_TEMPLATE % et._tablename, self.QUOTE_TEMPLATE % expression.name)
if field_type == 'string' and not expression.type in (
'string','text','json','password'):
out = self.CAST(out, self.types['text'])
@@ -1509,10 +1539,11 @@ class BaseAdapter(ConnectionPool):
else:
return str(expression)
def table_alias(self,name):
if not isinstance(name, Table):
name = self.db[name]._rname or self.db[name]
return str(name)
def table_alias(self, tbl):
if not isinstance(tbl, Table):
tbl = self.db[tbl]
return tbl.sqlsafe_alias
def alias(self, table, alias):
"""
@@ -1520,7 +1551,7 @@ class BaseAdapter(ConnectionPool):
with alias name.
"""
other = copy.copy(table)
other['_ot'] = other._ot or other._rname or other._tablename
other['_ot'] = other._ot or other.sqlsafe
other['ALL'] = SQLALL(other)
other['_tablename'] = alias
for fieldname in other.fields:
@@ -1532,8 +1563,7 @@ class BaseAdapter(ConnectionPool):
return other
def _truncate(self, table, mode=''):
tablename = table._rname or table._tablename
return ['TRUNCATE TABLE %s %s;' % (tablename, mode or '')]
return ['TRUNCATE TABLE %s %s;' % (table.sqlsafe, mode or '')]
def truncate(self, table, mode= ' '):
# Prepare functions "write_to_logfile" and "close_logfile"
@@ -1553,10 +1583,10 @@ class BaseAdapter(ConnectionPool):
sql_w = ' WHERE ' + self.expand(query)
else:
sql_w = ''
sql_v = ','.join(['%s=%s' % (field._rname or field.name,
sql_v = ','.join(['%s=%s' % (field.sqlsafe_name,
self.expand(value, field.type)) \
for (field, value) in fields])
tablename = "%s" % (self.db[tablename]._rname or tablename)
tablename = self.db[tablename].sqlsafe
return 'UPDATE %s SET %s%s;' % (tablename, sql_v, sql_w)
def update(self, tablename, query, fields):
@@ -1581,7 +1611,7 @@ class BaseAdapter(ConnectionPool):
sql_w = ' WHERE ' + self.expand(query)
else:
sql_w = ''
tablename = '%s' % (self.db[tablename]._rname or tablename)
tablename = self.db[tablename].sqlsafe
return 'DELETE FROM %s%s;' % (tablename, sql_w)
def delete(self, tablename, query):
@@ -1623,8 +1653,9 @@ class BaseAdapter(ConnectionPool):
if isinstance(item,SQLALL):
new_fields += item._table
elif isinstance(item,str):
if REGEX_TABLE_DOT_FIELD.match(item):
tablename,fieldname = item.split('.')
m = self.REGEX_TABLE_DOT_FIELD.match(item)
if m:
tablename,fieldname = m.groups()
append(db[tablename][fieldname])
else:
append(Expression(db,lambda item=item:item))
@@ -1645,10 +1676,11 @@ class BaseAdapter(ConnectionPool):
tablenames = tables(query)
tablenames_for_common_filters = tablenames
for field in fields:
if isinstance(field, basestring) \
and REGEX_TABLE_DOT_FIELD.match(field):
tn,fn = field.split('.')
field = self.db[tn][fn]
if isinstance(field, basestring):
m = self.REGEX_TABLE_DOT_FIELD.match(field)
if m:
tn,fn = m.groups()
field = self.db[tn][fn]
for tablename in tables(field):
if not tablename in tablenames:
tablenames.append(tablename)
@@ -1732,7 +1764,7 @@ class BaseAdapter(ConnectionPool):
tables_to_merge.keys()])
if joint:
sql_t += ' %s %s' % (command,
','.join([self.table_alias(t) for t in joint]))
','.join([t for t in joint]))
for t in joinon:
sql_t += ' %s %s' % (command, t)
elif inner_join and left:
@@ -1747,7 +1779,7 @@ class BaseAdapter(ConnectionPool):
sql_t += ' %s %s' % (icommand, t)
if joint:
sql_t += ' %s %s' % (command,
','.join([self.table_alias(t) for t in joint]))
','.join([t for t in joint]))
for t in joinon:
sql_t += ' %s %s' % (command, t)
else:
@@ -1767,9 +1799,9 @@ class BaseAdapter(ConnectionPool):
sql_o += ' ORDER BY %s' % self.expand(orderby)
if (limitby and not groupby and tablenames and orderby_on_limitby and not orderby):
sql_o += ' ORDER BY %s' % ', '.join(
['%s.%s'%(t,x) for t in tablenames for x in (
[self.db[t][x].sqlsafe for t in tablenames for x in (
hasattr(self.db[t],'_primarykey') and self.db[t]._primarykey
or [self.db[t]._id._rname or self.db[t]._id.name]
or ['_id']
)
]
)
@@ -1897,8 +1929,10 @@ class BaseAdapter(ConnectionPool):
def create_sequence_and_triggers(self, query, table, **args):
self.execute(query)
def log_execute(self, *a, **b):
if not self.connection: raise ValueError(a[0])
if not self.connection: return None
command = a[0]
if hasattr(self,'filter_sql_command'):
@@ -1955,8 +1989,17 @@ class BaseAdapter(ConnectionPool):
if field_is_type('decimal'):
return str(obj)
elif field_is_type('reference'): # reference
if fieldtype.find('.')>0:
return repr(obj)
# check for tablename first
referenced = fieldtype[9:].strip()
if referenced in self.db.tables:
return str(long(obj))
p = referenced.partition('.')
if p[2] != '':
try:
ftype = self.db[p[0]][p[2]].type
return self.represent(obj, ftype)
except (ValueError, KeyError):
return repr(obj)
elif isinstance(obj, (Row, Reference)):
return str(obj['id'])
return str(long(obj))
@@ -2162,14 +2205,15 @@ class BaseAdapter(ConnectionPool):
new_rows = []
tmps = []
for colname in colnames:
if not REGEX_TABLE_DOT_FIELD.match(colname):
col_m = self.REGEX_TABLE_DOT_FIELD.match(colname)
if not col_m:
tmps.append(None)
else:
(tablename, _the_sep_, fieldname) = colname.partition('.')
tablename, fieldname = col_m.groups()
table = db[tablename]
field = table[fieldname]
ft = field.type
tmps.append((tablename,fieldname,table,field,ft))
tmps.append((tablename, fieldname, table, field, ft))
for (i,row) in enumerate(rows):
new_row = Row()
for (j,colname) in enumerate(colnames):
@@ -2177,9 +2221,8 @@ class BaseAdapter(ConnectionPool):
tmp = tmps[j]
if tmp:
(tablename,fieldname,table,field,ft) = tmp
if tablename in new_row:
colset = new_row[tablename]
else:
colset = new_row.get(tablename, None)
if colset is None:
colset = new_row[tablename] = Row()
if tablename not in virtualtables:
virtualtables.append(tablename)
@@ -2287,6 +2330,14 @@ class BaseAdapter(ConnectionPool):
return Expression(self.db,'CASE WHEN %s THEN %s ELSE %s END' % \
(self.expand(query),represent(t),represent(f)))
def sqlsafe_table(self, tablename, ot=None):
if ot is not None:
return ('%s AS ' + self.QUOTE_TEMPLATE) % (ot, tablename)
return self.QUOTE_TEMPLATE % tablename
def sqlsafe_field(self, fieldname):
return self.QUOTE_TEMPLATE % fieldname
###################################################################################
# List of all the available adapters; they all extend BaseAdapter.
###################################################################################
@@ -2564,6 +2615,7 @@ class MySQLAdapter(BaseAdapter):
'list:reference': 'LONGTEXT',
'big-id': 'BIGINT AUTO_INCREMENT NOT NULL',
'big-reference': 'BIGINT, INDEX %(index_name)s (%(field_name)s), FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
'reference FK': ', CONSTRAINT `FK_%(constraint_name)s` FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
}
QUOTE_TEMPLATE = "`%s`"
@@ -2590,12 +2642,12 @@ class MySQLAdapter(BaseAdapter):
def _drop(self,table,mode):
# breaks db integrity but without this mysql does not drop table
table_rname = table._rname or table
table_rname = table.sqlsafe
return ['SET FOREIGN_KEY_CHECKS=0;','DROP TABLE %s;' % table_rname,
'SET FOREIGN_KEY_CHECKS=1;']
def _insert_empty(self, table):
return 'INSERT INTO %s VALUES (DEFAULT);' % table
return 'INSERT INTO %s VALUES (DEFAULT);' % (table.sqlsafe)
def distributed_transaction_begin(self,key):
self.execute('XA START;')
@@ -2668,6 +2720,8 @@ class MySQLAdapter(BaseAdapter):
class PostgreSQLAdapter(BaseAdapter):
drivers = ('psycopg2','pg8000')
QUOTE_TEMPLATE = '"%s"'
support_distributed_transaction = True
types = {
'boolean': 'CHAR(1)',
@@ -2694,12 +2748,11 @@ class PostgreSQLAdapter(BaseAdapter):
'geography': 'GEOGRAPHY',
'big-id': 'BIGSERIAL PRIMARY KEY',
'big-reference': 'BIGINT REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
'reference FK': ', CONSTRAINT FK_%(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s',
'reference FK': ', CONSTRAINT "FK_%(constraint_name)s" FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
'reference TFK': ' CONSTRAINT "FK_%(foreign_table)s_PK" FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s',
}
QUOTE_TEMPLATE = '"%s"'
def varquote(self,name):
return varquote_aux(name,'"%s"')
@@ -2713,7 +2766,7 @@ class PostgreSQLAdapter(BaseAdapter):
return "'%s'" % str(obj).replace("'","''")
def sequence_name(self,table):
return '%s_id_seq' % table
return self.QUOTE_TEMPLATE % (table + '_id_seq')
def RANDOM(self):
return 'RANDOM()'
@@ -2802,8 +2855,8 @@ class PostgreSQLAdapter(BaseAdapter):
self.execute("SET standard_conforming_strings=on;")
self.try_json()
def lastrowid(self,table):
self.execute("""select currval('"%s"')""" % table._sequence_name)
def lastrowid(self,table = None):
self.execute("select lastval()")
return int(self.cursor.fetchone()[0])
def try_json(self):
@@ -2925,6 +2978,14 @@ class PostgreSQLAdapter(BaseAdapter):
"""
return 'ST_Within(%s,%s)' %(self.expand(first), self.expand(second, first.type))
def ST_DWITHIN(self, first, (second, third)):
"""
http://postgis.org/docs/ST_DWithin.html
"""
return 'ST_DWithin(%s,%s,%s)' %(self.expand(first),
self.expand(second, first.type),
self.expand(third, 'double'))
def represent(self, obj, fieldtype):
field_is_type = fieldtype.startswith
if field_is_type('geo'):
@@ -2942,6 +3003,11 @@ class PostgreSQLAdapter(BaseAdapter):
return value
return BaseAdapter.represent(self, obj, fieldtype)
def _drop(self, table, mode='restrict'):
if mode not in ['restrict', 'cascade', '']:
raise ValueError('Invalid mode: %s' % mode)
return ['DROP TABLE ' + table.sqlsafe + ' ' + str(mode) + ';']
class NewPostgreSQLAdapter(PostgreSQLAdapter):
drivers = ('psycopg2','pg8000')
@@ -3074,8 +3140,6 @@ class OracleAdapter(BaseAdapter):
'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s',
}
def sequence_name(self,tablename):
return '%s_sequence' % tablename
def trigger_name(self,tablename):
return '%s_trigger' % tablename
@@ -3091,7 +3155,7 @@ class OracleAdapter(BaseAdapter):
def _drop(self,table,mode):
sequence_name = table._sequence_name
return ['DROP TABLE %s %s;' % (table, mode), 'DROP SEQUENCE %s;' % sequence_name]
return ['DROP TABLE %s %s;' % (table.sqlsafe, mode), 'DROP SEQUENCE %s;' % sequence_name]
def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby):
if limitby:
@@ -3218,11 +3282,18 @@ class OracleAdapter(BaseAdapter):
else:
return self.cursor.fetchall()
def sqlsafe_table(self, tablename, ot=None):
if ot is not None:
return (self.QUOTE_TEMPLATE + ' ' \
+ self.QUOTE_TEMPLATE) % (ot, tablename)
return self.QUOTE_TEMPLATE % tablename
class MSSQLAdapter(BaseAdapter):
drivers = ('pyodbc',)
T_SEP = 'T'
QUOTE_TEMPLATE = "[%s]"
QUOTE_TEMPLATE = '"%s"'
types = {
'boolean': 'BIT',
@@ -3685,7 +3756,7 @@ class FireBirdAdapter(BaseAdapter):
}
def sequence_name(self,tablename):
return 'genid_%s' % tablename
return ('genid_' + self.QUOTE_TEMPLATE) % tablename
def trigger_name(self,tablename):
return 'trg_id_%s' % tablename
@@ -3714,7 +3785,7 @@ class FireBirdAdapter(BaseAdapter):
def _drop(self,table,mode):
sequence_name = table._sequence_name
return ['DROP TABLE %s %s;' % (table, mode), 'DROP GENERATOR %s;' % sequence_name]
return ['DROP TABLE %s %s;' % (table.sqlsafe, mode), 'DROP GENERATOR %s;' % sequence_name]
def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby):
if limitby:
@@ -4283,7 +4354,7 @@ class SAPDBAdapter(BaseAdapter):
}
def sequence_name(self,table):
return '%s_id_Seq' % table
return (self.QUOTE_TEMPLATE + '_id_Seq') % table
def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby):
if limitby:
@@ -4548,6 +4619,7 @@ class GoogleSQLAdapter(UseDatabaseStoredFile,MySQLAdapter):
class NoSQLAdapter(BaseAdapter):
can_select_for_update = False
QUOTE_TEMPLATE = '%s'
@staticmethod
def to_unicode(obj):
@@ -4737,6 +4809,8 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
uploads_in_blob = True
types = {}
# reconnect is not required for Datastore dbs
reconnect = lambda *args, **kwargs: None
def file_exists(self, filename): pass
def file_open(self, filename, mode='rb', lock=True): pass
@@ -4839,12 +4913,10 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
elif field_type.startswith('reference'):
if field.notnull:
attr = dict(required=True)
referenced = field_type[10:].strip()
ftype = self.types[field_type[:9]](referenced, **attr)
ftype = self.types[field_type[:9]](**attr)
elif field_type.startswith('list:reference'):
if field.notnull:
attr['required'] = True
referenced = field_type[15:].strip()
ftype = self.types[field_type[:14]](**attr)
elif field_type.startswith('list:'):
ftype = self.types[field_type](**attr)
@@ -5168,6 +5240,12 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
processor = attributes.get('processor',self.parse)
return processor(rows,fields,colnames,False)
def parse_list_integers(self, value, field_type):
return value[:] if self.use_ndb else value
def parse_list_strings(self, value, field_type):
return value[:] if self.use_ndb else value
def count(self,query,distinct=None,limit=None):
if distinct:
raise RuntimeError("COUNT DISTINCT not supported")
@@ -5442,8 +5520,8 @@ def cleanup(text):
"""
validates that the given text is clean: only contains [0-9a-zA-Z_]
"""
if not REGEX_ALPHANUMERIC.match(text):
raise SyntaxError('invalid table or field name: %s' % text)
#if not REGEX_ALPHANUMERIC.match(text):
# raise SyntaxError('invalid table or field name: %s' % text)
return text
class MongoDBAdapter(NoSQLAdapter):
@@ -5582,9 +5660,6 @@ class MongoDBAdapter(NoSQLAdapter):
value = obj
else:
value = NoSQLAdapter.represent(self, obj, fieldtype)
if isinstance(obj, (list, tuple)) and \
(not fieldtype == "json" or fieldtype.startswith('list:')):
return value
# reference types must be convert to ObjectID
if fieldtype =='date':
if value == None:
@@ -7245,12 +7320,32 @@ class Row(object):
__init__ = lambda self,*args,**kwargs: self.__dict__.update(*args,**kwargs)
def __getitem__(self, k):
if isinstance(k, Table):
try:
return ogetattr(self, k._tablename)
except (KeyError,AttributeError,TypeError):
pass
elif isinstance(k, Field):
try:
return ogetattr(self, k.name)
except (KeyError,AttributeError,TypeError):
pass
try:
return ogetattr(ogetattr(self, k.tablename), k.name)
except (KeyError,AttributeError,TypeError):
pass
key=str(k)
_extra = self.__dict__.get('_extra', None)
_extra = ogetattr(self, '__dict__').get('_extra', None)
if _extra is not None:
v = _extra.get(key, DEFAULT)
if v != DEFAULT:
return v
try:
return ogetattr(self, key)
except (KeyError,AttributeError,TypeError):
pass
m = REGEX_TABLE_DOT_FIELD.match(key)
if m:
try:
@@ -7652,7 +7747,7 @@ class DAL(object):
adapter_args=None, attempts=5, auto_import=False,
bigint_id=False, debug=False, lazy_tables=False,
db_uid=None, do_connect=True,
after_connection=None, tables=None):
after_connection=None, tables=None, ignore_field_case=True):
"""
Creates a new Database Abstraction Layer instance.
@@ -7737,6 +7832,7 @@ class DAL(object):
self._decode_credentials = decode_credentials
self._attempts = attempts
self._do_connect = do_connect
self._ignore_field_case = ignore_field_case
if not str(attempts).isdigit() or attempts < 0:
attempts = 5
@@ -7768,6 +7864,7 @@ class DAL(object):
# copy so multiple DAL() possible
self._adapter.types = copy.copy(types)
self._adapter.build_parsemap()
self._adapter.ignore_field_case = ignore_field_case
if bigint_id:
if 'big-id' in types and 'reference' in types:
self._adapter.types['id'] = types['big-id']
@@ -7810,11 +7907,11 @@ class DAL(object):
def import_table_definitions(self, path, migrate=False,
fake_migrate=False, tables=None):
pattern = pjoin(path,self._uri_hash+'_*.table')
if tables:
for table in tables:
self.define_table(**table)
else:
pattern = pjoin(path,self._uri_hash+'_*.table')
for filename in glob.glob(pattern):
tfile = self._adapter.file_open(filename, 'r')
try:
@@ -8565,13 +8662,8 @@ class Table(object):
self._tablename = tablename
self._ot = None # args.get('rname')
self._rname = args.get('rname')
if not self._rname:
self._sequence_name = args.get('sequence_name') or \
db and db._adapter.sequence_name(tablename)
else:
tb = self._rname[1:-1]
self._sequence_name = args.get('sequence_name') or \
db and db._adapter.sequence_name(tb)
self._sequence_name = args.get('sequence_name') or \
db and db._adapter.sequence_name(self._rname or tablename)
self._trigger_name = args.get('trigger_name') or \
db and db._adapter.trigger_name(tablename)
self._common_filter = args.get('common_filter')
@@ -8652,7 +8744,7 @@ class Table(object):
fields.append(Field(fn,'blob',default='',
writable=False,readable=False))
lower_fieldnames = set()
fieldnames_set = set()
reserved = dir(Table) + ['fields']
if (db and db.check_reserved):
check_reserved = db.check_reserved_keyword
@@ -8663,12 +8755,15 @@ class Table(object):
for field in fields:
field_name = field.name
check_reserved(field_name)
fn_lower = field_name.lower()
if fn_lower in lower_fieldnames:
if db and db._ignore_field_case:
fname_item = field_name.lower()
else:
fname_item = field_name
if fname_item in fieldnames_set:
raise SyntaxError("duplicate field %s in table %s" \
% (field_name, tablename))
else:
lower_fieldnames.add(fn_lower)
fieldnames_set.add(fname_item)
self.fields.append(field_name)
self[field_name] = field
@@ -8910,8 +9005,23 @@ class Table(object):
if 'Oracle' in str(type(self._db._adapter)):
return '%s %s' % (ot, self._tablename)
return '%s AS %s' % (ot, self._tablename)
return self._tablename
@property
def sqlsafe(self):
rname = self._rname
if rname: return rname
return self._db._adapter.sqlsafe_table(self._tablename)
@property
def sqlsafe_alias(self):
rname = self._rname
ot = self._ot
if rname and not ot: return rname
return self._db._adapter.sqlsafe_table(self._tablename, self._ot)
def _drop(self, mode = ''):
return self._db._adapter._drop(self, mode)
@@ -9566,6 +9676,10 @@ class Expression(object):
db = self.db
return Query(db, db._adapter.ST_WITHIN, self, value)
def st_dwithin(self, value, distance):
db = self.db
return Query(db, db._adapter.ST_DWITHIN, self, (value, distance))
# for use in both Query and sortby
@@ -9738,7 +9852,12 @@ class Field(Expression):
if not isinstance(fieldname, str) or hasattr(Table, fieldname) or \
fieldname[0] == '_' or REGEX_PYTHON_KEYWORDS.match(fieldname):
raise SyntaxError('Field: invalid field name: %s' % fieldname)
self.type = type if not isinstance(type, (Table,Field)) else 'reference %s' % type
if not isinstance(type, (Table,Field)):
self.type = type
else:
self.type = 'reference %s' % type
self.length = length if not length is None else DEFAULTLENGTH.get(self.type,512)
self.default = default if default!=DEFAULT else (update or None)
self.required = required # is this field required
@@ -10004,6 +10123,16 @@ class Field(Expression):
except:
return '<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):
@@ -10356,7 +10485,7 @@ class Set(object):
fields = table._listify(update_fields,update=True)
if not fields:
raise SyntaxError("No fields to update")
ret = db._adapter.update("%s" % table,self.query,fields)
ret = db._adapter.update("%s" % table._tablename,self.query,fields)
ret and [f(self,update_fields) for f in table._after_update]
return ret
@@ -10869,11 +10998,21 @@ class Rows(object):
represent = kwargs.get('represent', False)
writer = csv.writer(ofile, delimiter=delimiter,
quotechar=quotechar, quoting=quoting)
def unquote_colnames(colnames):
unq_colnames = []
for col in colnames:
m = self.db._adapter.REGEX_TABLE_DOT_FIELD.match(col)
if not m:
unq_colnames.append(col)
else:
unq_colnames.append('.'.join(m.groups()))
return unq_colnames
colnames = kwargs.get('colnames', self.colnames)
write_colnames = kwargs.get('write_colnames',True)
# a proper csv starting with the column names
if write_colnames:
writer.writerow(colnames)
writer.writerow(unquote_colnames(colnames))
def none_exception(value):
"""
@@ -10896,10 +11035,11 @@ class Rows(object):
for record in self:
row = []
for col in colnames:
if not REGEX_TABLE_DOT_FIELD.match(col):
m = self.db._adapter.REGEX_TABLE_DOT_FIELD.match(col)
if not m:
row.append(record._extra[col])
else:
(t, f) = col.split('.')
(t, f) = m.groups()
field = self.db[t][f]
if isinstance(record.get(t, None), (Row,dict)):
value = record[t][f]
+4 -1
View File
@@ -816,7 +816,10 @@ class Session(Storage):
# if on GAE tickets go also in DB
if settings.global_settings.web2py_runtime_gae:
request.tickets_db = db
table_migrate = (masterapp == request.application)
if masterapp == request.application:
table_migrate = migrate
else:
table_migrate = False
tname = tablename + '_' + masterapp
table = db.get(tname, None)
Field = db.Field
+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 URL, truncate_string, FIELDSET
from gluon.dal import DAL, Field, Table, Row, CALLABLETYPES, smart_query, \
bar_encode, Reference, REGEX_TABLE_DOT_FIELD, Expression, SQLCustomType
bar_encode, Reference, Expression, SQLCustomType
from gluon.storage import Storage
from gluon.utils import md5_hash
from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \
@@ -45,6 +45,9 @@ except ImportError:
table_field = re.compile('[\w_]+\.[\w_]+')
widget_class = re.compile('^\w*')
def add_class(a,b):
return a+' '+b if a else b
def represent(field, value, record):
f = field.represent
if not callable(f):
@@ -334,7 +337,7 @@ class RadioWidget(OptionsWidget):
attr = cls._attributes(field, {}, **attributes)
attr['_class'] = attr.get('_class', 'web2py_radiowidget')
attr['_class'] = add_class(attr.get('_class'), 'web2py_radiowidget')
requires = field.requires
if not isinstance(requires, (list, tuple)):
@@ -398,7 +401,9 @@ class CheckboxesWidget(OptionsWidget):
values = [str(value)]
attr = cls._attributes(field, {}, **attributes)
attr['_class'] = attr.get('_class', 'web2py_checkboxeswidget')
attr['_class'] = add_class(attr.get('_class'), 'web2py_checkboxeswidget')
label = attr.get('label',True)
requires = field.requires
if not isinstance(requires, (list, tuple)):
@@ -439,7 +444,8 @@ class CheckboxesWidget(OptionsWidget):
requires=attr.get('requires', None),
hideerror=True, _value=k,
value=r_value),
LABEL(v, _for='%s%s' % (field.name, k))))
LABEL(v, _for='%s%s' % (field.name, k))
if label else ''))
opts.append(child(tds))
if opts:
@@ -2893,7 +2899,7 @@ class SQLTABLE(TABLE):
if not sqlrows:
return
if not columns:
columns = sqlrows.colnames
columns = ['.'.join(sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD.match(c).groups()) for c in sqlrows.colnames]
if headers == 'fieldname:capitalize':
headers = {}
for c in columns:
@@ -3116,7 +3122,7 @@ class ExportClass(object):
for record in self.rows:
row = []
for col in self.rows.colnames:
if not REGEX_TABLE_DOT_FIELD.match(col):
if not self.rows.db._adapter.REGEX_TABLE_DOT_FIELD.match(col):
row.append(record._extra[col])
else:
(t, f) = col.split('.')
+135 -1
View File
@@ -79,6 +79,9 @@ def tearDownModule():
class TestFields(unittest.TestCase):
def testFieldName(self):
return
# Any table name is supported as long as underlying db does. The following code is ignored.
# Check that Fields cannot start with underscores
self.assertRaises(SyntaxError, Field, '_abc', 'string')
@@ -1394,6 +1397,137 @@ class TestRNameFields(unittest.TestCase):
db.person.drop()
class TestQuoting(unittest.TestCase):
# tests for complex table names
def testRun(self):
return
db = DAL(DEFAULT_URI, check_reserved=['all'])
t0 = db.define_table('A.table.with.dots and spaces',
Field('f', 'string'))
t1 = db.define_table('A.table',
Field('f_other', t0),
Field('words', 'text'))
blather = 'blah blah and so'
t0[0] = {'f': 'content'}
t1[0] = {'f_other': int(t0[1]['id']),
'words': blather}
r = db(t1['f_other']==t0.id).select()
self.assertEqual(r[0][db['A.table']].words, blather)
db.define_table('t0', Field('f0'))
db.define_table('t1', Field('f1'), Field('t0', db['t0']))
db.t0[0]=dict(f0=3)
db.t1[0]=dict(f1=3, t0=1)
rows=db(db.t0.id==db.t1.t0).select()
self.assertEqual(rows[0].t1.t0, rows[0].t0.id)
if DEFAULT_URI.startswith('mssql'):
#there's no drop cascade in mssql
t1.drop()
t0.drop()
else:
t0.drop('cascade')
t1.drop()
db.t1.drop()
db.t0.drop()
# tests for case sensitivity
def testCase(self):
return
db = DAL(DEFAULT_URI, check_reserved=['all'], ignore_field_case=False)
if DEFAULT_URI.startswith('mssql'):
#multiple cascade gotcha
for key in ['reference','reference FK']:
db._adapter.types[key]=db._adapter.types[key].replace(
'%(on_delete_action)s','NO ACTION')
# test table case
t0 = db.define_table('B',
Field('f', 'string'))
try:
t1 = db.define_table('b',
Field('B', t0),
Field('words', 'text'))
except Exception, e:
# An error is expected when database does not support case
# sensitive entity names.
if DEFAULT_URI.startswith('sqlite:'):
self.assertTrue(isinstance(e, db._adapter.driver.OperationalError))
return
raise e
blather = 'blah blah and so'
t0[0] = {'f': 'content'}
t1[0] = {'B': int(t0[1]['id']),
'words': blather}
r = db(db.B.id==db.b.B).select()
self.assertEqual(r[0].b.words, blather)
t1.drop()
t0.drop()
# test field case
try:
t0 = db.define_table('table is a test',
Field('a_a'),
Field('a_A'))
except Exception, e:
# some db does not support case sensitive field names mysql is one of them.
if DEFAULT_URI.startswith('mysql:'):
db.rollback()
return
raise e
t0[0] = dict(a_a = 'a_a', a_A='a_A')
self.assertEqual(t0[1].a_a, 'a_a')
self.assertEqual(t0[1].a_A, 'a_A')
t0.drop()
def testPKFK(self):
# test primary keys
db = DAL(DEFAULT_URI, check_reserved=['all'], ignore_field_case=False)
if DEFAULT_URI.startswith('mssql'):
#multiple cascade gotcha
for key in ['reference','reference FK']:
db._adapter.types[key]=db._adapter.types[key].replace(
'%(on_delete_action)s','NO ACTION')
# test table without surrogate key. Length must is limited to
# 100 because of MySQL limitations: it cannot handle more than
# 767 bytes in unique keys.
t0 = db.define_table('t0', Field('Code', length=100), primarykey=['Code'])
t2 = db.define_table('t2', Field('f'), Field('t0_Code', 'reference t0'))
t3 = db.define_table('t3', Field('f', length=100), Field('t0_Code', t0.Code), primarykey=['f'])
t4 = db.define_table('t4', Field('f', length=100), Field('t0', t0), primarykey=['f'])
try:
t5 = db.define_table('t5', Field('f', length=100), Field('t0', 'reference no_table_wrong_reference'), primarykey=['f'])
except Exception, e:
self.assertTrue(isinstance(e, KeyError))
if DEFAULT_URI.startswith('mssql'):
#there's no drop cascade in mssql
t3.drop()
t4.drop()
t2.drop()
t0.drop()
else:
t0.drop('cascade')
t2.drop()
t3.drop()
t4.drop()
if __name__ == '__main__':
unittest.main()
tearDownModule()
tearDownModule()
+3 -3
View File
@@ -2915,7 +2915,7 @@ class Auth(object):
formname='reset_password', dbio=False,
onvalidation=onvalidation,
hideerror=self.settings.hideerror):
user = table_user(**{userfield:form.vars.email})
user = table_user(**{userfield:form.vars.get(userfield)})
if not user:
session.flash = self.messages['invalid_%s' % userfield]
redirect(self.url(args=request.args),
@@ -5395,7 +5395,7 @@ class Wiki(object):
if (auth.user and
check_credentials(current.request, gae_login=False) and
not 'wiki_editor' in auth.user_groups.values() and
self.settings.groups is None):
self.settings.groups == auth.user_groups.values()):
group = db.auth_group(role='wiki_editor')
gid = group.id if group else db.auth_group.insert(
role='wiki_editor')
@@ -5523,7 +5523,7 @@ class Wiki(object):
url = URL(args=('_edit', slug))
return dict(content=A('Create page "%s"' % slug, _href=url, _class="btn"))
else:
html = page.html if not force_render else self.get_renderer(page)
html = page.html if not force_render else self.get_renderer()(page)
content = XML(self.fix_hostname(html))
return dict(title=page.title,
slug=page.slug,
+4 -1
View File
@@ -1224,7 +1224,10 @@ end tell
if not options.nobanner:
print 'please visit:'
print '\t', url
print 'use "kill -SIGTERM %i" to shutdown the web2py server' % os.getpid()
if sys.platform.startswith('win'):
print 'use "taskkill /f /pid %i" to shutdown the web2py server' % os.getpid()
else:
print 'use "kill -SIGTERM %i" to shutdown the web2py server' % os.getpid()
# enhance linecache.getline (used by debugger) to look at the source file
# if the line was not found (under py2exe & when file was modified)