diff --git a/VERSION b/VERSION index 518850f7..bee3b373 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.8.2-stable+timestamp.2013.11.28.07.51.37 +Version 2.8.2-stable+timestamp.2013.12.17.16.49.17 diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 221458de..2e616459 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -30,10 +30,18 @@ from gluon.languages import (read_possible_languages, read_dict, write_dict, read_plural_dict, write_plural_dict) -if DEMO_MODE and request.function in ['change_password', 'pack', 'pack_custom','pack_plugin', 'upgrade_web2py', 'uninstall', 'cleanup', 'compile_app', 'remove_compiled_app', 'delete', 'delete_plugin', 'create_file', 'upload_file', 'update_languages', 'reload_routes', 'git_push', 'git_pull']: +if DEMO_MODE and request.function in ['change_password', 'pack', +'pack_custom','pack_plugin', 'upgrade_web2py', 'uninstall', +'cleanup', 'compile_app', 'remove_compiled_app', 'delete', +'delete_plugin', 'create_file', 'upload_file', 'update_languages', +'reload_routes', 'git_push', 'git_pull', 'install_plugin']: session.flash = T('disabled in demo mode') redirect(URL('site')) +if is_gae and request.function in ('edit', 'edit_language', +'edit_plurals', 'update_languages', 'create_file', 'install_plugin'): + session.flash = T('disabled in GAE mode') + redirect(URL('site')) if not is_manager() and request.function in ['change_password', 'upgrade_web2py']: session.flash = T('disabled in multi user mode') @@ -63,10 +71,12 @@ def log_progress(app, mode='EDIT', filename=None, progress=0): def safe_open(a, b): - if DEMO_MODE and ('w' in b or 'a' in b): + if (DEMO_MODE or is_gae) and ('w' in b or 'a' in b): class tmp: def write(self, data): pass + def close(self): + pass return tmp() return open(a, b) @@ -750,6 +760,7 @@ def edit(): return response.json({'file_hash': file_hash, 'saved_on': saved_on, 'functions': functions, 'controller': controller, 'application': request.args[0], 'highlight': highlight}) else: file_details = dict(app=request.args[0], + lineno=request.vars.lineno or 1, editor_settings=preferences, filename=filename, realfilename=realfilename, @@ -773,6 +784,35 @@ def edit(): else: return response.json(file_details) +def todolist(): + """ Returns all TODO of the requested app + """ + app = request.vars.app or '' + app_path = apath('%(app)s' % {'app':app}, r=request) + dirs=['models', 'controllers', 'modules', 'private' ] + def listfiles(app, dir, regexp='.*\.py$'): + files = sorted( listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp)) + files = [x.replace(os.path.sep, '/') for x in files if not x.endswith('.bak')] + return files + + pattern = '#\s*(todo)+\s+(.*)' + regex = re.compile(pattern, re.IGNORECASE) + + output = [] + for d in dirs: + for f in listfiles(app, d): + matches = [] + filename= apath(os.path.join(app, d, f), r=request) + with open(filename, 'r') as f_s: + src = f_s.read() + for m in regex.finditer(src): + start = m.start() + lineno = src.count('\n', 0, start) + 1 + matches.append({'text':m.group(0), 'lineno':lineno}) + if len(matches) != 0: + output.append({'filename':f,'matches':matches, 'dir':d}) + + return {'todo':output, 'app': app} def resolve(): """ @@ -1053,11 +1093,12 @@ def design(): #Get crontab cronfolder = apath('%s/cron' % app, r=request) - if not os.path.exists(cronfolder): - os.mkdir(cronfolder) crontab = apath('%s/cron/crontab' % app, r=request) - if not os.path.exists(crontab): - safe_write(crontab, '#crontab') + if not is_gae: + if not os.path.exists(cronfolder): + os.mkdir(cronfolder) + if not os.path.exists(crontab): + safe_write(crontab, '#crontab') plugins = [] @@ -1209,7 +1250,6 @@ def plugin(): languages=languages, crontab=crontab) - def create_file(): """ Create files handler """ if request.vars and not request.vars.token == session.token: @@ -1220,6 +1260,8 @@ def create_file(): app = get_app(request.vars.app) path = abspath(request.vars.location) else: + if request.vars.dir: + request.vars.location += request.vars.dir + '/' app = get_app(name=request.vars.location.split('/')[0]) path = apath(request.vars.location, r=request) filename = re.sub('[^\w./-]+', '_', request.vars.filename) @@ -1347,7 +1389,11 @@ def create_file(): safe_write(full_filename, text) log_progress(app, 'CREATE', filename) - session.flash = T('file "%(filename)s" created', + if request.vars.dir: + result = T('file "%(filename)s" created', + dict(filename=full_filename[len(path):])) + else: + session.flash = T('file "%(filename)s" created', dict(filename=full_filename[len(path):])) vars = {} if request.vars.id: @@ -1356,13 +1402,51 @@ def create_file(): vars['app'] = request.vars.app redirect(URL('edit', args=[os.path.join(request.vars.location, filename)], vars=vars)) + except Exception, e: if not isinstance(e, HTTP): session.flash = T('cannot create file') - redirect(request.vars.sender + anchor) + if request.vars.dir: + response.flash = result + response.headers['web2py-component-content'] = 'append' + response.headers['web2py-component-command'] = """ + $.web2py.invalidate('#files_menu'); + load_file('%s'); + $.web2py.enableElement($('#form form').find($.web2py.formInputClickSelector)); + """ % URL('edit', args=[app,request.vars.dir,filename]) + return '' + else: + redirect(request.vars.sender + anchor) +def listfiles(app, dir, regexp='.*\.py$'): + files = sorted( + listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp)) + files = [x.replace('\\', '/') for x in files if not x.endswith('.bak')] + return files + +def editfile(path,file,vars={}, app = None): + args=(path,file) if 'app' in vars else (app,path,file) + url = URL('edit', args=args, vars=vars) + return A(file, _class='editor_filelink', _href=url, _style='word-wrap: nowrap;') + +def files_menu(): + app = request.vars.app or 'welcome' + dirs=[{'name':'models', 'reg':'.*\.py$'}, + {'name':'controllers', 'reg':'.*\.py$'}, + {'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'}, + {'name':'modules', 'reg':'.*\.py$'}, + {'name':'static', 'reg': '[^\.#].*'}] + result_files = [] + for dir in dirs: + result_files.append(TAG[''](LI(dir['name'], _class="nav-header component", _onclick="collapse('" + dir['name'] + "_files');"), + LI(UL(*[LI(editfile(dir['name'], f, dict(id=dir['name'] + f.replace('.','__')), app), _style="overflow:hidden", _id=dir['name']+"__"+f.replace('.','__')) + for f in listfiles(app, dir['name'], regexp=dir['reg'])], + _class="nav nav-list small-font"), + _id=dir['name'] + '_files', _style="display: none;"))) + return dict(result_files = result_files) + def upload_file(): """ File uploading handler """ if request.vars and not request.vars.token == session.token: @@ -1422,8 +1506,11 @@ def errors(): import hashlib app = get_app() - - method = request.args(1) or 'new' + if is_gae: + method = 'dbold' if ('old' in + (request.args(1) or '')) else 'dbnew' + else: + method = request.args(1) or 'new' db_ready = {} db_ready['status'] = get_ticket_storage(app) db_ready['errmessage'] = T( @@ -1490,32 +1577,30 @@ def errors(): for fn in tk_db(tk_table.id > 0).select(): try: error = pickle.loads(fn.ticket_data) - except AttributeError: + hash = hashlib.md5(error['traceback']).hexdigest() + + if hash in delete_hashes: + tk_db(tk_table.id == fn.id).delete() + tk_db.commit() + else: + try: + hash2error[hash]['count'] += 1 + except KeyError: + error_lines = error['traceback'].split("\n") + last_line = error_lines[-2] + error_causer = os.path.split(error['layer'])[1] + hash2error[hash] = dict(count=1, + pickel=error, causer=error_causer, + last_line=last_line, hash=hash, + ticket=fn.ticket_id) + except AttributeError, e: tk_db(tk_table.id == fn.id).delete() tk_db.commit() - hash = hashlib.md5(error['traceback']).hexdigest() - - if hash in delete_hashes: - tk_db(tk_table.id == fn.id).delete() - tk_db.commit() - else: - try: - hash2error['hash']['count'] += 1 - except KeyError: - error_lines = error['traceback'].split("\n") - last_line = error_lines[-2] - error_causer = os.path.split(error['layer'])[1] - hash2error[hash] = dict(count=1, pickel=error, - causer=error_causer, - last_line=last_line, - hash=hash, ticket=fn.ticket_id) - decorated = [(x['count'], x) for x in hash2error.values()] - decorated.sort(key=operator.itemgetter(0), reverse=True) - - return dict(errors=[x[1] for x in decorated], app=app, method=method) + return dict(errors=[x[1] for x in decorated], app=app, + method=method, db_ready=db_ready) elif method == 'dbold': tk_db, tk_table = get_ticket_storage(app) @@ -1523,16 +1608,18 @@ def errors(): if item[:7] == 'delete_': tk_db(tk_table.ticket_id == item[7:]).delete() tk_db.commit() - tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id, tk_table.created_datetime, orderby=~tk_table.created_datetime) + tickets_ = tk_db(tk_table.id > 0).select(tk_table.ticket_id, + tk_table.created_datetime, + orderby=~tk_table.created_datetime) tickets = [row.ticket_id for row in tickets_] - times = dict( - [(row.ticket_id, row.created_datetime) for row in tickets_]) - - return dict(app=app, tickets=tickets, method=method, times=times) + times = dict([(row.ticket_id, row.created_datetime) for + row in tickets_]) + return dict(app=app, tickets=tickets, method=method, + times=times, db_ready=db_ready) else: for item in request.vars: - # delete_all} rows doesn't contain any ticket + # delete_all rows doesn't contain any ticket # Remove anything else as requested if item[:7] == 'delete_' and (not item == "delete_all}"): os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request)) @@ -1552,6 +1639,9 @@ def get_ticket_storage(app): if os.path.exists(ticket_file): db_string = open(ticket_file).read() db_string = db_string.strip().replace('\r', '').replace('\n', '') + elif is_gae: + # use Datastore as fallback if there is no ticket_file + db_string = "google:datastore" else: return False tickets_table = 'web2py_ticket' @@ -1804,24 +1894,7 @@ def plugins(): "public/api.json/action/list/content/Package?package" + "_type=plugin&search_index=false").read() session.plugins = loads_json(rawlist) - plugins = TABLE( - *[TR(TD(H5(article["article"]["title"]), - A(T("Install"), - _href=URL(c="default", - f="install_plugin", - args=[app,], - vars={"source": - article["package_data"]["download"], - "plugin": article["article"]["title"]} - ))), - TD(article["article"]["description"], BR(), - A(T("Plugin page"), - _href="http://www.web2pyslices.com/slice/show/%s/" % \ - article["article"]["id"])), - TD(IMG(_src="http://www.web2pyslices.com/download/%s" % \ - article["article"]["thumbnail"]))) - for article in session.plugins["results"]]) - return dict(plugins=plugins, app=request.args(0)) + return dict(plugins=session.plugins["results"], app=request.args(0)) def install_plugin(): app = request.args(0) diff --git a/applications/admin/controllers/shell.py b/applications/admin/controllers/shell.py index 6d394861..3a835c7d 100644 --- a/applications/admin/controllers/shell.py +++ b/applications/admin/controllers/shell.py @@ -3,6 +3,7 @@ import cStringIO import gluon.contrib.shell import code import thread +import cgi from gluon.shell import env if DEMO_MODE or MULTI_USER_MODE: @@ -40,7 +41,7 @@ def callback(): k = len(session['commands:' + app]) - 1 #output = PRE(output) #return TABLE(TR('In[%i]:'%k,PRE(command)),TR('Out[%i]:'%k,output)) - return 'In [%i] : %s%s\n' % (k + 1, command, output) + return cgi.escape('In [%i] : %s%s\n' % (k + 1, command, output)) def reset(): diff --git a/applications/admin/languages/es.py b/applications/admin/languages/es.py index e064e408..d8c4c4ef 100644 --- a/applications/admin/languages/es.py +++ b/applications/admin/languages/es.py @@ -1,4 +1,4 @@ -# coding: utf8 +# -*- coding: utf-8 -*- { '!langcode!': 'es', '!langname!': 'Español', @@ -9,6 +9,7 @@ '%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(requires internet access, experimental)': '(requiere acceso a internet, experimental)', '(something like "it-it")': '(algo como "it-it")', +'(version %s)': '(version %s)', '@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(file **gluon/contrib/plural_rules/%s.py** is not found)', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', @@ -122,6 +123,7 @@ 'delete plugin': 'eliminar plugin', 'Delete this file (you will be asked to confirm deletion)': 'Elimine este fichero (se le pedirá confirmación)', 'Delete:': 'Elimine:', +'Demo': 'Demo', 'Deploy': 'Deploy', 'Deploy on Google App Engine': 'Instale en Google App Engine', 'Deploy to OpenShift': 'Instale en OpenShift', @@ -134,6 +136,7 @@ 'direction: ltr': 'direction: ltr', 'Disable': 'Deshabilitar', 'docs': 'docs', +'Docs': 'Docs', 'Done!': 'Done!', 'done!': 'listo!', 'Download': 'Descargar', @@ -157,6 +160,7 @@ 'Editing myemail': 'Editing myemail', 'Editing rbare': 'Editing rbare', 'Editing ul': 'Editing ul', +'Enable': 'Enable', 'Enterprise Web Framework': 'Armazón Empresarial para Internet', 'Error': 'Error', 'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"', @@ -190,6 +194,8 @@ 'First name': 'Nombre', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Funciones sin doctests equivalen a pruebas [aceptadas].', +'Git Pull': 'Git Pull', +'Git Push': 'Git Push', 'Globals##debug': 'Globals', 'graph model': 'graph model', 'Group ID': 'ID de Grupo', @@ -213,6 +219,7 @@ 'internal error': 'error interno', 'Internal State': 'Estado Interno', 'Invalid action': 'Acción inválida', +'Invalid application name': 'Invalid application name', 'Invalid email': 'Correo inválido', 'invalid password': 'contraseña inválida', 'invalid password.': 'invalid password.', @@ -230,6 +237,7 @@ 'Last name': 'Apellido', 'Last saved on:': 'Guardado en:', 'License for': 'Licencia para', +'License:': 'License:', 'lists by ticket': 'lists by ticket', 'loading...': 'cargando...', 'locals': 'locals', @@ -250,6 +258,7 @@ 'new application "%s" created': 'nueva aplicación "%s" creada', 'New application wizard': 'Asistente para nueva aplicación', 'new plugin installed': 'nuevo plugin instalado', +'New plugin installed: %s': 'New plugin installed: %s', 'New plugin installed: web2py.plugin.attachment.w2p': 'New plugin installed: web2py.plugin.attachment.w2p', 'New plugin installed: web2py.plugin.dialog.w2p': 'New plugin installed: web2py.plugin.dialog.w2p', 'New plugin installed: web2py.plugin.math2py.w2p': 'New plugin installed: web2py.plugin.math2py.w2p', @@ -310,6 +319,8 @@ 'Removed Breakpoint on %s at line %s': 'Eliminado punto de ruptura en %s en la línea %s', 'Replace': 'Reemplazar', 'Replace All': 'Reemplazar todos', +'Repository (%s)': 'Repository (%s)', +'Repository: %s': 'Repository: %s', 'request': 'request', 'Resolve Conflict file': 'archivo Resolución de Conflicto', 'response': 'response', @@ -329,6 +340,8 @@ 'Save file: %s': 'Guardar: %s', 'Save via Ajax': 'Guardar via Ajax', 'Saved file hash:': 'Hash del archivo guardado:', +'Screenshot %s': 'Screenshot %s', +'Screenshots': 'Screenshots', 'selected': 'seleccionado(s)', 'session': 'session', 'session expired': 'sesión expirada', @@ -336,6 +349,7 @@ 'shell': 'shell', 'Site': 'sitio', 'some files could not be removed': 'algunos archivos no pudieron ser removidos', +'source : filesystem': 'source : filesystem', 'Start searching': 'Iniciar búsqueda', 'Start wizard': 'Iniciar asistente', 'state': 'estado', @@ -355,8 +369,8 @@ 'test': 'probar', 'Testing application': 'Probando aplicación', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La "consulta" es una condición como "db.tabla1.campo1==\'valor\'". Algo como "db.tabla1.campo1==db.tabla2.campo2" resulta en un JOIN SQL.', -'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', 'The application logic, each URL path is mapped in one exposed function in the controller': 'La lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', +'the application logic, each URL path is mapped in one exposed function in the controller': 'la lógica de la aplicación, cada ruta URL se mapea en una función expuesta en el controlador', 'the data representation, define database tables and sets': 'la representación de datos, define tablas y conjuntos de base de datos', 'The data representation, define database tables and sets': 'La representación de datos, define tablas y conjuntos de base de datos', 'The presentations layer, views are also known as templates': 'La capa de presentación, las vistas también son llamadas plantillas', diff --git a/applications/admin/models/access.py b/applications/admin/models/access.py index fee8d464..823ec438 100644 --- a/applications/admin/models/access.py +++ b/applications/admin/models/access.py @@ -144,7 +144,7 @@ if session.is_mobile == 'true': elif session.is_mobile == 'false': is_mobile = False else: - is_mobile = request.user_agent().is_mobile + is_mobile = request.user_agent().get('is_mobile',False) if DEMO_MODE: session.authorized = True diff --git a/applications/admin/models/buttons.py b/applications/admin/models/buttons.py index 41c8d7d2..67d9ec92 100644 --- a/applications/admin/models/buttons.py +++ b/applications/admin/models/buttons.py @@ -24,7 +24,7 @@ def button_enable(href, app): return A(label, _class='button btn', _id=id, callback=href, target=id) def sp_button(href, label): - if request.user_agent().is_mobile: + if request.user_agent().get('is_mobile'): ret = A_button(SPAN(label), _href=href) else: ret = A(SPAN(label), _class='button special btn btn-inverse', _href=href) @@ -37,4 +37,4 @@ def searchbox(elementid): return SPAN(LABEL(IMG(_id="search_start", _src=URL('static', 'images/search.png'), _alt=T('filter')), _class='icon', _for=elementid), ' ', INPUT(_id=elementid, _type='text', _size=12, _class="input-medium"), - _class="searchbox") \ No newline at end of file + _class="searchbox") diff --git a/applications/admin/static/css/web2py-codemirror.css b/applications/admin/static/css/web2py-codemirror.css index a11e8d88..39b5311b 100644 --- a/applications/admin/static/css/web2py-codemirror.css +++ b/applications/admin/static/css/web2py-codemirror.css @@ -1,7 +1,13 @@ +/* TODO rename this file as web2py-editor.css */ /* Fullscreen */ .CodeMirror-fullscreen { z-index: 1030; } +.CodeMirror { + border-top: 1px solid #ddd; + /*border-left: 1px solid #ddd;*/ + border-bottom: 1px solid #ddd; +} /* BREAKPOINTS */ @@ -36,3 +42,15 @@ /*.nav-tabs>li { min-width: 100px; }*/ + +#windows_divs > div { + position: fixed; + height: 30%; + left: 0; + background: white; + right: 0; + bottom: 41px; + z-index: 1030; + overflow: inherit; + border-top: 1px solid #ddd; +} diff --git a/applications/admin/static/js/ajax_editor.js b/applications/admin/static/js/ajax_editor.js index 2b355fe4..8a7916a4 100644 --- a/applications/admin/static/js/ajax_editor.js +++ b/applications/admin/static/js/ajax_editor.js @@ -248,7 +248,7 @@ function keepalive(url) { }); } -function load_file(url) { +function load_file(url, lineno) { $.ajax({ type: "GET", contentType: 'application/json', @@ -263,13 +263,13 @@ function load_file(url) { var tab_header = '
[a-zA-Z][_a-zA-Z\-\d]*)\])?)?$') regex_proto = re.compile(r'(?/=])(?P
\w+):(?P popup))?\s*$',re.S)
regex_media_level2=re.compile(r'^(?P img|IMG|left|right|center|video|audio|blockleft|blockright)(?:\s+(?P.+?)\]\]',re.S)
regex_link_level2=re.compile(r'^(?P' % url
elif u_url.endswith(('.mp4','.mpeg','.mov','.ogv')):
return '' % url
@@ -673,6 +675,9 @@ def protolinks_simple(proto, url):
return '
'%url
return proto+':'+url
+def email_simple(email):
+ return '%s' % (email, email)
+
def render(text,
extra={},
allowed={},
diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py
index 4d206036..0a224100 100755
--- a/gluon/contrib/shell.py
+++ b/gluon/contrib/shell.py
@@ -258,7 +258,7 @@ def run(history, statement, env={}):
if not name.startswith('__'):
try:
history.set_global(name, val)
- except TypeError, ex:
+ except (TypeError, cPickle.PicklingError), ex:
UNPICKLABLE_TYPES.append(type(val))
history.add_unpicklable(statement, new_globals.keys())
diff --git a/gluon/contrib/spreadsheet.py b/gluon/contrib/spreadsheet.py
index 1ff05e73..7cca9c3d 100644
--- a/gluon/contrib/spreadsheet.py
+++ b/gluon/contrib/spreadsheet.py
@@ -858,8 +858,8 @@ class Sheet:
sorted_headers = [TH(), ] + \
[TH(header[1]) for header in sorted(unsorted_headers)]
table.insert(0, TR(*sorted_headers,
- **{_class: "%s_fieldnames" %
- attributes["_class"]}))
+ **{'_class': "%s_fieldnames" %
+ attributes["_class"]}))
else:
data = SCRIPT(""" // web2py Spreadsheets: no db data.""")
diff --git a/gluon/dal.py b/gluon/dal.py
index 8dc63f7f..3d7b33d2 100644
--- a/gluon/dal.py
+++ b/gluon/dal.py
@@ -252,7 +252,8 @@ THREAD_LOCAL = threading.local()
REGEX_TYPE = re.compile('^([\w\_\:]+)')
REGEX_DBNAME = re.compile('^(\w+)(\:\w+)*')
REGEX_W = re.compile('^\w+$')
-REGEX_TABLE_DOT_FIELD = re.compile('^(\w+)\.(\w+)$')
+REGEX_TABLE_DOT_FIELD = re.compile('^(\w+)\.([^.]+)$')
+REGEX_NO_GREEDY_ENTITY_NAME = r'(.+?)'
REGEX_UPLOAD_PATTERN = re.compile('(?P
[\w\-]+)\.(?P