diff --git a/Makefile b/Makefile index ee7ecd03..cc599250 100644 --- a/Makefile +++ b/Makefile @@ -62,8 +62,8 @@ app: echo 'did you uncomment import_all in gluon/main.py?' python2.5 -c 'import compileall; compileall.compile_dir("gluon/")' #python web2py.py -S welcome -R __exit__.py + #cd ../web2py_osx/site-packages/; unzip ../site-packages.zip find gluon -path '*.pyc' -exec cp {} ../web2py_osx/site-packages/{} \; - cd ../web2py_osx/site-packages/; unzip ../site-packages.zip cd ../web2py_osx/site-packages/; zip -r ../site-packages.zip * mv ../web2py_osx/site-packages.zip ../web2py_osx/web2py/web2py.app/Contents/Resources/lib/python2.5 cp README.markdown ../web2py_osx/web2py/web2py.app/Contents/Resources @@ -85,9 +85,9 @@ app: mv ../web2py_osx/web2py_osx.zip . win: echo 'did you uncomment import_all in gluon/main.py?' - python2.5 -c 'import compileall; compileall.compile_dir("gluon/")' + python2.7 -c 'import compileall; compileall.compile_dir("gluon/")' + #cd ../web2py_win/library/; unzip ../library.zip find gluon -path '*.pyc' -exec cp {} ../web2py_win/library/{} \; - cd ../web2py_win/library/; unzip ../library.zip cd ../web2py_win/library/; zip -r ../library.zip * mv ../web2py_win/library.zip ../web2py_win/web2py cp README.markdown ../web2py_win/web2py/ diff --git a/VERSION b/VERSION index 45616fdb..3159a75d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 17:37:48) dev +Version 2.00.0 (2012-07-27 09:32:24) dev diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 711f0330..341f1ddc 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -14,15 +14,24 @@ from gluon.fileutils import abspath, read_file, write_file from glob import glob import shutil import platform +try: + from git import * + have_git = True +except ImportError: + have_git = False + GIT_MISSING = 'requires python-git module, but not installed' + from gluon.languages import (regex_language, read_possible_languages, read_possible_plurals, lang_sampling, read_dict, write_dict, read_plural_dict, write_plural_dict) -if DEMO_MODE and request.function in ['change_password','pack','pack_plugin','upgrade_web2py','uninstall','cleanup','compile_app','remove_compiled_app','delete','delete_plugin','create_file','upload_file','update_languages','reload_routes']: + +if DEMO_MODE and request.function in ['change_password','pack','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']: session.flash = T('disabled in demo mode') redirect(URL('site')) + if not is_manager() and request.function in ['change_password','upgrade_web2py']: session.flash = T('disabled in multi user mode') redirect(URL('site')) @@ -188,6 +197,23 @@ def site(): msg = 'you must specify a name for the uploaded application' response.flash = T(msg) + elif (request.vars.appurl or '').endswith('.git') and request.vars.filename: + if not have_git: + session.flash = GIT_MISSING + elif request.vars.filename: + target = os.path.join(apath(r=request),request.vars.filename) + if os.path.exists(target): + session.flash = 'Application by that name already exists.' + else: + try: + new_repo = Repo.clone_from(request.vars.appurl,target) + session.flash = T('new application "%s" imported',request.vars.filename) + except GitCommandError, err: + session.flash = T('Invalid git repository specified.') + else: + session.flash = 'Application Name required for git import.' + redirect(URL(r=request)) + elif file_or_appurl and request.vars.filename: # fetch an application via URL or file upload f = None @@ -377,6 +403,32 @@ def delete(): redirect(URL(sender, anchor=request.vars.id2)) return dict(filename=filename, sender=sender) +def delete(): + """ Object delete handler """ + app = get_app() + filename = '/'.join(request.args) + sender = request.vars.sender + + if isinstance(sender, list): # ## fix a problem with Vista + sender = sender[0] + + dialog = FORM.dialog(T('Delete'), + {T('Cancel'):URL(sender, anchor=request.vars.id)}) + + if dialog.accepted: + try: + full_path = apath(filename, r=request) + lineno = count_lines(open(full_path,'r').read()) + os.unlink(full_path) + log_progress(app,'DELETE',filename,progress=-lineno) + session.flash = T('file "%(filename)s" deleted', + dict(filename=filename)) + except Exception: + session.flash = T('unable to delete file "%(filename)s"', + dict(filename=filename)) + redirect(URL(sender, anchor=request.vars.id2)) + return dict(dialog=dialog,filename=filename) + def enable(): app = get_app() filename = os.path.join(apath(app, r=request),'DISABLED') @@ -854,6 +906,11 @@ def design(): modules = modules=[x.replace('\\','/') for x in modules] modules.sort() + # Get all private files + privates = listdir(apath('%s/private/' % app, r=request), '[^\.#].*') + privates = [x.replace('\\','/') for x in privates] + privates.sort() + # Get all static files statics = listdir(apath('%s/static/' % app, r=request), '[^\.#].*') statics = [x.replace('\\','/') for x in statics] @@ -908,6 +965,7 @@ def design(): modules=filter_plugins(modules,plugins), extend=extend, include=include, + privates=filter_plugins(privates,plugins), statics=filter_plugins(statics,plugins), languages=languages, plurals=plurals, @@ -924,7 +982,7 @@ def delete_plugin(): redirect(URL('design', args=app, anchor=request.vars.id)) elif 'delete' in request.vars: try: - for folder in ['models','views','controllers','static','modules']: + for folder in ['models','views','controllers','static','modules', 'private']: path=os.path.join(apath(app,r=request),folder) for item in os.listdir(path): if item.rsplit('.',1)[0] == plugin_name: @@ -994,6 +1052,11 @@ def plugin(): modules = modules=[x.replace('\\','/') for x in modules] modules.sort() + # Get all private files + privates = listdir(apath('%s/private/' % app, r=request), '[^\.#].*') + privates = [x.replace('\\','/') for x in privates] + privates.sort() + # Get all static files statics = listdir(apath('%s/static/' % app, r=request), '[^\.#].*') statics = [x.replace('\\','/') for x in statics] @@ -1023,6 +1086,7 @@ def plugin(): modules=filter_plugins(modules), extend=extend, include=include, + privates=filter_plugins(privates), statics=filter_plugins(statics), languages=languages, crontab=crontab) @@ -1142,10 +1206,11 @@ def create_file(): # coding: utf8 from gluon import *\n""")[1:] - elif path[-8:] == '/static/': + elif (path[-8:] == '/static/') or (path[-9:] == '/private/'): if request.vars.plugin and not filename.startswith('plugin_%s/' % request.vars.plugin): filename = 'plugin_%s/%s' % (request.vars.plugin, filename) text = '' + else: redirect(request.vars.sender+anchor) @@ -1545,3 +1610,76 @@ def bulk_register(): redirect(URL('site')) return locals() +### Begin experimental stuff need fixes: +# 1) should run in its own process - cannot os.chdir +# 2) should not prompt user at console +# 3) should give option to force commit and not reuqire manual merge + +def git_pull(): + """ Git Pull handler """ + app = get_app() + if not have_git: + session.flash = GIT_MISSING + redirect(URL('site')) + if 'pull' in request.vars: + try: + repo = Repo(os.path.join(apath(r=request),app)) + origin = repo.remotes.origin + origin.fetch() + origin.pull() + session.flash = T("Application updated via git pull") + redirect(URL('site')) + except CheckoutError, message: + logging.error(message) + session.flash = T("Pull failed, certain files could not be checked out. Check logs for details.") + redirect(URL('site')) + except UnmergedEntriesError: + session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.") + redirect(URL('site')) + except AssertionError: + session.flash = T("Pull is not possible because you have unmerged files. Fix them up in the work tree, and then try again.") + redirect(URL('site')) + except GitCommandError, status: + logging.error(str(status)) + session.flash = T("Pull failed, git exited abnormally. See logs for details.") + redirect(URL('site')) + except Exception,e: + logging.error("Unexpected error:", sys.exc_info()[0]) + session.flash = T("Pull failed, git exited abnormally. See logs for details.") + redirect(URL('site')) + elif 'cancel' in request.vars: + redirect(URL('site')) + return dict(app=app) + + +def git_push(): + """ Git Push handler """ + app = get_app() + if not have_git: + session.flash = GIT_MISSING + redirect(URL('site')) + form = SQLFORM.factory(Field('changelog',requires=IS_NOT_EMPTY())) + form.element('input[type=submit]')['_value']=T('Push') + form.add_button(T('Cancel'),URL('site')) + form.process() + if form.accepted: + try: + repo = Repo(os.path.join(apath(r=request),app)) + index = repo.index + os.chdir(os.path.join(apath(r=request),app)) + index.add('*') + new_commit = index.commit(form.vars.changelog) + origin = repo.remotes.origin + origin.push() + session.flash = T("Git repo updated with latest application changes.") + redirect(URL('site')) + except UnmergedEntriesError: + session.flash = T("Push failed, there are unmerged entries in the cache. Resolve merge issues manually and try again.") + redirect(URL('site')) + except Exception, e: + logging.error("Unexpected error:", sys.exc_info()[0]) + session.flash = T("Push failed, git exited abnormally. See logs for details.") + redirect(URL('site')) + os.chdir(apath(r=request)) + return dict(app=app,form=form) + diff --git a/applications/admin/controllers/openshift.py b/applications/admin/controllers/openshift.py index 52889b5e..88945f74 100644 --- a/applications/admin/controllers/openshift.py +++ b/applications/admin/controllers/openshift.py @@ -34,7 +34,7 @@ def deploy(): assert repo.bare == False for i in form.vars.applications: - appsrc = os.path.join(os.getcwd(),'applications',i) + appsrc = os.path.join(apath(r=request),i) appdest = os.path.join(osrepo,'wsgi',osname,'applications',i) dir_util.copy_tree(appsrc,appdest) #shutil.copytree(appsrc,appdest) diff --git a/applications/admin/views/default/delete.html b/applications/admin/views/default/delete.html index 2e53ab7e..d0f86f44 100644 --- a/applications/admin/views/default/delete.html +++ b/applications/admin/views/default/delete.html @@ -4,6 +4,6 @@

{{=T('Are you sure you want to delete file "%s"?', filename)}}

-

{{=FORM(INPUT(_type='submit',_name='nodelete',_value=T('Abort')),INPUT(_type='hidden',_name='sender',_value=sender), _class="inline")}}{{=FORM(INPUT(_type='submit',_name='delete',_value=T('Delete')),INPUT(_type='hidden',_name='sender',_value=sender), _class="inline")}}

+

{{=dialog}}

diff --git a/applications/admin/views/default/design.html b/applications/admin/views/default/design.html index 3664edbf..e5b92ab8 100644 --- a/applications/admin/views/default/design.html +++ b/applications/admin/views/default/design.html @@ -57,6 +57,7 @@ def deletefile(arglist, vars={}): {{=button('#languages', T("languages"))}} {{=button('#static', T("static"))}} {{=button('#modules', T("modules"))}} + {{=button('#private', T("private files"))}} {{=button('#plugins', T("plugins"))}}

@@ -318,6 +319,57 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi {{=file_upload_form('%s/modules/' % app, 'modules')}} + + +

+ {{=T("Private files")}} + {{=helpicon()}} {{=T("These files are not served, they are only available from within your app")}} +

+
+
+
+ {{if not privates:}}

{{=T("There are no private files")}}

{{pass}} + +
{{=file_create_form('%s/private/' % app, 'private')}} + {{=file_upload_form('%s/private/' % app, 'private')}}
+
+ +

diff --git a/applications/admin/views/default/git_pull.html b/applications/admin/views/default/git_pull.html new file mode 100644 index 00000000..68623136 --- /dev/null +++ b/applications/admin/views/default/git_pull.html @@ -0,0 +1,9 @@ +{{extend 'layout.html'}} + +
+

{{=T('This will pull changes from the remote repo for application "%s"?', app)}}

+ + + +
{{=FORM(INPUT(_type='submit',_name='cancel',_value=T('Cancel')))}}{{=FORM(INPUT(_type='submit',_name='pull',_value=T('Pull')))}}
+
diff --git a/applications/admin/views/default/git_push.html b/applications/admin/views/default/git_push.html new file mode 100644 index 00000000..03db083f --- /dev/null +++ b/applications/admin/views/default/git_push.html @@ -0,0 +1,6 @@ +{{extend 'layout.html'}} + +

{{=T('This will push changes to the remote repo for application "%s".', app)}}

+
+{{=form}} +
diff --git a/applications/admin/views/default/site.html b/applications/admin/views/default/site.html index 6a54cb5a..fd809416 100644 --- a/applications/admin/views/default/site.html +++ b/applications/admin/views/default/site.html @@ -34,6 +34,10 @@ {{=button(URL('remove_compiled_app',args=a), T("Remove compiled"))}} {{pass}} {{pass}} + {{if os.path.exists(os.path.join(apath(r=request),a,'.git')): }} + {{=button(URL('git_pull',args=a), T("Git Pull"))}} + {{=button(URL('git_push',args=a), T("Git Push"))}} + {{pass}} {{if a!=request.application:}} {{=button(URL('uninstall',args=a), T("Uninstall"))}} {{=button_enable(URL('enable',args=a), a)}} @@ -117,16 +121,18 @@ - {{=LABEL(T("Get from URL:"), _for='upload_url')}} + {{=LABEL(T("Get from URL:"), _for='upload_url')}} - +
- - - + + ({{=T('can be a git repo')}}) + + + {{=LABEL(T("Overwrite installed app"), _for='upload_overwrite')}} @@ -142,13 +148,11 @@
-

{{=T("Deploy on Google App Engine")}}

-

{{=button(URL('gae','deploy'), T('Deploy'))}}

-

- -
-

{{=T("Deploy to OpenShift")}}

-

{{=button(URL('openshift','deploy'),T('Deploy'))}}

+

{{=T("Deploy")}}

+

+ {{=button(URL('gae','deploy'), T('Deploy on Google App Engine'))}} + {{=button(URL('openshift','deploy'),T('Deploy to OpenShift'))}} +


{{if TWITTER_HASH:}}
diff --git a/applications/welcome/languages/it.py b/applications/welcome/languages/it.py index 8039c9db..afa3109a 100644 --- a/applications/welcome/languages/it.py +++ b/applications/welcome/languages/it.py @@ -3,14 +3,18 @@ '!langcode!': 'it', '!langname!': 'Italiano', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" è un\'espressione opzionale come "campo1=\'nuovo valore\'". Non si può fare "update" o "delete" dei risultati di un JOIN ', -'%Y-%m-%d': '%d/%m/%Y', -'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', +'%d seconds ago': '%d seconds ago', '%s %%{row} deleted': '%s righe ("record") cancellate', '%s %%{row} updated': '%s righe ("record") modificate', '%s selected': '%s selezionato', +'%Y-%m-%d': '%d/%m/%Y', +'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', 'Administrative interface': 'Interfaccia amministrativa', +'appadmin is disabled because insecure channel': 'Amministrazione (appadmin) disabilitata: comunicazione non sicura', 'Available databases and tables': 'Database e tabelle disponibili', +'cache': 'cache', 'Cannot be empty': 'Non può essere vuoto', +'change password': 'Cambia password', 'Check to delete': 'Seleziona per cancellare', 'Client IP': 'Client IP', 'Controller': 'Controller', @@ -18,50 +22,79 @@ 'Current request': 'Richiesta (request) corrente', 'Current response': 'Risposta (response) corrente', 'Current session': 'Sessione (session) corrente', -'DB Model': 'Modello di DB', +'customize me!': 'Personalizzami!', +'data uploaded': 'dati caricati', 'Database': 'Database', +'database': 'database', +'database %s select': 'database %s select', +'db': 'db', +'DB Model': 'Modello di DB', 'Delete': 'Delete', 'Delete:': 'Cancella:', 'Description': 'Descrizione', +'design': 'progetta', 'Documentation': 'Documentazione', +'done!': 'fatto!', 'E-mail': 'E-mail', 'Edit': 'Modifica', -'Edit This App': 'Modifica questa applicazione', 'Edit current record': 'Modifica record corrente', +'edit profile': 'modifica profilo', +'Edit This App': 'Modifica questa applicazione', +'export as csv file': 'esporta come file CSV', 'First name': 'Nome', 'Group ID': 'ID Gruppo', +'hello': 'hello', +'hello world': 'salve mondo', 'Hello World': 'Salve Mondo', 'Hello World in a flash!': 'Salve Mondo in un flash!', 'Import/Export': 'Importa/Esporta', 'Index': 'Indice', +'insert new': 'inserisci nuovo', +'insert new %s': 'inserisci nuovo %s', 'Internal State': 'Stato interno', -'Invalid Query': 'Richiesta (query) non valida', 'Invalid email': 'Email non valida', +'Invalid Query': 'Richiesta (query) non valida', +'invalid request': 'richiesta non valida', 'Last name': 'Cognome', 'Layout': 'Layout', +'login': 'accesso', +'logout': 'uscita', +'lost password?': 'dimenticato la password?', 'Main Menu': 'Menu principale', 'Menu Model': 'Menu Modelli', 'Name': 'Nome', 'New Record': 'Nuovo elemento (record)', +'new record inserted': 'nuovo record inserito', +'next 100 rows': 'prossime 100 righe', 'No databases in this application': 'Nessun database presente in questa applicazione', +'not authorized': 'non autorizzato', 'Online examples': 'Vedere gli esempi', +'or import from csv file': 'oppure importa da file CSV', 'Origin': 'Origine', 'Password': 'Password', 'Powered by': 'Powered by', +'previous 100 rows': '100 righe precedenti', 'Query:': 'Richiesta (query):', +'record': 'record', +'record does not exist': 'il record non esiste', +'record id': 'record id', 'Record ID': 'Record ID', +'register': 'registrazione', 'Registration key': 'Chiave di Registazione', 'Reset Password key': 'Resetta chiave Password ', 'Role': 'Ruolo', 'Rows in table': 'Righe nella tabella', 'Rows selected': 'Righe selezionate', +'state': 'stato', 'Stylesheet': 'Foglio di stile (stylesheet)', 'Sure you want to delete this object?': 'Vuoi veramente cancellare questo oggetto?', +'table': 'tabella', 'Table name': 'Nome tabella', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'La richiesta (query) è una condizione come ad esempio "db.tabella1.campo1==\'valore\'". Una condizione come "db.tabella1.campo1==db.tabella2.campo2" produce un "JOIN" SQL.', 'The output of the file is a dictionary that was rendered by the view %s': 'L\'output del file è un "dictionary" che è stato visualizzato dalla vista %s', 'This is a copy of the scaffolding application': "Questa è una copia dell'applicazione di base (scaffold)", 'Timestamp': 'Ora (timestamp)', +'unable to parse csv file': 'non riesco a decodificare questo file CSV', 'Update': 'Update', 'Update:': 'Aggiorna:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Per costruire richieste (query) più complesse si usano (...)&(...) come "e" (AND), (...)|(...) come "o" (OR), e ~(...) come negazione (NOT).', @@ -73,36 +106,4 @@ 'You are successfully running web2py': 'Stai eseguendo web2py con successo', 'You can modify this application and adapt it to your needs': 'Puoi modificare questa applicazione adattandola alle tue necessità', 'You visited the url %s': "Hai visitato l'URL %s", -'appadmin is disabled because insecure channel': 'Amministrazione (appadmin) disabilitata: comunicazione non sicura', -'cache': 'cache', -'change password': 'Cambia password', -'customize me!': 'Personalizzami!', -'data uploaded': 'dati caricati', -'database': 'database', -'database %s select': 'database %s select', -'db': 'db', -'design': 'progetta', -'done!': 'fatto!', -'edit profile': 'modifica profilo', -'export as csv file': 'esporta come file CSV', -'hello': 'hello', -'hello world': 'salve mondo', -'insert new': 'inserisci nuovo', -'insert new %s': 'inserisci nuovo %s', -'invalid request': 'richiesta non valida', -'login': 'accesso', -'logout': 'uscita', -'lost password?': 'dimenticato la password?', -'new record inserted': 'nuovo record inserito', -'next 100 rows': 'prossime 100 righe', -'not authorized': 'non autorizzato', -'or import from csv file': 'oppure importa da file CSV', -'previous 100 rows': '100 righe precedenti', -'record': 'record', -'record does not exist': 'il record non esiste', -'record id': 'record id', -'register': 'registrazione', -'state': 'stato', -'table': 'tabella', -'unable to parse csv file': 'non riesco a decodificare questo file CSV', } diff --git a/applications/welcome/languages/ro.py b/applications/welcome/languages/ro.py index b48c1a5c..e2f0a31a 100644 --- a/applications/welcome/languages/ro.py +++ b/applications/welcome/languages/ro.py @@ -5,12 +5,16 @@ '!langname!': 'Română', '"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"update" (actualizează) este o expresie opțională precum "câmp1=\'valoare_nouă\'". Nu puteți actualiza sau șterge rezultatele unui JOIN', '%(nrows)s records found': '%(nrows)s înregistrări găsite', -'%Y-%m-%d': '%Y-%m-%d', -'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', +'%d days ago': '%d days ago', +'%d weeks ago': '%d weeks ago', '%s %%{row} deleted': '%s linii șterse', '%s %%{row} updated': '%s linii actualizate', '%s selected': '%s selectat(e)', +'%Y-%m-%d': '%Y-%m-%d', +'%Y-%m-%d %H:%M:%S': '%Y-%m-%d %H:%M:%S', '(something like "it-it")': '(ceva ce seamănă cu "it-it")', +'1 day ago': '1 day ago', +'1 week ago': '1 week ago', '<': '<', '<=': '<=', '=': '=', @@ -18,13 +22,15 @@ '>=': '>=', 'A new version of web2py is available': 'O nouă versiune de web2py este disponibilă', 'A new version of web2py is available: %s': 'O nouă versiune de web2py este disponibilă: %s', -'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENȚIE: Nu vă puteți conecta decât utilizând o conexiune securizată (HTTPS) sau rulând aplicația pe computerul local.', -'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENȚIE: Nu puteți efectua mai multe teste o dată deoarece lansarea în execuție a mai multor subpocese nu este sigură.', -'ATTENTION: you cannot edit the running application!': 'ATENȚIE: nu puteți edita o aplicație în curs de execuție!', 'About': 'Despre', +'about': 'despre', 'About application': 'Despre aplicație', 'Access Control': 'Control acces', 'Add': 'Adaugă', +'additional code for your application': 'cod suplimentar pentru aplicația dvs.', +'admin disabled because no admin password': 'administrare dezactivată deoarece parola de administrator nu a fost furnizată', +'admin disabled because not supported on google app engine': 'administrare dezactivată deoarece funcționalitatea nu e suportat pe Google App Engine', +'admin disabled because unable to access password file': 'administrare dezactivată deoarece nu există acces la fișierul cu parole', 'Admin is disabled because insecure channel': 'Adminstrarea este dezactivată deoarece conexiunea nu este sigură', 'Admin is disabled because unsecure channel': 'Administrarea este dezactivată deoarece conexiunea nu este securizată', 'Administration': 'Administrare', @@ -32,62 +38,118 @@ 'Administrator Password:': 'Parolă administrator:', 'Ajax Recipes': 'Rețete Ajax', 'And': 'Și', +'and rename it (required):': 'și renumiți (obligatoriu):', +'and rename it:': ' și renumiți:', +'appadmin': 'appadmin', +'appadmin is disabled because insecure channel': 'appadmin dezactivat deoarece conexiunea nu e sigură', +'application "%s" uninstalled': 'aplicația "%s" a fost dezinstalată', +'application compiled': 'aplicația a fost compilată', +'application is compiled and cannot be designed': 'aplicația este compilată și nu poate fi editată', 'Are you sure you want to delete file "%s"?': 'Sigur ștergeți fișierul "%s"?', 'Are you sure you want to delete this object?': 'Sigur ștergeți acest obiect?', 'Are you sure you want to uninstall application "%s"': 'Sigur dezinstalați aplicația "%s"', 'Are you sure you want to uninstall application "%s"?': 'Sigur dezinstalați aplicația "%s"?', +'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENȚIE: Nu vă puteți conecta decât utilizând o conexiune securizată (HTTPS) sau rulând aplicația pe computerul local.', +'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENȚIE: Nu puteți efectua mai multe teste o dată deoarece lansarea în execuție a mai multor subpocese nu este sigură.', +'ATTENTION: you cannot edit the running application!': 'ATENȚIE: nu puteți edita o aplicație în curs de execuție!', 'Authentication': 'Autentificare', 'Available databases and tables': 'Baze de date și tabele disponibile', 'Back': 'Înapoi', 'Buy this book': 'Cumpără această carte', +'cache': 'cache', 'Cache Keys': 'Chei cache', +'cache, errors and sessions cleaned': 'cache, erori și sesiuni golite', 'Cannot be empty': 'Nu poate fi vid', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Compilare imposibilă: aplicația conține erori. Debogați aplicația și încercați din nou.', +'cannot create file': 'fișier imposibil de creat', +'cannot upload file "%(filename)s"': 'imposibil de încărcat fișierul "%(filename)s"', 'Change Password': 'Schimbare parolă', 'Change password': 'Schimbare parolă', +'change password': 'schimbare parolă', +'check all': 'coșați tot', 'Check to delete': 'Coșați pentru a șterge', +'clean': 'golire', 'Clear': 'Golește', +'click to check for upgrades': 'Clic pentru a verifica dacă există upgrade-uri', 'Client IP': 'IP client', 'Community': 'Comunitate', +'compile': 'compilare', +'compiled application removed': 'aplicația compilată a fost ștearsă', 'Components and Plugins': 'Componente și plugin-uri', +'contains': 'conține', 'Controller': 'Controlor', 'Controllers': 'Controlori', +'controllers': 'controlori', 'Copyright': 'Drepturi de autor', +'create file with filename:': 'crează fișier cu numele:', 'Create new application': 'Creați aplicație nouă', +'create new application:': 'crează aplicație nouă:', +'crontab': 'crontab', 'Current request': 'Cerere curentă', 'Current response': 'Răspuns curent', 'Current session': 'Sesiune curentă', -'DB Model': 'Model bază de date', -'DESIGN': 'DESIGN', +'currently saved or': 'în prezent salvat sau', +'customize me!': 'Personalizează-mă!', +'data uploaded': 'date încărcate', 'Database': 'Baza de date', +'database': 'bază de date', +'database %s select': 'selectare bază de date %s', +'database administration': 'administrare bază de date', 'Date and Time': 'Data și ora', +'db': 'db', +'DB Model': 'Model bază de date', +'defines tables': 'definire tabele', 'Delete': 'Șterge', +'delete': 'șterge', +'delete all checked': 'șterge tot ce e coșat', 'Delete:': 'Șterge:', 'Demo': 'Demo', 'Deploy on Google App Engine': 'Instalare pe Google App Engine', 'Deployment Recipes': 'Rețete de instalare', 'Description': 'Descriere', +'design': 'design', +'DESIGN': 'DESIGN', 'Design for': 'Design pentru', 'Disk Cache Keys': 'Chei cache de disc', 'Documentation': 'Documentație', "Don't know what to do?": 'Nu știți ce să faceți?', +'done!': 'gata!', 'Download': 'Descărcare', 'E-mail': 'E-mail', 'E-mail invalid': 'E-mail invalid', +'edit': 'editare', 'EDIT': 'EDITARE', 'Edit': 'Editare', -'Edit Profile': 'Editare profil', -'Edit This App': 'Editați această aplicație', 'Edit application': 'Editare aplicație', +'edit controller': 'editare controlor', 'Edit current record': 'Editare înregistrare curentă', +'Edit Profile': 'Editare profil', +'edit profile': 'editare profil', +'Edit This App': 'Editați această aplicație', 'Editing file': 'Editare fișier', 'Editing file "%s"': 'Editare fișier "%s"', 'Email and SMS': 'E-mail și SMS', +'enter a number between %(min)g and %(max)g': 'introduceți un număr între %(min)g și %(max)g', +'enter an integer between %(min)g and %(max)g': 'introduceți un întreg între %(min)g și %(max)g', 'Error logs for "%(app)s"': 'Log erori pentru "%(app)s"', +'errors': 'erori', 'Errors': 'Erori', 'Export': 'Export', -'FAQ': 'Întrebări frecvente', +'export as csv file': 'exportă ca fișier csv', +'exposes': 'expune', +'extends': 'extinde', +'failed to reload module': 'reîncarcare modul nereușită', 'False': 'Neadevărat', +'FAQ': 'Întrebări frecvente', +'file "%(filename)s" created': 'fișier "%(filename)s" creat', +'file "%(filename)s" deleted': 'fișier "%(filename)s" șters', +'file "%(filename)s" uploaded': 'fișier "%(filename)s" încărcat', +'file "%(filename)s" was not deleted': 'fișierul "%(filename)s" n-a fost șters', +'file "%s" of %s restored': 'fișier "%s" de %s restaurat', +'file changed on disk': 'fișier modificat pe disc', +'file does not exist': 'fișier inexistent', +'file saved on %(time)s': 'fișier salvat %(time)s', +'file saved on %s': 'fișier salvat pe %s', 'First name': 'Prenume', 'Forbidden': 'Interzis', 'Forms and Validators': 'Formulare și validatori', @@ -98,19 +160,31 @@ 'Group uniquely assigned to user %(id)s': 'Grup asociat în mod unic utilizatorului %(id)s', 'Groups': 'Grupuri', 'Hello World': 'Salutare lume', +'help': 'ajutor', 'Home': 'Acasă', 'How did you get here?': 'Cum ați ajuns aici?', +'htmledit': 'editare html', 'Import/Export': 'Import/Export', +'includes': 'include', 'Index': 'Index', +'insert new': 'adaugă nou', +'insert new %s': 'adaugă nou %s', 'Installed applications': 'Aplicații instalate', +'internal error': 'eroare internă', 'Internal State': 'Stare internă', 'Introduction': 'Introducere', -'Invalid Query': 'Interogare invalidă', 'Invalid action': 'Acțiune invalidă', 'Invalid email': 'E-mail invalid', +'invalid password': 'parolă invalidă', 'Invalid password': 'Parolă invalidă', +'Invalid Query': 'Interogare invalidă', +'invalid request': 'cerere invalidă', +'invalid ticket': 'tichet invalid', +'language file "%(filename)s" created/updated': 'fișier de limbă "%(filename)s" creat/actualizat', 'Language files (static strings) updated': 'Fișierele de limbă (șirurile statice de caractere) actualizate', +'languages': 'limbi', 'Languages': 'Limbi', +'languages updated': 'limbi actualizate', 'Last name': 'Nume', 'Last saved on:': 'Ultima salvare:', 'Layout': 'Șablon', @@ -118,39 +192,54 @@ 'Layouts': 'Șabloane', 'License for': 'Licență pentru', 'Live Chat': 'Chat live', +'loading...': 'încarc...', 'Logged in': 'Logat', 'Logged out': 'Delogat', 'Login': 'Autentificare', +'login': 'autentificare', 'Login to the Administrative Interface': 'Logare interfață de administrare', +'logout': 'ieșire', 'Logout': 'Ieșire', 'Lost Password': 'Parolă pierdută', 'Lost password?': 'Parolă pierdută?', 'Main Menu': 'Meniu principal', 'Menu Model': 'Model meniu', +'merge': 'unește', 'Models': 'Modele', +'models': 'modele', 'Modules': 'Module', +'modules': 'module', 'My Sites': 'Site-urile mele', -'NO': 'NU', 'Name': 'Nume', 'New': 'Nou', -'New Record': 'Înregistrare nouă', +'new application "%s" created': 'aplicația nouă "%s" a fost creată', 'New password': 'Parola nouă', +'New Record': 'Înregistrare nouă', +'new record inserted': 'înregistrare nouă adăugată', +'next 100 rows': 'următoarele 100 de linii', +'NO': 'NU', 'No databases in this application': 'Aplicație fără bază de date', 'Object or table name': 'Obiect sau nume de tabel', 'Old password': 'Parola veche', 'Online examples': 'Exemple online', 'Or': 'Sau', +'or import from csv file': 'sau importă din fișier csv', +'or provide application url:': 'sau furnizează adresă url:', 'Origin': 'Origine', 'Original/Translation': 'Original/Traducere', 'Other Plugins': 'Alte plugin-uri', 'Other Recipes': 'Alte rețete', 'Overview': 'Prezentare de ansamblu', +'pack all': 'împachetează toate', +'pack compiled': 'pachet compilat', 'Password': 'Parola', "Password fields don't match": 'Câmpurile de parolă nu se potrivesc', 'Peeking at file': 'Vizualizare fișier', +'please input your password again': 'introduceți parola din nou', 'Plugins': 'Plugin-uri', 'Powered by': 'Pus în mișcare de', 'Preface': 'Prefață', +'previous 100 rows': '100 de linii anterioare', 'Profile': 'Profil', 'Python': 'Python', 'Query': 'Interogare', @@ -158,52 +247,88 @@ 'Quick Examples': 'Exemple rapide', 'RAM Cache Keys': 'Chei cache RAM', 'Recipes': 'Rețete', +'record': 'înregistrare', +'record does not exist': 'înregistrare inexistentă', +'record id': 'id înregistrare', 'Record ID': 'ID înregistrare', +'register': 'înregistrare', 'Register': 'Înregistrare', 'Registration identifier': 'Identificator de autentificare', 'Registration key': 'Cheie înregistrare', 'Registration successful': 'Autentificare reușită', 'Remember me (for 30 days)': 'Ține-mă minte (timp de 30 de zile)', +'remove compiled': 'șterge compilate', 'Request reset password': 'Cerere resetare parolă', 'Reset Password key': 'Cheie restare parolă', 'Resolve Conflict file': 'Fișier rezolvare conflict', +'restore': 'restaurare', +'revert': 'revenire', 'Role': 'Rol', 'Rows in table': 'Linii în tabel', 'Rows selected': 'Linii selectate', +'save': 'salvare', 'Save profile': 'Salvează profil', 'Saved file hash:': 'Hash fișier salvat:', 'Search': 'Căutare', 'Semantic': 'Semantică', 'Services': 'Servicii', +'session expired': 'sesiune expirată', +'shell': 'line de commandă', +'site': 'site', +'some files could not be removed': 'anumite fișiere n-au putut fi șterse', +'starts with': 'începe cu', +'state': 'stare', +'static': 'static', 'Static files': 'Fișiere statice', 'Stylesheet': 'Foaie de stiluri', 'Submit': 'Înregistrează', 'Support': 'Suport', 'Sure you want to delete this object?': 'Sigur ștergeți acest obiect?', +'table': 'tabel', 'Table name': 'Nume tabel', +'test': 'test', 'Testing application': 'Testare aplicație', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': '"Interogarea (query)" este o condiție de tipul "db.tabel1.câmp1==\'valoare\'". Ceva de genul "db.tabel1.câmp1==db.tabel2.câmp2" generează un JOIN SQL.', +'the application logic, each URL path is mapped in one exposed function in the controller': 'logica aplicației, fiecare rută URL este mapată într-o funcție expusă de controlor', 'The Core': 'Nucleul', -'The Views': 'Vederile', +'the data representation, define database tables and sets': 'reprezentarea datelor, definește tabelele bazei de date și seturile (de date)', 'The output of the file is a dictionary that was rendered by the view %s': 'Fișierul produce un dicționar care a fost prelucrat de vederea %s', +'the presentations layer, views are also known as templates': 'nivelul de prezentare, vederile sunt de asemenea numite și șabloane', +'The Views': 'Vederile', 'There are no controllers': 'Nu există controlori', 'There are no models': 'Nu există modele', 'There are no modules': 'Nu există module', 'There are no static files': 'Nu există fișiere statice', 'There are no translators, only default language is supported': 'Nu există traduceri, doar limba implicită este suportată', 'There are no views': 'Nu există vederi', +'these files are served without processing, your images go here': 'aceste fișiere sunt servite fără procesare, imaginea se plasează acolo', 'This App': 'Această aplicație', 'This is a copy of the scaffolding application': 'Aceasta este o copie a aplicației schelet', 'This is the %(filename)s template': 'Aceasta este șablonul fișierului %(filename)s', 'Ticket': 'Tichet', 'Timestamp': 'Moment în timp (timestamp)', +'to previous version.': 'la versiunea anterioară.', +'too short': 'prea scurt', +'translation strings for the application': 'șiruri de caractere folosite la traducerea aplicației', 'True': 'Adevărat', +'try': 'încearcă', +'try something like': 'încearcă ceva de genul', 'Twitter': 'Twitter', 'Unable to check for upgrades': 'Imposibil de verificat dacă există actualizări', +'unable to create application "%s"': 'imposibil de creat aplicația "%s"', +'unable to delete file "%(filename)s"': 'imposibil de șters fișierul "%(filename)s"', 'Unable to download': 'Imposibil de descărcat', 'Unable to download app': 'Imposibil de descărcat aplicația', +'unable to parse csv file': 'imposibil de analizat fișierul csv', +'unable to uninstall "%s"': 'imposibil de dezinstalat "%s"', +'uncheck all': 'decoșează tot', +'uninstall': 'dezinstalează', +'update': 'actualizează', +'update all languages': 'actualizează toate limbile', 'Update:': 'Actualizare:', +'upload application:': 'incarcă aplicația:', 'Upload existing application': 'Încarcă aplicația existentă', +'upload file:': 'încarcă fișier:', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Folosiți (...)&(...) pentru AND, (...)|(...) pentru OR, și ~(...) pentru NOT, pentru a crea interogări complexe.', 'User %(id)s Logged-in': 'Utilizator %(id)s autentificat', 'User %(id)s Logged-out': 'Utilizator %(id)s delogat', @@ -212,10 +337,16 @@ 'User %(id)s Profile updated': 'Profil utilizator %(id)s actualizat', 'User %(id)s Registered': 'Utilizator %(id)s înregistrat', 'User ID': 'ID utilizator', +'value already in database or empty': 'Valoare existentă în baza de date sau vidă', 'Verify Password': 'Verifică parola', +'versioning': 'versiuni', 'Videos': 'Video-uri', 'View': 'Vedere', +'view': 'vedere', 'Views': 'Vederi', +'views': 'vederi', +'web2py is up to date': 'web2py este la zi', +'web2py Recent Tweets': 'Ultimele tweet-uri web2py', 'Welcome': 'Bine ați venit', 'Welcome %s': 'Bine ați venit %s', 'Welcome to web2py': 'Bun venit la web2py', @@ -225,131 +356,4 @@ 'You are successfully running web2py': 'Rulați cu succes web2py', 'You can modify this application and adapt it to your needs': 'Puteți modifica și adapta aplicația nevoilor dvs.', 'You visited the url %s': 'Ați vizitat adresa %s', -'about': 'despre', -'additional code for your application': 'cod suplimentar pentru aplicația dvs.', -'admin disabled because no admin password': 'administrare dezactivată deoarece parola de administrator nu a fost furnizată', -'admin disabled because not supported on google app engine': 'administrare dezactivată deoarece funcționalitatea nu e suportat pe Google App Engine', -'admin disabled because unable to access password file': 'administrare dezactivată deoarece nu există acces la fișierul cu parole', -'and rename it (required):': 'și renumiți (obligatoriu):', -'and rename it:': ' și renumiți:', -'appadmin': 'appadmin', -'appadmin is disabled because insecure channel': 'appadmin dezactivat deoarece conexiunea nu e sigură', -'application "%s" uninstalled': 'aplicația "%s" a fost dezinstalată', -'application compiled': 'aplicația a fost compilată', -'application is compiled and cannot be designed': 'aplicația este compilată și nu poate fi editată', -'cache': 'cache', -'cache, errors and sessions cleaned': 'cache, erori și sesiuni golite', -'cannot create file': 'fișier imposibil de creat', -'cannot upload file "%(filename)s"': 'imposibil de încărcat fișierul "%(filename)s"', -'change password': 'schimbare parolă', -'check all': 'coșați tot', -'clean': 'golire', -'click to check for upgrades': 'Clic pentru a verifica dacă există upgrade-uri', -'compile': 'compilare', -'compiled application removed': 'aplicația compilată a fost ștearsă', -'contains': 'conține', -'controllers': 'controlori', -'create file with filename:': 'crează fișier cu numele:', -'create new application:': 'crează aplicație nouă:', -'crontab': 'crontab', -'currently saved or': 'în prezent salvat sau', -'customize me!': 'Personalizează-mă!', -'data uploaded': 'date încărcate', -'database': 'bază de date', -'database %s select': 'selectare bază de date %s', -'database administration': 'administrare bază de date', -'db': 'db', -'defines tables': 'definire tabele', -'delete': 'șterge', -'delete all checked': 'șterge tot ce e coșat', -'design': 'design', -'done!': 'gata!', -'edit': 'editare', -'edit controller': 'editare controlor', -'edit profile': 'editare profil', -'enter a number between %(min)g and %(max)g': 'introduceți un număr între %(min)g și %(max)g', -'enter an integer between %(min)g and %(max)g': 'introduceți un întreg între %(min)g și %(max)g', -'errors': 'erori', -'export as csv file': 'exportă ca fișier csv', -'exposes': 'expune', -'extends': 'extinde', -'failed to reload module': 'reîncarcare modul nereușită', -'file "%(filename)s" created': 'fișier "%(filename)s" creat', -'file "%(filename)s" deleted': 'fișier "%(filename)s" șters', -'file "%(filename)s" uploaded': 'fișier "%(filename)s" încărcat', -'file "%(filename)s" was not deleted': 'fișierul "%(filename)s" n-a fost șters', -'file "%s" of %s restored': 'fișier "%s" de %s restaurat', -'file changed on disk': 'fișier modificat pe disc', -'file does not exist': 'fișier inexistent', -'file saved on %(time)s': 'fișier salvat %(time)s', -'file saved on %s': 'fișier salvat pe %s', -'help': 'ajutor', -'htmledit': 'editare html', -'includes': 'include', -'insert new': 'adaugă nou', -'insert new %s': 'adaugă nou %s', -'internal error': 'eroare internă', -'invalid password': 'parolă invalidă', -'invalid request': 'cerere invalidă', -'invalid ticket': 'tichet invalid', -'language file "%(filename)s" created/updated': 'fișier de limbă "%(filename)s" creat/actualizat', -'languages': 'limbi', -'languages updated': 'limbi actualizate', -'loading...': 'încarc...', -'login': 'autentificare', -'logout': 'ieșire', -'merge': 'unește', -'models': 'modele', -'modules': 'module', -'new application "%s" created': 'aplicația nouă "%s" a fost creată', -'new record inserted': 'înregistrare nouă adăugată', -'next 100 rows': 'următoarele 100 de linii', -'or import from csv file': 'sau importă din fișier csv', -'or provide application url:': 'sau furnizează adresă url:', -'pack all': 'împachetează toate', -'pack compiled': 'pachet compilat', -'please input your password again': 'introduceți parola din nou', -'previous 100 rows': '100 de linii anterioare', -'record': 'înregistrare', -'record does not exist': 'înregistrare inexistentă', -'record id': 'id înregistrare', -'register': 'înregistrare', -'remove compiled': 'șterge compilate', -'restore': 'restaurare', -'revert': 'revenire', -'save': 'salvare', -'session expired': 'sesiune expirată', -'shell': 'line de commandă', -'site': 'site', -'some files could not be removed': 'anumite fișiere n-au putut fi șterse', -'starts with': 'începe cu', -'state': 'stare', -'static': 'static', -'table': 'tabel', -'test': 'test', -'the application logic, each URL path is mapped in one exposed function in the controller': 'logica aplicației, fiecare rută URL este mapată într-o funcție expusă de controlor', -'the data representation, define database tables and sets': 'reprezentarea datelor, definește tabelele bazei de date și seturile (de date)', -'the presentations layer, views are also known as templates': 'nivelul de prezentare, vederile sunt de asemenea numite și șabloane', -'these files are served without processing, your images go here': 'aceste fișiere sunt servite fără procesare, imaginea se plasează acolo', -'to previous version.': 'la versiunea anterioară.', -'too short': 'prea scurt', -'translation strings for the application': 'șiruri de caractere folosite la traducerea aplicației', -'try': 'încearcă', -'try something like': 'încearcă ceva de genul', -'unable to create application "%s"': 'imposibil de creat aplicația "%s"', -'unable to delete file "%(filename)s"': 'imposibil de șters fișierul "%(filename)s"', -'unable to parse csv file': 'imposibil de analizat fișierul csv', -'unable to uninstall "%s"': 'imposibil de dezinstalat "%s"', -'uncheck all': 'decoșează tot', -'uninstall': 'dezinstalează', -'update': 'actualizează', -'update all languages': 'actualizează toate limbile', -'upload application:': 'incarcă aplicația:', -'upload file:': 'încarcă fișier:', -'value already in database or empty': 'Valoare existentă în baza de date sau vidă', -'versioning': 'versiuni', -'view': 'vedere', -'views': 'vederi', -'web2py Recent Tweets': 'Ultimele tweet-uri web2py', -'web2py is up to date': 'web2py este la zi', } diff --git a/gluon/cache.py b/gluon/cache.py index 14006087..437e140e 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -35,7 +35,7 @@ except ImportError: logger = logging.getLogger("web2py.cache") -__all__ = ['Cache', 'lazy_lazy_cache'] +__all__ = ['Cache', 'lazy_cache'] DEFAULT_TIME_EXPIRE = 300 @@ -478,7 +478,14 @@ class Cache(object): return CacheAction(func,key,time_expire,self,cache_model) return tmp -def lazy_lazy_cache(key=None,time_expire=None,cache_model='ram'): +def lazy_cache(key=None,time_expire=None,cache_model='ram'): + """ + can be used to cache any function including in modules, + as long as the cached function is only called within a web2py request + if a key is not provided, one is generated from the function name + the time_expire defaults to None (no cache expiration) + if cache_model is "ram" then the model is current.cache.ram, etc. + """ def decorator(f,key=key,time_expire=time_expire,cache_model=cache_model): key = key or repr(f) def g(*c,**d): diff --git a/gluon/cfs.py b/gluon/cfs.py index 8dd69cc2..a99f9c06 100644 --- a/gluon/cfs.py +++ b/gluon/cfs.py @@ -33,7 +33,10 @@ def getcfs(key, filename, filter=None): This is used on Google App Engine since pyc files cannot be saved. """ - t = os.stat(filename).st_mtime + try: + t = os.stat(filename).st_mtime + except OSError: + return filter() cfs_lock.acquire() item = cfs.get(key, None) cfs_lock.release() diff --git a/gluon/contrib/spreadsheet.py b/gluon/contrib/spreadsheet.py index 09a00e34..e91dc733 100644 --- a/gluon/contrib/spreadsheet.py +++ b/gluon/contrib/spreadsheet.py @@ -855,9 +855,9 @@ 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"])) - + table.insert(0, TR(*sorted_headers, + **{_class:"%s_fieldnames" % \ + attributes["_class"]})) else: data = SCRIPT(""" // web2py Spreadsheets: no db data.""") diff --git a/gluon/dal.py b/gluon/dal.py index 42d357e1..50d52717 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -3785,6 +3785,9 @@ class GoogleSQLAdapter(UseDatabaseStoredFile,MySQLAdapter): self.execute("SET FOREIGN_KEY_CHECKS=1;") self.execute("SET sql_mode='NO_BACKSLASH_ESCAPES';") + def execute(self,a): + return self.log_execute(a.decode('utf8')) + class NoSQLAdapter(BaseAdapter): can_select_for_update = False @@ -7109,6 +7112,9 @@ class Table(dict): "primarykey must be a list of fields from table '%s'" \ % tablename self._primarykey = primarykey + if len(primarykey)==1: + self._id = [f for f in fields if isinstance(f,Field) \ + and f.name==primarykey[0]][0] elif not [f for f in fields if isinstance(f,Field) and f.type=='id']: field = Field('id', 'id') newfields.append(field) diff --git a/gluon/html.py b/gluon/html.py index aed30ae2..3776f67b 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -472,7 +472,18 @@ class XmlComponent(object): def xml(self): raise NotImplementedError - + def __mul__(self,n): + return CAT(*[self for i in range(n)]) + def __add__(self,other): + if isinstance(self,CAT): + components = self.components + else: + components = [self] + if isinstance(other,CAT): + components += other.components + else: + components += [other] + return CAT(*components) class XML(XmlComponent): """ @@ -2023,8 +2034,28 @@ class FORM(DIV): self.validate(**kwargs) return self + REDIRECT_JS = "window.location='%s';return false" + def add_button(self,value,url,_class=None): - self[0][-1][1].append(INPUT(_type="button",_value=value,_onclick="window.location='%s';return false" % url,_class=_class)) + self[0][-1][1].append(INPUT(_type="button",_value=value,_class=_class, + _onclick=self.REDIRECT_JS % url)) + + + @staticmethod + def dialog(text='OK',buttons=None,hidden=None): + if not buttons: buttons = {} + if not hidden: hidden={} + inputs = [INPUT(_type='button', + _value=name, + _onclick=FORM.REDIRECT_JS % link) \ + for name,link in buttons.items()] + inputs += [INPUT(_type='hidden', + _name=name, + _value=value) + for name,value in hidden.items()] + form = FORM(INPUT(_type='submit',_value=text),*inputs) + form.process() + return form class BEAUTIFY(DIV): diff --git a/gluon/main.py b/gluon/main.py index cff04b3c..a3779359 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -87,6 +87,7 @@ from settings import global_settings from validators import CRYPT from cache import Cache from html import URL as Url +from utils import is_valid_ip_address import newcron import rewrite @@ -402,6 +403,8 @@ def wsgibase(environ, responder): try: local_hosts.append(socket.gethostbyname(http_host)) except socket.gaierror: pass request.client = get_client(request.env) + if not is_valid_ip_address(request.client): + raise HTTP(400,"Bad Request") request.folder = abspath('applications', request.application) + os.sep x_req_with = str(request.env.http_x_requested_with).lower() diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 0c7cae23..f77e35e5 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -410,8 +410,11 @@ class CheckboxesWidget(OptionsWidget): LABEL(v,_for='%s%s' % (field.name,k)))) opts.append(child(tds)) + if opts: - opts[-1][0][0]['hideerror'] = False + opts.append(INPUT(_class="hidden", requires=attr.get('requires', None), + _disabled="disabled", _name=field.name, + hideerror=False)) return parent(*opts, **attr) @@ -1173,8 +1176,7 @@ class SQLFORM(FORM): row_id = '%s_%s%s' % (self.table, fieldname, SQLFORM.ID_ROW_SUFFIX) widget = field.widget(field, value) self.field_parent[row_id].components = [ widget ] - if not field.type.startswith('list:'): - self.field_parent[row_id]._traverse(False, hideerror) + self.field_parent[row_id]._traverse(False, hideerror) self.custom.widget[ fieldname ] = widget self.accepted = ret return ret @@ -1241,7 +1243,8 @@ class SQLFORM(FORM): ### do not know why this happens, it should not (source_file, original_filename) = \ (cStringIO.StringIO(f), 'file.txt') - newfilename = field.store(source_file, original_filename, field.uploadfolder) + newfilename = field.store(source_file, original_filename, + field.uploadfolder) # this line is for backward compatibility only self.vars['%s_newfilename' % fieldname] = newfilename fields[fieldname] = newfilename diff --git a/gluon/tools.py b/gluon/tools.py index 24467443..28e7129b 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -881,7 +881,8 @@ class Auth(object): return URL(args=current.request.args,vars=current.request.vars) def __init__(self, environment=None, db=None, mailer=True, - hmac_key=None, controller='default', function='user', cas_provider=None): + hmac_key=None, controller='default', function='user', + cas_provider=None, signature=True): """ auth=Auth(db) @@ -1133,7 +1134,10 @@ class Auth(object): # when user wants to be logged in for longer response.cookies[response.session_id_name]["expires"] = \ auth.expiration - + if signature: + self.define_signature() + else: + self.signature = None def _get_user_id(self): "accessor for auth.user_id" @@ -1332,7 +1336,8 @@ class Auth(object): db = self.db settings = self.settings - self.define_signature() + if not self.signature: + self.define_signature() if signature==True: signature_list = [self.signature] elif not signature: @@ -1622,7 +1627,7 @@ class Auth(object): userfield = 'email' passfield = self.settings.password_field user = self.db(table_user[userfield] == username).select().first() - if user: + if user and user.get(passfield,False): password = table_user[passfield].validate(password)[0] if not user.registration_key and password == user[passfield]: user = Storage(table_user._filter_fields(user, id=True)) @@ -3392,6 +3397,8 @@ class Crud(object): query = table.id > 0 if not fields: fields = [field for field in table if field.readable] + else: + fields = [table[f] if isinstance(f,str) else f for f in fields] rows = self.db(query).select(*fields,**dict(orderby=orderby, limitby=limitby)) return rows diff --git a/gluon/utils.py b/gluon/utils.py index d429b100..911182fb 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -16,6 +16,7 @@ import random import time import os import logging +import socket from contrib.pbkdf2 import pbkdf2_hex logger = logging.getLogger("web2py") @@ -69,7 +70,7 @@ def get_digest(value): elif value == "sha512": return hashlib.sha512 else: - raise ValueError("Invalid digest algorithm: %s" % value) + raise ValueError("Invalid digest algorithm: %s" % value) DIGEST_ALG_BY_SIZE = { 128/4: 'md5', @@ -146,6 +147,25 @@ def web2py_uuid(): bytes = ''.join(chr(c ^ ctokens[i]) for i,c in enumerate(bytes)) return str(uuid.UUID(bytes=bytes, version=4)) +def is_valid_ip_address(address): + """ + >>> is_valid_ip_address('127.0') + False + >>> is_valid_ip_address('127.0.0.1') + True + >>> is_valid_ip_address('2001:660::1') + True + """ + try: + if address.count('.')==3: + addr = socket.inet_aton(address) + else: + addr = socket.inet_pton(socket.AF_INET6, address) + except AttributeError: # no socket.inet_pton + return False + except socket.error: + return False + return True diff --git a/gluon/widget.py b/gluon/widget.py index 96ef1d6e..916f3495 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -220,44 +220,54 @@ class web2pyDialog(object): justify=Tkinter.LEFT).grid(row=0, column=0, sticky=sticky) - self.ip = Tkinter.Entry(self.root) - self.ip.insert(Tkinter.END, self.options.ip) - self.ip.grid(row=0, column=1, sticky=sticky) - + self.ips = {} + self.selected_ip = Tkinter.StringVar() + row=0 + ips = [('127.0.0.1','Local')] + \ + [(ip,'Public') for ip in options.ips] + \ + [('0.0.0.0','Public')] + for ip,legend in ips: + self.ips[ip] = Tkinter.Radiobutton( + self.root,text='%s (%s)' % (legend,ip), + variable=self.selected_ip, value=ip) + self.ips[ip].grid(row=row, column=1, sticky=sticky) + if row==0: self.ips[ip].select() + row+=1 + shift = row # Port Tkinter.Label(self.root, text='Server Port:', - justify=Tkinter.LEFT).grid(row=1, + justify=Tkinter.LEFT).grid(row=shift, column=0, sticky=sticky) self.port_number = Tkinter.Entry(self.root) self.port_number.insert(Tkinter.END, self.options.port) - self.port_number.grid(row=1, column=1, sticky=sticky) + self.port_number.grid(row=shift, column=1, sticky=sticky) # Password Tkinter.Label(self.root, text='Choose Password:', - justify=Tkinter.LEFT).grid(row=2, + justify=Tkinter.LEFT).grid(row=shift+1, column=0, sticky=sticky) self.password = Tkinter.Entry(self.root, show='*') self.password.bind('', lambda e: self.start()) self.password.focus_force() - self.password.grid(row=2, column=1, sticky=sticky) + self.password.grid(row=shift+1, column=1, sticky=sticky) # Prepare the canvas self.canvas = Tkinter.Canvas(self.root, width=300, height=100, bg='black') - self.canvas.grid(row=3, column=0, columnspan=2) + self.canvas.grid(row=shift+2, column=0, columnspan=2) self.canvas.after(1000, self.update_canvas) # Prepare the frame frame = Tkinter.Frame(self.root) - frame.grid(row=4, column=0, columnspan=2) + frame.grid(row=shift+3, column=0, columnspan=2) # Start button self.button_start = Tkinter.Button(frame, @@ -359,7 +369,7 @@ class web2pyDialog(object): if not password: self.error('no password, no web admin interface') - ip = self.ip.get() + ip = self.selected_ip.get() regexp = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' if ip and not re.compile(regexp).match(ip): @@ -416,7 +426,7 @@ class web2pyDialog(object): thread.start_new_thread(start_browser, (proto, ip, port)) self.password.configure(state='readonly') - self.ip.configure(state='readonly') + [ip.configure(state='disabled') for ip in self.ips.values()] self.port_number.configure(state='readonly') if self.tb: @@ -435,7 +445,7 @@ class web2pyDialog(object): self.button_start.configure(state='normal') self.button_stop.configure(state='disabled') self.password.configure(state='normal') - self.ip.configure(state='normal') + [ip.configure(state='normal') for ip in self.ips.values()] self.port_number.configure(state='normal') self.server.stop() @@ -790,6 +800,13 @@ def console(): global_settings.cmd_options = options global_settings.cmd_args = args + try: + options.ips = [ + ip for ip in socket.gethostbyname_ex(socket.getfqdn())[2] + if ip!='127.0.0.1'] + except socket.gaierror: + options.ips = [] + if options.run_system_tests: run_system_tests() diff --git a/gluon/winservice.py b/gluon/winservice.py index bbda61ca..78c28d1a 100644 --- a/gluon/winservice.py +++ b/gluon/winservice.py @@ -90,8 +90,12 @@ class Web2pyService(Service): cls = _winreg.QueryValue(h, 'PythonClass') finally: _winreg.CloseKey(h) - dir = os.path.dirname(cls) + dir = os.path.dirname(cls) os.chdir(dir) + from gluon.settings import global_settings + global_settings.gluon_parent = dir + from gluon.custom_import import custom_import_install + custom_import_install(dir) return True except: self.log("Can't change to web2py working path; server is stopped") @@ -149,8 +153,10 @@ class Web2pyService(Service): def web2py_windows_service_handler(argv=None, opt_file='options'): path = os.path.dirname(__file__) - classstring = os.path.normpath(os.path.join(up(path), - 'gluon.winservice.Web2pyService')) + web2py_path = up(path) + os.chdir(web2py_path) + classstring = os.path.normpath( + os.path.join(web2py_path,'gluon.winservice.Web2pyService')) if opt_file: Web2pyService._exe_args_ = opt_file win32serviceutil.HandleCommandLine(Web2pyService, @@ -158,7 +164,6 @@ def web2py_windows_service_handler(argv=None, opt_file='options'): win32serviceutil.HandleCommandLine(Web2pyService, serviceClassString=classstring, argv=argv) - if __name__ == '__main__': web2py_windows_service_handler()