Merge branch 'master' into pydal-pip

Conflicts:
	gluon/__init__.py
	gluon/dal/_load.py
	gluon/dal/adapters/base.py
	gluon/dal/adapters/google.py
	gluon/dal/adapters/postgres.py
	gluon/dal/adapters/teradata.py
	gluon/dal/base.py
	gluon/dal/objects.py
	gluon/tests/__init__.py
	gluon/tests/test_dal_nosql.py
This commit is contained in:
gi0baro
2015-01-17 13:32:06 +01:00
55 changed files with 2715 additions and 1907 deletions
+18
View File
@@ -1,3 +1,21 @@
## 2.9.12
- Tornado HTTPS support, thanks Diego
- Modular DAL, thanks Giovanni
- Added coverage support, thanks Niphlod
- More tests, thanks Niphlod and Paolo Valleri
- Added support for show_if in readonly sqlform, thanks Paolo
- Improved scheduler, thanks Niphlod
- Email timeout support
- Made web2py's custom_import work with circular imports, thanks Jack Kuan
- Added Portuguese, Catalan, and Burmese translations
- Allow map_hyphen to work for application names, thanks Tim Nyborg
- New module appconfig.py, thanks Niphlod
- Added geospatial support to Teradata adaptor, thanks Andrew Willimott
- Many bug fixes
## 2.9.6 - 2.9.10 ## 2.9.6 - 2.9.10
- fixed support of GAE + SQL - fixed support of GAE + SQL
+2 -2
View File
@@ -37,7 +37,7 @@ update:
echo "remember that pymysql was tweaked" echo "remember that pymysql was tweaked"
src: src:
### Use semantic versioning ### Use semantic versioning
echo 'Version 2.10.0-beta+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION echo 'Version 2.9.12-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
### rm -f all junk files ### rm -f all junk files
make clean make clean
### clean up baisc apps ### clean up baisc apps
@@ -61,7 +61,7 @@ src:
### build web2py_src.zip ### build web2py_src.zip
echo '' > NEWINSTALL echo '' > NEWINSTALL
mv web2py_src.zip web2py_src_old.zip | echo 'no old' mv web2py_src.zip web2py_src_old.zip | echo 'no old'
cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/gluon/*.py web2py/gluon/contrib/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py cd ..; zip -r web2py/web2py_src.zip web2py/web2py.py web2py/anyserver.py web2py/gluon/*.py web2py/gluon/dal/* web2py/gluon/contrib/* web2py/extras/* web2py/handlers/* web2py/examples/* web2py/README.markdown web2py/LICENSE web2py/CHANGELOG web2py/NEWINSTALL web2py/VERSION web2py/MANIFEST.in web2py/scripts/*.sh web2py/scripts/*.py web2py/applications/admin web2py/applications/examples/ web2py/applications/welcome web2py/applications/__init__.py web2py/site-packages/__init__.py web2py/gluon/tests/*.sh web2py/gluon/tests/*.py
mdp: mdp:
make src make src
+3 -3
View File
@@ -15,13 +15,13 @@ Then edit ./app.yaml and replace "yourappname" with yourappname.
## Documentation (readthedocs.org) ## Documentation (readthedocs.org)
[![Docs Status](https://readthedocs.org/projects/web2py/badge/?version=latest)](http://web2py.rtfd.org/) [![Docs Status](https://readthedocs.org/projects/web2py/badge/?version=latest&style=flat-square)](http://web2py.rtfd.org/)
## Tests ## Tests
[![Build Status](https://travis-ci.org/web2py/web2py.png)](https://travis-ci.org/web2py/web2py) [![Build Status](https://img.shields.io/travis/web2py/web2py.svg?style=flat-square)](https://travis-ci.org/web2py/web2py)
[![Coverage Status](https://coveralls.io/repos/web2py/web2py/badge.png)](https://coveralls.io/r/web2py/web2py) [![Coverage Status](https://img.shields.io/coveralls/web2py/web2py.svg?style=flat-square)](https://coveralls.io/r/web2py/web2py)
## Installation Instructions ## Installation Instructions
+1 -1
View File
@@ -1 +1 @@
Version 2.10.0-beta+timestamp.2014.10.16.15.58.50 Version 2.9.12-stable+timestamp.2015.01.17.00.07.04
+1 -2
View File
@@ -72,8 +72,7 @@ def interact():
f_globals = {} f_globals = {}
for name, value in env['globals'].items(): for name, value in env['globals'].items():
if name not in gluon.html.__all__ and \ if name not in gluon.html.__all__ and \
name not in gluon.validators.__all__ and \ name not in gluon.validators.__all__:
name not in gluon.dal.__all__:
f_globals[name] = pydoc.text.repr(value) f_globals[name] = pydoc.text.repr(value)
else: else:
f_locals = {} f_locals = {}
+18 -19
View File
@@ -1278,7 +1278,7 @@ def create_file():
path = abspath(request.vars.location) path = abspath(request.vars.location)
else: else:
if request.vars.dir: if request.vars.dir:
request.vars.location += request.vars.dir + '/' request.vars.location += request.vars.dir + '/'
app = get_app(name=request.vars.location.split('/')[0]) app = get_app(name=request.vars.location.split('/')[0])
path = apath(request.vars.location, r=request) path = apath(request.vars.location, r=request)
filename = re.sub('[^\w./-]+', '_', request.vars.filename) filename = re.sub('[^\w./-]+', '_', request.vars.filename)
@@ -1434,36 +1434,36 @@ def create_file():
""" % URL('edit', args=[app,request.vars.dir,filename]) """ % URL('edit', args=[app,request.vars.dir,filename])
return '' return ''
else: else:
redirect(request.vars.sender + anchor) redirect(request.vars.sender + anchor)
def listfiles(app, dir, regexp='.*\.py$'): def listfiles(app, dir, regexp='.*\.py$'):
files = sorted( files = sorted(
listdir(apath('%(app)s/%(dir)s/' % {'app':app, 'dir':dir}, r=request), regexp)) 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')] files = [x.replace('\\', '/') for x in files if not x.endswith('.bak')]
return files return files
def editfile(path,file,vars={}, app = None): def editfile(path,file,vars={}, app = None):
args=(path,file) if 'app' in vars else (app,path,file) args=(path,file) if 'app' in vars else (app,path,file)
url = URL('edit', args=args, vars=vars) url = URL('edit', args=args, vars=vars)
return A(file, _class='editor_filelink', _href=url, _style='word-wrap: nowrap;') return A(file, _class='editor_filelink', _href=url, _style='word-wrap: nowrap;')
def files_menu(): def files_menu():
app = request.vars.app or 'welcome' app = request.vars.app or 'welcome'
dirs=[{'name':'models', 'reg':'.*\.py$'}, dirs=[{'name':'models', 'reg':'.*\.py$'},
{'name':'controllers', 'reg':'.*\.py$'}, {'name':'controllers', 'reg':'.*\.py$'},
{'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'}, {'name':'views', 'reg':'[\w/\-]+(\.\w+)+$'},
{'name':'modules', 'reg':'.*\.py$'}, {'name':'modules', 'reg':'.*\.py$'},
{'name':'static', 'reg': '[^\.#].*'}, {'name':'static', 'reg': '[^\.#].*'},
{'name':'private', 'reg':'.*\.py$'}] {'name':'private', 'reg':'.*\.py$'}]
result_files = [] result_files = []
for dir in dirs: for dir in dirs:
result_files.append(TAG[''](LI(dir['name'], _class="nav-header component", _onclick="collapse('" + dir['name'] + "_files');"), 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('.','__')) 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'])], for f in listfiles(app, dir['name'], regexp=dir['reg'])],
_class="nav nav-list small-font"), _class="nav nav-list small-font"),
_id=dir['name'] + '_files', _style="display: none;"))) _id=dir['name'] + '_files', _style="display: none;")))
return dict(result_files = result_files) return dict(result_files = result_files)
def upload_file(): def upload_file():
""" File uploading handler """ """ File uploading handler """
@@ -1941,4 +1941,3 @@ def install_plugin():
T('unable to install plugin "%s"', filename) T('unable to install plugin "%s"', filename)
redirect(URL(f="plugins", args=[app,])) redirect(URL(f="plugins", args=[app,]))
return dict(form=form, app=app, plugin=plugin, source=source) return dict(form=form, app=app, plugin=plugin, source=source)
+2 -2
View File
@@ -325,7 +325,7 @@
'unable to uninstall "%s"': 'impossibile disinstallare "%s"', 'unable to uninstall "%s"': 'impossibile disinstallare "%s"',
'unable to upgrade because "%s"': 'impossibile aggiornare perché "%s"', 'unable to upgrade because "%s"': 'impossibile aggiornare perché "%s"',
'uncheck all': 'smarca tutti', 'uncheck all': 'smarca tutti',
'Uninstall': 'disinstalla', 'Uninstall': 'Disinstalla',
'update': 'aggiorna', 'update': 'aggiorna',
'update all languages': 'aggiorna tutti i linguaggi', 'update all languages': 'aggiorna tutti i linguaggi',
'Update:': 'Aggiorna:', 'Update:': 'Aggiorna:',
@@ -348,7 +348,7 @@
'Versioning': 'Versioning', 'Versioning': 'Versioning',
'View': 'Vista', 'View': 'Vista',
'view': 'vista', 'view': 'vista',
'Views': 'viste', 'Views': 'Viste',
'views': 'viste', 'views': 'viste',
'Web Framework': 'Web Framework', 'Web Framework': 'Web Framework',
'web2py is up to date': 'web2py è aggiornato', 'web2py is up to date': 'web2py è aggiornato',
-2
View File
@@ -47,5 +47,3 @@ if 'adminLanguage' in request.cookies and not (request.cookies['adminLanguage']
#set static_version #set static_version
from gluon.settings import global_settings from gluon.settings import global_settings
response.static_version = global_settings.web2py_version.split('-')[0] response.static_version = global_settings.web2py_version.split('-')[0]
-1
View File
@@ -34,4 +34,3 @@ else:
URL(_a, 'default', f='logout'))) URL(_a, 'default', f='logout')))
response.menu.append((T('Debug'), False, response.menu.append((T('Debug'), False,
URL(_a, 'debug', 'interact'))) URL(_a, 'debug', 'interact')))
@@ -14,4 +14,3 @@ def stateWidget(field, value, data={'on-label':'Enabled', 'off-label':'Disabled'
jQuery(".make-bootstrap-switch input[name='%s']").parent().bootstrapSwitch(); jQuery(".make-bootstrap-switch input[name='%s']").parent().bootstrapSwitch();
""" % fieldName) """ % fieldName)
return DIV(div, script) return DIV(div, script)
+492
View File
@@ -0,0 +1,492 @@
# -*- coding: utf-8 -*-
{
'!langcode!': 'ca',
'!langname!': 'Català',
'"update" is an optional expression like "field1=\'newvalue\'". You cannot update or delete the results of a JOIN': '"actualizi" és una expressió opcional com "camp1=\'nou_valor\'". No es poden actualitzar o eliminar resultats de un JOIN',
'%(nrows)s records found': '%(nrows)s registres trobats',
'%s %%{position}': '%s %%{posició}',
'%s %%{row} deleted': '%s %%{fila} %%{eliminada}',
'%s %%{row} updated': '%s %%{fila} %%{actualitzada}',
'%s selected': '%s %%{seleccionat}',
'%Y-%m-%d': '%d/%m/%Y',
'%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S',
'(something like "it-it")': '(similar a "això-això")',
'@markmin\x01An error occured, please [[reload %s]] the page': 'Hi ha hagut un error, si us plau [[recarregui %s]] la pàgina',
'@markmin\x01Number of entries: **%s**': "Nombre d'entrades: **%s**",
'A new version of web2py is available': 'Hi ha una nova versió de wep2py disponible',
'A new version of web2py is available: %s': 'Hi ha una nova versió de wep2py disponible: %s',
'About': 'Sobre',
'about': 'sobre',
'About application': "Sobre l'aplicació",
'Access Control': "Control d'Accés",
'Add': 'Afegir',
'Add Record': 'Afegeix registre',
'additional code for your application': '`codi addicional per a la seva aplicació',
'admin disabled because no admin password': 'admin inhabilitat per falta de contrasenya',
'admin disabled because not supported on google app engine': 'admin inhabilitat, no és suportat en GAE',
'admin disabled because unable to access password file': 'admin inhabilitat, impossible accedir al fitxer con la contrasenya',
'Admin is disabled because insecure channel': 'Admin inhabilitat, el canal no és segur',
'Admin is disabled because unsecure channel': 'Admin inhabilitat, el canal no és segur',
'Administrative interface': 'Interfície administrativa',
'Administrative Interface': 'Interfície Administrativa',
'administrative interface': 'interfície administrativa',
'Administrator Password:': 'Contrasenya del Administrador:',
'Ajax Recipes': 'Receptes AJAX',
'An error occured, please %s the page': 'Hi ha hagut un error, per favor %s la pàgina',
'And': 'I',
'and rename it (required):': 'i renombri-la (requerit):',
'and rename it:': " i renombri'l:",
'appadmin': 'appadmin',
'appadmin is disabled because insecure channel': 'admin inhabilitat, el canal no és segur',
'application "%s" uninstalled': 'aplicació "%s" desinstal·lada',
'application compiled': 'aplicació compilada',
'application is compiled and cannot be designed': 'la aplicació està compilada i no pot ser modificada',
'Apply changes': 'Aplicar canvis',
'Appointment': 'Nomenament',
'Are you sure you want to delete file "%s"?': 'Està segur que vol eliminar el arxiu "%s"?',
'Are you sure you want to delete this object?': 'Està segur que vol esborrar aquest objecte?',
'Are you sure you want to uninstall application "%s"': '¿Està segur que vol desinstalar la aplicació "%s"',
'Are you sure you want to uninstall application "%s"?': '¿Està segur que vol desinstalar la aplicació "%s"?',
'at': 'a',
'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENCIÓ: Inici de sessió requereix una connexió segura (HTTPS) o localhost.',
'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENCION: NO EJECUTE VARIAS PRUEBAS SIMULTANEAMENTE, NO SON THREAD SAFE.',
'ATTENTION: you cannot edit the running application!': 'ATENCIO: no pot modificar la aplicació que està ejecutant-se!',
'Authentication': 'Autenticació',
'Authentication failed at client DB!': '¡La autenticació ha fallat en la BDD client!',
'Authentication failed at main DB!': '¡La autenticació ha fallat en la BDD principal!',
'Available Databases and Tables': 'Bases de dades i taules disponibles',
'Back': 'Endarrera',
'Buy this book': 'Compra aquest lllibre',
'Cache': 'Caché',
'cache': 'caché',
'Cache Cleared': 'Caché Netejada',
'Cache Keys': 'Claus de la Caché',
'cache, errors and sessions cleaned': 'caché, errors i sessions eliminats',
'Cannot be empty': 'No pot estar buit',
'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'No se pot compilar: hi ha errors en la seva aplicació. Depuri, corregeixi errors i torni a intentar-ho.',
'cannot upload file "%(filename)s"': 'no és possible pujar fitxer "%(filename)s"',
'Change Password': 'Canviï la Contrasenya',
'Change password': 'Canviï la contrasenya',
'change password': 'canviï la contrasenya',
'Changelog': 'Changelog',
'check all': 'marcar tots',
'Check to delete': 'Marqui per a eliminar',
'choose one': 'escolliu un',
'clean': 'neteja',
'Clear': 'Netejar',
'Clear CACHE?': 'Netejar Memòrica Cau?',
'Clear DISK': 'Netejar DISC',
'Clear RAM': 'Netejar RAM',
'Click on the link %(link)s to reset your password': "Cliqui en l'enllaç %(link)s per a reiniciar la seva contrasenya",
'click to check for upgrades': 'feu clic per buscar actualitzacions',
'client': 'cliente',
'Client IP': 'IP del Client',
'Close': 'Tancar',
'Comma-separated export including columns not shown; fields from other tables are exported as raw values for faster export': 'Comma-separated export including columns not shown; fields from other tables are exported as raw values for faster export',
'Comma-separated export of visible columns. Fields from other tables are exported as they appear on-screen but this may be slow for many rows': 'Comma-separated export of visible columns. Fields from other tables are exported as they appear on-screen but this may be slow for many rows',
'Community': 'Comunitat',
'compile': 'compilar',
'compiled application removed': 'aplicació compilada eliminada',
'Components and Plugins': 'Components i Plugins',
'contains': 'conté',
'Controller': 'Controlador',
'Controllers': 'Controladors',
'controllers': 'controladors',
'Copyright': 'Copyright',
'Correo electrónico invàlid': 'Correu electrònic invàlid',
'create file with filename:': 'crear el fitxer amb el nom:',
'Create new application': 'Crear una nova aplicació',
'create new application:': 'crear una nova aplicació:',
'Create New Page': 'Crear Pàgina Nova',
'Create Page from Slug': 'Create Page from Slug',
'Created By': 'Creat Per',
'Created On': 'Creat a',
'CSV': 'CSV',
'CSV (hidden cols)': 'CSV (columnas ocultes)',
'Current request': 'Sol·licitud en curs',
'Current response': 'Resposta en curs',
'Current session': 'Sessió en curs',
'currently saved or': 'actualment guardat o',
'customize me!': "¡Adapta'm!",
'data uploaded': 'dades pujades',
'Database': 'Base de dades',
'Database %s select': 'selecció a base de dades %s',
'database administration': 'administració de base de dades',
'Database Administration (appadmin)': 'Administració de Base de Dades (appadmin)',
'Date and Time': 'Data i Hora',
'DB': 'BDD',
'db': 'bdd',
'DB Model': 'Model BDD',
'defines tables': 'defineix taules',
'Delete': 'Eliminar',
'delete': 'eliminar',
'delete all checked': 'eliminar marcats',
'Delete:': 'Eliminar:',
'Demo': 'Demostració',
'Deploy on Google App Engine': 'Desplegament a Google App Engine',
'Deployment Recipes': 'Receptes de desplegament',
'Description': 'Descripció',
'design': 'diseny',
'DESIGN': 'DISENY',
'Design for': 'Diseny per a',
'detecting': 'detectant',
'DISK': 'DISC',
'Disk Cache Keys': 'Claus de Caché en Disc',
'Disk Cleared': 'Disc netejat',
'Documentation': 'Documentació',
"Don't know what to do?": 'No sap què fer?',
'done!': '¡fet!',
'Download': 'Descàrregues',
'E-mail': 'Correu electrònic',
'edit': 'editar',
'EDIT': 'EDITAR',
'Edit': 'Editar',
'Edit application': 'Editar aplicació',
'edit controller': 'editar controlador',
'Edit current record': 'Editar el registre actual',
'Edit Menu': 'Editar Menu',
'Edit Page': 'Editar Pàgina',
'Edit Page Media': 'Edit Page Media',
'Edit Profile': 'Editar Perfil',
'edit profile': 'editar perfil',
'Edit This App': 'Editi aquesta App',
'Editing file': 'Editant fitxer',
'Editing file "%s"': 'Editant fitxer "%s"',
'El fitxer ha de ser PDF': 'El fitxer ha de ser PDF',
'El fitxer ha de ser PDF o XML': 'El fitxer ha de ser PDF o XML',
'Email': 'Email',
'Email and SMS': 'Correu electrònic i SMS',
'Email sent': 'Correu electrònic enviat',
'End of impersonation': 'Fi de suplantació',
'enter a number between %(min)g and %(max)g': 'introdueixi un número entre %(min)g i %(max)g',
'Enter a valid email address': 'Entri una adreça email vàlida',
'enter a value': 'entri un valor',
'Enter a value': 'Entri un valor',
'Enter an integer between %(min)g and %(max)g': 'Entri un numero enter entre %(min)g i %(max)g',
'enter an integer between %(min)g and %(max)g': 'entri numero enter entre %(min)g i %(max)g',
'enter date and time as %(format)s': 'entri data i hora com %(format)s',
'Enter from %(min)g to %(max)g characters': 'Entri des de %(min)g a %(max)g caràcters',
'Enter valid filename': 'Entri nom de fitxer vàlid',
'Error logs for "%(app)s"': 'Bitàcora de errors a "%(app)s"',
'errors': 'errors',
'Errors': 'Errors',
'Errors in form, please check it out.': 'Hi ha errors en el formulari, per favor comprovi-ho.',
'export as csv file': 'exportar com fitxer CSV',
'Export:': 'Exportar:',
'exposes': 'exposa',
'extends': 'extén',
'failed to reload module': 'la recàrrega del mòdul ha fallat',
'FAQ': 'FAQ',
'file': 'fitxer',
'file "%(filename)s" created': 'fitxer "%(filename)s" creat',
'file "%(filename)s" deleted': 'fitxer "%(filename)s" eliminat',
'file "%(filename)s" uploaded': 'fitxer "%(filename)s" pujat',
'file "%(filename)s" was not deleted': 'fitxer "%(filename)s" no fou eliminat',
'file "%s" of %s restored': 'fitxer "%s" de %s restaurat',
'file ## download': 'file ',
'file changed on disk': 'fitxer modificat en el disco',
'file does not exist': 'fitxer no existeix',
'file saved on %(time)s': 'fitxer guardat a %(time)s',
'file saved on %s': 'fitxer guardat a %s',
'First name': 'Nom',
'Forgot username?': 'Ha oblidat el nom de usuari?',
'Forms and Validators': 'Formularis i validadors',
'Free Applications': 'Aplicacions Lliures',
'Functions with no doctests will result in [passed] tests.': 'Funcions sense doctests equivalen a pruebas [aceptades].',
'Group %(group_id)s created': 'Grupo %(group_id)s creat',
'Group ID': 'ID de Grup',
'Group uniquely assigned to user %(id)s': 'Grup assignat únicament al usuari %(id)s',
'Groups': 'Grups',
'Hello': 'Hola',
'Hello World': 'Hola Món',
'help': 'ajuda',
'Home': 'Inici',
'Hosted by': 'Hosted by',
'How did you get here?': 'Com has arribat aquí?',
'HTML': 'HTML',
'HTML export of visible columns': 'HTML export de columnes visibles',
'htmledit': 'htmledit',
'Impersonate': 'Suplantar',
'import': 'importar',
'Import/Export': 'Importar/Exportar',
'in': 'a',
'includes': 'inclou',
'Index': 'Índex',
'insert new': 'inserti nou',
'insert new %s': 'inserti nou %s',
'Installed applications': 'Aplicacions instalades',
'Insufficient privileges': 'Privilegis insuficients',
'internal error': 'error intern',
'Internal State': 'Estat Intern',
'Introduction': 'Introducció',
'Invalid action': 'Acció invàlida',
'Invalid email': 'Correo electrónico invàlid',
'invalid expression': 'expressió invàlida',
'Invalid login': 'Inici de sessió invàlida',
'invalid password': 'contrasenya invàlida',
'Invalid Query': 'Consulta invàlida',
'invalid request': 'sol·licitud invàlida',
'Invalid reset password': 'Reinici de contrasenya invàlid',
'invalid ticket': 'tiquet invàlid',
'Is Active': 'Està Actiu',
'Key': 'Clau',
'language file "%(filename)s" created/updated': 'fitxer de llenguatge "%(filename)s" creat/actualitzat',
'Language files (static strings) updated': 'Fitxers de llenguatge (cadenes estàtiques) actualitzats',
'languages': 'llenguatges',
'Languages': 'Llenguatges',
'languages updated': 'llenguatges actualitzats',
'Last name': 'Cognom',
'Last saved on:': 'Guardat a:',
'Layout': 'Diseny de pàgina',
'Layout Plugins': 'Plugins de disseny',
'Layouts': 'Dissenys de pàgines',
'License for': 'Llicència per a',
'Live Chat': 'Xat en viu',
'loading...': 'carregant...',
'Log In': 'Log In',
'Log Out': 'Log Out',
'Logged in': 'Sessió iniciada',
'Logged out': 'Sessió finalitzada',
'Login': 'Inici de sessió',
'login': 'inici de sessió',
'Login disabled by administrator': 'Inici de sessió inhabilitat pel administrador',
'Login to the Administrative Interface': 'Inici de sessió per a la Interfície Administrativa',
'logout': 'fi de sessió',
'Logout': 'Fi de sessió',
'Lost Password': 'Contrasenya perdida',
'Lost password?': 'Ha oblidat la contrasenya?',
'lost password?': '¿ha oblidat la contrasenya?',
'Main Menu': 'Menú principal',
'Manage %(action)s': 'Manage %(action)s',
'Manage Access Control': 'Manage Access Control',
'Manage Cache': 'Gestionar la Caché',
'Menu Model': 'Model "menu"',
'merge': 'combinar',
'Models': 'Models',
'models': 'models',
'Modified By': 'Modificat Per',
'Modified On': 'Modificat A',
'Modules': 'Mòduls',
'modules': 'mòduls',
'must be YYYY-MM-DD HH:MM:SS!': '¡debe ser DD/MM/YYYY HH:MM:SS!',
'must be YYYY-MM-DD!': '¡debe ser DD/MM/YYYY!',
'My Sites': 'Els Meus Llocs',
'Name': 'Nombre',
'New': 'Nuevo',
'New %(entity)s': 'Nou %(entity)s',
'new application "%s" created': 'nova aplicació "%s" creada',
'New password': 'Contrasenya nova',
'New Record': 'Registre nou',
'new record inserted': 'nou registre insertat',
'New Search': 'Cerca nova',
'next %s rows': 'següents %s files',
'next 100 rows': '100 files següents',
'NO': 'NO',
'No databases in this application': 'No hi ha bases de dades en esta aplicació',
'No records found': "No s'han trobat registres",
'Not authorized': 'No autoritzat',
'not in': 'no a',
'Object or table name': 'Nom del objecte o taula',
'Old password': 'Contrasenya anterior',
'Online examples': 'Ejemples en línia',
'Or': 'O',
'or import from csv file': 'o importar desde fitxer CSV',
'or provide application url:': 'o proveeix URL de la aplicació:',
'Origin': 'Origen',
'Original/Translation': 'Original/Traducció',
'Other Plugins': 'Altres Plugins',
'Other Recipes': 'Altres Receptes',
'Overview': 'Resum',
'pack all': 'empaquetar tot',
'pack compiled': 'empaquetar compilats',
'Password': 'Contrasenya',
'Password changed': 'Contrasenya cambiada',
"Password fields don't match": 'Els camps de contrasenya no coincideixen',
'Password reset': 'Reinici de contrasenya',
'Peeking at file': 'Visualitzant fitxer',
'Permission': 'Permís',
'Permissions': 'Permisos',
'Phone': 'Telèfon',
'please input your password again': 'si us plau, entri un altre cop la seva contrasenya',
'Plugins': 'Plugins',
'Powered by': 'Aquest lloc utilitza',
'Preface': 'Prefaci',
'Presentar Factures': 'Presentar Factures',
'Presentar factures': 'Presentar factures',
'previous %s rows': '%s files prèvies',
'previous 100 rows': '100 files anteriors',
'Profile': 'Perfil',
'Profile updated': 'Perfil actualitzat',
'pygraphviz library not found': 'pygraphviz library not found',
'Python': 'Python',
'Query Not Supported: %s': 'Consulta No Suportada: %s',
'Query:': 'Consulta:',
'Quick Examples': 'Exemple Ràpids',
'RAM': 'RAM',
'RAM Cache Keys': 'Claus de la Caché en RAM',
'Ram Cleared': 'Ram Netjeda',
'Recipes': 'Receptes',
'Record': 'Registre',
'Record %(id)s created': 'Registre %(id)s creat',
'Record Created': 'Registre Creat',
'record does not exist': 'el registre no existe',
'Record ID': 'ID de Registre',
'Record id': 'Id de registre',
'Ref APB': 'Ref APB',
'register': "registri's",
'Register': "Registri's",
'Registration identifier': 'Identificador de Registre',
'Registration key': 'Clau de registre',
'Registration successful': 'Registre amb èxit',
'reload': 'recarregar',
'Remember me (for 30 days)': "Recordi'm (durant 30 dies)",
'remove compiled': 'eliminar compilades',
'Request reset password': 'Sol·licitud de restabliment de contrasenya',
'Reset password': 'Reiniciar contrasenya',
'Reset Password key': 'Restaurar Clau de la Contrasenya',
'Resolve Conflict file': 'Resolgui el Conflicte de fitxer',
'restore': 'restaurar',
'Retrieve username': 'Recuperar nom de usuari',
'revert': 'revertir',
'Role': 'Rol',
'Roles': 'Rols',
'Rows in Table': 'Files a la taula',
'Rows selected': 'Files seleccionades',
'save': 'guardar',
'Save model as...': 'Save model as...',
'Saved file hash:': 'Hash del fitxer guardat:',
'Search': 'Buscar',
'Search Pages': 'Search Pages',
'Semantic': 'Semàntica',
'Services': 'Serveis',
'session expired': 'sessió expirada',
'shell': 'terminal',
'Sign Up': 'Sign Up',
'site': 'lloc',
'Size of cache:': 'Mida de la Caché:',
'Slug': 'Slug',
'some files could not be removed': 'algunos archivos no pudieron ser removidos',
'Spreadsheet-optimised export of tab-separated content including hidden columns. May be slow': 'Spreadsheet-optimised export of tab-separated content including hidden columns. May be slow',
'Spreadsheet-optimised export of tab-separated content, visible columns only. May be slow.': 'Spreadsheet-optimised export of tab-separated content, visible columns only. May be slow.',
'start': 'inici',
'Start building a new search': 'Start building a new search',
'starts with': 'comença per',
'state': 'estat',
'static': 'estàtics',
'Static files': 'Fitxers estàtics',
'Statistics': 'Estadístiques',
'Stylesheet': "Fulla d'estil",
'Submit': 'Enviar',
'submit': 'enviar',
'Success!': 'Correcte!',
'Support': 'Suport',
'Sure you want to delete this object?': '¿Està segur que vol eliminar aquest objecte?',
'Table': 'taula',
'Table name': 'Nom de la taula',
'test': 'provar',
'Testing application': 'Provant aplicació',
'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" és una condición com "db.tabla1.campo1==\'valor\'". Algo com "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ó, cada ruta URL es mapeja en una funció exposada en el controlador',
'The Core': 'El Nucli',
'the data representation, define database tables and sets': 'la representació de dades, defineix taules i conjunts de base de dades',
'The output of the file is a dictionary that was rendered by the view %s': 'El resultat de aquesta funció és un diccionari que és desplegat per la vista %s',
'the presentations layer, views are also known as templates': 'la capa de presentació, les vistes també són anomenades plantilles',
'The Views': 'Les Vistes',
'There are no controllers': 'No hi ha controladors',
'There are no models': 'No hi ha models',
'There are no modules': 'No hi ha mòduls',
'There are no static files': 'No hi ha fitxers estàtics',
'There are no translators, only default language is supported': 'No hi ha traductors, només el llenguatge per defecte és suportat',
'There are no views': 'No hi ha vistes',
'these files are served without processing, your images go here': 'aquests fitxers són servits sense processar, les seves imatges van aquí',
'This App': 'Aquesta Aplicació',
'This email already has an account': 'Aquest correu electrònic ja té un compte',
'This is a copy of the scaffolding application': 'Aquesta és una còpia de la aplicació de bastiment',
'This is the %(filename)s template': 'Aquesta és la plantilla %(filename)s',
'Ticket': 'Tiquet',
'Time in Cache (h:m:s)': 'Temps en Caché (h:m:s)',
'Timestamp': 'Marca de temps',
'Title': 'Títol',
'to previous version.': 'a la versió prèvia.',
'To emulate a breakpoint programatically, write:': 'Emular un punto de ruptura programàticament, escribir:',
'to use the debugger!': 'usar el depurador!',
'toggle breakpoint': 'alternar punt de ruptura',
'Toggle comment': 'Alternar comentari',
'Toggle Fullscreen': 'Alternar pantalla completa',
'too short': 'massa curt',
'Traceback': 'Traceback',
'translation strings for the application': 'cadenes de caracters de traducció per a la aplicació',
'try': 'intenti',
'try something like': 'intenti algo com',
'TSV (Excel compatible)': 'TSV (compatible Excel)',
'TSV (Excel compatible, hidden cols)': 'TSV (compatible Excel, columnes ocultes)',
'TSV (Spreadsheets)': 'TSV (Fulls de càlcul)',
'TSV (Spreadsheets, hidden cols)': 'TSV (Fulls de càlcul, columnes amagades)',
'Twitter': 'Twitter',
'Unable to check for upgrades': 'No és possible verificar la existencia de actualitzacions',
'unable to create application "%s"': 'no és possible crear la aplicació "%s"',
'unable to delete file "%(filename)s"': 'no és possible eliminar el fitxer "%(filename)s"',
'Unable to download': 'No és possible la descàrrega',
'Unable to download app': 'No és possible descarregar la aplicació',
'unable to parse csv file': 'no és possible analitzar el fitxer CSV',
'unable to uninstall "%s"': 'no és possible instalar "%s"',
'uncheck all': 'desmarcar tots',
'uninstall': 'desinstalar',
'unknown': 'desconocido',
'update': 'actualitzar',
'update all languages': 'actualitzar tots els llenguatges',
'Update:': 'Actualizi:',
'upload application:': 'pujar aplicació:',
'Upload existing application': 'Puji aquesta aplicació',
'upload file:': 'puji fitxer:',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, i ~(...) para NOT, para crear consultas més complexes.',
'User': 'Usuari',
'User %(id)s is impersonating %(other_id)s': 'El usuari %(id)s està suplantant %(other_id)s',
'User %(id)s Logged-in': 'El usuari %(id)s inicià la sessió',
'User %(id)s Logged-out': 'El usuari %(id)s finalitzà la sessió',
'User %(id)s Password changed': 'Contrasenya del usuari %(id)s canviada',
'User %(id)s Password reset': 'Contrasenya del usuari %(id)s reiniciada',
'User %(id)s Profile updated': 'Actualitzat el perfil del usuari %(id)s',
'User %(id)s Registered': 'Usuari %(id)s Registrat',
'User %(id)s Username retrieved': 'Se ha recuperat el nom de usuari del usuari %(id)s',
'User %(username)s Logged-in': 'El usuari %(username)s inicià la sessió',
"User '%(username)s' Logged-in": "El usuari '%(username)s' inicià la sessió",
"User '%(username)s' Logged-out": "El usuari '%(username)s' finalitzà la sessió",
'User Id': 'Id de Usuari',
'User ID': 'ID de Usuari',
'User Logged-out': 'El usuari finalitzà la sessió',
'Username': 'Nom de usuari',
'Username retrieve': 'Recuperar nom de usuari',
'Users': 'Usuaris',
'Value already in database or empty': 'El valor ya existeix en la base de dades o està buit',
'value already in database or empty': 'el valor ya existeix en la base de dades o està buit',
'value not allowed': 'valor no permès',
'Value not in database': 'El valor no està a la base de dades',
'value not in database': 'el valor no està a la base de dades',
'Verify Password': 'Verificar Contrasenya',
'Version': 'Versió',
'versioning': 'versions',
'Videos': 'Videos',
'View': 'Vista',
'view': 'vista',
'View %(entity)s': 'Veure %(entity)s',
'View Page': 'View Page',
'Views': 'Vistes',
'views': 'vistes',
'web2py is up to date': 'web2py està actualitzat',
'web2py Recent Tweets': 'Tweets Recents de web2py',
'Welcome': 'Benvingut',
'Welcome %s': 'Benvingut %s',
'Welcome to web2py': 'Benvingut a web2py',
'Welcome to web2py!': '¡Benvingut a web2py!',
'Which called the function %s located in the file %s': 'La qual va cridar la funció %s localitzada en el fitxer %s',
'Wiki Page': 'Wiki Page',
'Working...': 'Treballant ...',
'XML': 'XML',
'XML export of columns shown': 'XML export of columns shown',
'YES': 'SÍ',
'You are successfully running web2py': 'Vostè està executant web2py amb èxit',
'You can modify this application and adapt it to your needs': 'Vostè pot modificar aquesta aplicació i adaptar-la a les seves necessitats',
'You visited the url %s': 'Vostè va visitar la url %s',
'Your username is: %(username)s': 'El seu nom de usuari és: %(username)s',
}
+40 -7
View File
@@ -128,10 +128,10 @@ class CacheAbstract(object):
Auxiliary function called by `clear` to search and clear cache entries Auxiliary function called by `clear` to search and clear cache entries
""" """
r = re.compile(regex) r = re.compile(regex)
for key in storage: for key in storage.keys():
if r.match(str(key)): if r.match(str(key)):
del storage[key] del storage[key]
break return
class CacheInRam(CacheAbstract): class CacheInRam(CacheAbstract):
@@ -246,11 +246,11 @@ class CacheOnDisk(CacheAbstract):
""" """
Disk based cache Disk based cache
This is implemented as a shelve object and it is shared by multiple web2py This is implemented as a key value store where each key corresponds to a
processes (and threads) as long as they share the same filesystem. single file in disk which is replaced when the value changes.
Disk cache provides persistance when web2py is started/stopped but it slower Disk cache provides persistance when web2py is started/stopped but it is
than `CacheInRam` slower than `CacheInRam`
Values stored in disk cache must be pickable. Values stored in disk cache must be pickable.
""" """
@@ -259,8 +259,11 @@ class CacheOnDisk(CacheAbstract):
""" """
Implements a key based storage in disk. Implements a key based storage in disk.
""" """
def __init__(self, folder): def __init__(self, folder):
self.folder = folder self.folder = folder
self.key_filter_in = lambda key: key
self.key_filter_out = lambda key: key
# Check the best way to do atomic file replacement. # Check the best way to do atomic file replacement.
if sys.version_info >= (3, 3): if sys.version_info >= (3, 3):
self.replace = os.replace self.replace = os.replace
@@ -287,6 +290,25 @@ class CacheOnDisk(CacheAbstract):
# POSIX rename() is always atomic # POSIX rename() is always atomic
self.replace = os.rename self.replace = os.rename
# Make sure we use valid filenames.
if sys.platform == "win32":
import base64
def key_filter_in_windows(key):
"""
Windows doesn't allow \ / : * ? "< > | in filenames.
To go around this encode the keys with base32.
"""
return base64.b32encode(key)
def key_filter_out_windows(key):
"""
We need to decode the keys so regex based removal works.
"""
return base64.b32decode(key)
self.key_filter_in = key_filter_in_windows
self.key_filter_out = key_filter_out_windows
def __setitem__(self, key, value): def __setitem__(self, key, value):
tmp_name, tmp_path = tempfile.mkstemp(dir=self.folder) tmp_name, tmp_path = tempfile.mkstemp(dir=self.folder)
@@ -295,6 +317,7 @@ class CacheOnDisk(CacheAbstract):
pickle.dump((time.time(), value), tmp, pickle.HIGHEST_PROTOCOL) pickle.dump((time.time(), value), tmp, pickle.HIGHEST_PROTOCOL)
finally: finally:
tmp.close() tmp.close()
key = self.key_filter_in(key)
fullfilename = os.path.join(self.folder, recfile.generate(key)) fullfilename = os.path.join(self.folder, recfile.generate(key))
if not os.path.exists(os.path.dirname(fullfilename)): if not os.path.exists(os.path.dirname(fullfilename)):
os.makedirs(os.path.dirname(fullfilename)) os.makedirs(os.path.dirname(fullfilename))
@@ -302,24 +325,32 @@ class CacheOnDisk(CacheAbstract):
def __getitem__(self, key): def __getitem__(self, key):
key = self.key_filter_in(key)
if recfile.exists(key, path=self.folder): if recfile.exists(key, path=self.folder):
timestamp, value = pickle.load(recfile.open(key, 'rb', path=self.folder)) timestamp, value = pickle.load(recfile.open(key, 'rb', path=self.folder))
return value return value
else: else:
raise KeyError raise KeyError
def __contains__(self, key): def __contains__(self, key):
key = self.key_filter_in(key)
return recfile.exists(key, path=self.folder) return recfile.exists(key, path=self.folder)
def __delitem__(self, key): def __delitem__(self, key):
key = self.key_filter_in(key)
recfile.remove(key, path=self.folder) recfile.remove(key, path=self.folder)
def __iter__(self): def __iter__(self):
for dirpath, dirnames, filenames in os.walk(self.folder): for dirpath, dirnames, filenames in os.walk(self.folder):
for filename in filenames: for filename in filenames:
yield filename yield self.key_filter_out(filename)
def keys(self):
return list(self.__iter__())
def get(self, key, default=None): def get(self, key, default=None):
@@ -333,6 +364,7 @@ class CacheOnDisk(CacheAbstract):
for key in self: for key in self:
del self[key] del self[key]
def __init__(self, request=None, folder=None): def __init__(self, request=None, folder=None):
self.initialized = False self.initialized = False
self.request = request self.request = request
@@ -387,6 +419,7 @@ class CacheOnDisk(CacheAbstract):
return value return value
def clear(self, regex=None): def clear(self, regex=None):
self.initialize() self.initialize()
storage = self.storage storage = self.storage
+2 -2
View File
@@ -127,7 +127,7 @@ class mybuiltin(object):
def LOAD(c=None, f='index', args=None, vars=None, def LOAD(c=None, f='index', args=None, vars=None,
extension=None, target=None, ajax=False, ajax_trap=False, extension=None, target=None, ajax=False, ajax_trap=False,
url=None, user_signature=False, timeout=None, times=1, url=None, user_signature=False, timeout=None, times=1,
content='loading...', **attr): content='loading...', post_vars=Storage(), **attr):
""" LOADs a component into the action's document """ LOADs a component into the action's document
Args: Args:
@@ -202,7 +202,7 @@ def LOAD(c=None, f='index', args=None, vars=None,
other_request.args = List(args) other_request.args = List(args)
other_request.vars = vars other_request.vars = vars
other_request.get_vars = vars other_request.get_vars = vars
other_request.post_vars = Storage() other_request.post_vars = post_vars
other_response = Response() other_response = Response()
other_request.env.path_info = '/' + \ other_request.env.path_info = '/' + \
'/'.join([request.application, c, f] + '/'.join([request.application, c, f] +
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Read from configuration files easily without hurting performances
USAGE:
During development you can load a config file either in .ini or .json
format (by default app/private/appconfig.ini or app/private/appconfig.json)
The result is a dict holding the configured values. Passing reload=True
is meant only for development: in production, leave reload to False and all
values will be cached
from gluon.contrib.appconfig import AppConfig
myconfig = AppConfig(path_to_configfile, reload=False)
print myconfig['db']['uri']
The returned dict can walk with "dot notation" an arbitrarely nested dict
print myconfig.take('db.uri')
You can even pass a cast function, i.e.
print myconfig.take('auth.expiration', cast=int)
Once the value has been fetched (and casted) it won't change until the process
is restarted (or reload=True is passed).
"""
import thread
import os
from ConfigParser import SafeConfigParser
from gluon import current
from gluon.serializers import json_parser
locker = thread.allocate_lock()
def AppConfig(*args, **vars):
locker.acquire()
reload_ = vars.pop('reload', False)
try:
instance_name = 'AppConfig_' + current.request.application
if reload_ or not hasattr(AppConfig, instance_name):
setattr(AppConfig, instance_name, AppConfigLoader(*args, **vars))
return getattr(AppConfig, instance_name).settings
finally:
locker.release()
class AppConfigDict(dict):
"""
dict that has a .take() method to fetch nested values and puts
them into cache
"""
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.int_cache = {}
def take(self, path, cast=None):
parts = path.split('.')
if path in self.int_cache:
return self.int_cache[path]
value = self
walking = []
for part in parts:
if part not in value:
raise BaseException("%s not in config [%s]" %
(part, '-->'.join(walking)))
value = value[part]
walking.append(part)
if cast is None:
self.int_cache[path] = value
else:
try:
value = cast(value)
self.int_cache[path] = value
except (ValueError, TypeError):
raise BaseException("%s can't be converted to %s" %
(value, cast))
return value
class AppConfigLoader(object):
def __init__(self, configfile=None):
if not configfile:
priv_folder = os.path.join(current.request.folder, 'private')
configfile = os.path.join(priv_folder, 'appconfig.ini')
if not os.path.isfile(configfile):
configfile = os.path.join(priv_folder, 'appconfig.json')
if not os.path.isfile(configfile):
configfile = None
if not configfile or not os.path.isfile(configfile):
raise BaseException("Config file not found")
self.file = configfile
self.ctype = os.path.splitext(configfile)[1][1:]
self.settings = None
self.read_config()
def read_config_ini(self):
config = SafeConfigParser()
config.read(self.file)
settings = {}
for section in config.sections():
settings[section] = {}
for option in config.options(section):
settings[section][option] = config.get(section, option)
self.settings = AppConfigDict(settings)
def read_config_json(self):
with open(self.file, 'r') as c:
self.settings = AppConfigDict(json_parser.load(c))
def read_config(self):
if self.settings is None:
try:
getattr(self, 'read_config_' + self.ctype)()
except AttributeError:
raise BaseException("Unsupported config file format")
return self.settings
-1
View File
@@ -340,4 +340,3 @@ example_policies = {
'DELETE':{'query':None}, 'DELETE':{'query':None},
}, },
} }
@@ -73,7 +73,7 @@ class BrowserID(object):
auth_info_json = fetch(self.verify_url, data=verify_data) auth_info_json = fetch(self.verify_url, data=verify_data)
j = json.loads(auth_info_json) j = json.loads(auth_info_json)
epoch_time = int(time.time() * 1000) # we need 13 digit epoch time epoch_time = int(time.time() * 1000) # we need 13 digit epoch time
if j["status"] == "okay" and j["audience"] == audience and j['issuer'] == issuer and j['expires'] >= epoch_time: if j["status"] == "okay" and j["audience"] == audience and j['issuer'].endswith(issuer) and j['expires'] >= epoch_time:
return dict(email=j['email']) return dict(email=j['email'])
elif self.on_login_failure: elif self.on_login_failure:
#print "status: ", j["status"]=="okay", j["status"] #print "status: ", j["status"]=="okay", j["status"]
-1
View File
@@ -252,4 +252,3 @@ class IMAP4(object):
return Connection() return Connection()
IMAP4_SSL = IMAP4 IMAP4_SSL = IMAP4
+1 -1
View File
@@ -235,7 +235,7 @@ class SoapDispatcher(object):
body.marshall("%s:Fault" % soap_ns, fault, ns=False) body.marshall("%s:Fault" % soap_ns, fault, ns=False)
else: else:
# return normal value # return normal value
res = body.add_child("%sResponse" % name, ns=prefix) res = body.add_child("%sResponse" % name, ns=self.namespace)
if not prefix: if not prefix:
res['xmlns'] = self.namespace # add target namespace res['xmlns'] = self.namespace # add target namespace
+4 -4
View File
@@ -192,7 +192,7 @@ jQuery(function(){
<label class="control-label">Card Number</label> <label class="control-label">Card Number</label>
<div class="controls"> <div class="controls">
<input type="text" size="20" data-stripe="number" <input type="text" size="20" data-stripe="number"
placeholder="4242424242424242"/> placeholder="4242424242424242"/>
</div> </div>
</div> </div>
@@ -200,7 +200,7 @@ jQuery(function(){
<label class="control-label">CVC</label> <label class="control-label">CVC</label>
<div class="controls"> <div class="controls">
<input type="text" size="4" style="width:80px" data-stripe="cvc" <input type="text" size="4" style="width:80px" data-stripe="cvc"
placeholder="XXX"/> placeholder="XXX"/>
<a href="http://en.wikipedia.org/wiki/Card_Verification_Code" target="_blank">What is this?</a> <a href="http://en.wikipedia.org/wiki/Card_Verification_Code" target="_blank">What is this?</a>
</div> </div>
</div> </div>
@@ -209,10 +209,10 @@ jQuery(function(){
<label class="control-label">Expiration</label> <label class="control-label">Expiration</label>
<div class="controls"> <div class="controls">
<input type="text" size="2" style="width:40px" data-stripe="exp-month" <input type="text" size="2" style="width:40px" data-stripe="exp-month"
placeholder="MM"/> placeholder="MM"/>
/ /
<input type="text" size="4" style="width:80px" data-stripe="exp-year" <input type="text" size="4" style="width:80px" data-stripe="exp-year"
placeholder="YYYY"/> placeholder="YYYY"/>
</div> </div>
</div> </div>
-1
View File
@@ -1,4 +1,3 @@
""" """
Developed by Massimo Di Pierro Developed by Massimo Di Pierro
Released under the web2py license (LGPL) Released under the web2py license (LGPL)
+3 -1
View File
@@ -550,7 +550,7 @@ class Response(Storage):
else: else:
attname = filename attname = filename
headers["Content-Disposition"] = \ headers["Content-Disposition"] = \
"attachment;filename=%s" % attname 'attachment;filename="%s"' % attname
if not request: if not request:
request = current.request request = current.request
@@ -630,6 +630,8 @@ class Response(Storage):
return self.stream(stream, chunk_size=chunk_size, request=request) return self.stream(stream, chunk_size=chunk_size, request=request)
def json(self, data, default=None): def json(self, data, default=None):
if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = 'application/json'
return json(data, default=default or custom_json) return json(data, default=default or custom_json)
def xmlrpc(self, request, methods): def xmlrpc(self, request, methods):
+1 -1
View File
@@ -850,7 +850,7 @@ class DIV(XmlComponent):
""" """
components = [] components = []
for c in self.components: for c in self.components:
if isinstance(c, allowed_parents): if isinstance(c, (allowed_parents,CAT)):
pass pass
elif wrap_lambda: elif wrap_lambda:
c = wrap_lambda(c) c = wrap_lambda(c)
+2 -1
View File
@@ -878,6 +878,7 @@ class MapUrlIn(object):
self.domain_application = None self.domain_application = None
self.domain_controller = None self.domain_controller = None
self.domain_function = None self.domain_function = None
self.map_hyphen = base.map_hyphen
arg0 = self.harg0 arg0 = self.harg0
if not base.exclusive_domain and base.applications and arg0 in base.applications: if not base.exclusive_domain and base.applications and arg0 in base.applications:
self.application = arg0 self.application = arg0
@@ -1256,9 +1257,9 @@ class MapUrlOut(object):
"Builds a/c/f from components" "Builds a/c/f from components"
acf = '' acf = ''
if self.map_hyphen: if self.map_hyphen:
self.application = self.application.replace('_', '-')
self.controller = self.controller.replace('_', '-') self.controller = self.controller.replace('_', '-')
if self.controller != 'static' and not self.controller.startswith('static/'): if self.controller != 'static' and not self.controller.startswith('static/'):
self.application = self.application.replace('_', '-')
self.function = self.function.replace('_', '-') self.function = self.function.replace('_', '-')
if not self.omit_application: if not self.omit_application:
acf += '/' + self.application acf += '/' + self.application
+5 -4
View File
@@ -1850,13 +1850,14 @@ class WSGIWorker(Worker):
if data: if data:
self.write(data, sections) self.write(data, sections)
if self.chunked: if not self.headers_sent:
# If chunked, send our final chunk length
self.conn.sendall(b('0\r\n\r\n'))
elif not self.headers_sent:
# Send headers if the body was empty # Send headers if the body was empty
self.send_headers('', sections) self.send_headers('', sections)
if self.chunked and self.request_method != 'HEAD':
# If chunked, send our final chunk length
self.conn.sendall(b('0\r\n\r\n'))
# Don't capture exceptions here. The Worker class handles # Don't capture exceptions here. The Worker class handles
# them appropriately. # them appropriately.
finally: finally:
+34 -20
View File
@@ -87,11 +87,17 @@ if 'WEB2PY_PATH' not in os.environ:
os.environ['WEB2PY_PATH'] = path os.environ['WEB2PY_PATH'] = path
try: try:
from gluon.contrib.simplejson import loads, dumps # try external module
except:
from simplejson import loads, dumps from simplejson import loads, dumps
except ImportError:
try:
# try stdlib (Python >= 2.6)
from json import loads, dumps
except:
# fallback to pure-Python module
from gluon.contrib.simplejson import loads, dumps
IDENTIFIER = "%s#%s" % (socket.gethostname(),os.getpid()) IDENTIFIER = "%s#%s" % (socket.gethostname(), os.getpid())
logger = logging.getLogger('web2py.scheduler.%s' % IDENTIFIER) logger = logging.getLogger('web2py.scheduler.%s' % IDENTIFIER)
@@ -160,6 +166,7 @@ class TaskReport(object):
def __str__(self): def __str__(self):
return '<TaskReport: %s>' % self.status return '<TaskReport: %s>' % self.status
class JobGraph(object): class JobGraph(object):
"""Experimental: with JobGraph you can specify """Experimental: with JobGraph you can specify
dependencies amongs tasks""" dependencies amongs tasks"""
@@ -170,7 +177,9 @@ class JobGraph(object):
def add_deps(self, task_parent, task_child): def add_deps(self, task_parent, task_child):
"""Creates a dependency between task_parent and task_child""" """Creates a dependency between task_parent and task_child"""
self.db.scheduler_task_deps.insert(task_parent=task_parent, task_child=task_child, job_name=self.job_name) self.db.scheduler_task_deps.insert(task_parent=task_parent,
task_child=task_child,
job_name=self.job_name)
def validate(self, job_name): def validate(self, job_name):
"""Validates if all tasks job_name can be completed, i.e. there """Validates if all tasks job_name can be completed, i.e. there
@@ -195,16 +204,18 @@ class JobGraph(object):
try: try:
rtn = [] rtn = []
for k, v in nested_dict.items(): for k, v in nested_dict.items():
v.discard(k) # Ignore self dependencies v.discard(k) # Ignore self dependencies
extra_items_in_deps = reduce(set.union, nested_dict.values()) - set(nested_dict.keys()) extra_items_in_deps = reduce(set.union, nested_dict.values()) - set(nested_dict.keys())
nested_dict.update(dict((item, set()) for item in extra_items_in_deps)) nested_dict.update(dict((item, set()) for item in extra_items_in_deps))
while True: while True:
ordered = set(item for item,dep in nested_dict.items() if not dep) ordered = set(item for item, dep in nested_dict.items() if not dep)
if not ordered: if not ordered:
break break
rtn.append(ordered) rtn.append(ordered)
nested_dict = dict((item, (dep - ordered)) for item, dep in nested_dict.items() nested_dict = dict(
if item not in ordered) (item, (dep - ordered)) for item, dep in nested_dict.items()
if item not in ordered
)
assert not nested_dict, "A cyclic dependency exists amongst %r" % nested_dict assert not nested_dict, "A cyclic dependency exists amongst %r" % nested_dict
db.commit() db.commit()
return rtn return rtn
@@ -212,6 +223,7 @@ class JobGraph(object):
db.rollback() db.rollback()
return None return None
def demo_function(*argv, **kwargs): def demo_function(*argv, **kwargs):
""" test function """ """ test function """
for i in range(argv[0]): for i in range(argv[0]):
@@ -268,7 +280,7 @@ def executor(queue, task, out):
def write(self, data): def write(self, data):
self.out_queue.put(data) self.out_queue.put(data)
W2P_TASK = Storage({'id' : task.task_id, 'uuid' : task.uuid}) W2P_TASK = Storage({'id': task.task_id, 'uuid': task.uuid})
stdout = LogOutput(out) stdout = LogOutput(out)
try: try:
if task.app: if task.app:
@@ -293,7 +305,7 @@ def executor(queue, task, out):
raise NameError( raise NameError(
"name '%s' not found in scheduler's environment" % f) "name '%s' not found in scheduler's environment" % f)
#Inject W2P_TASK into environment #Inject W2P_TASK into environment
_env.update({'W2P_TASK' : W2P_TASK}) _env.update({'W2P_TASK': W2P_TASK})
#Inject W2P_TASK into current #Inject W2P_TASK into current
from gluon import current from gluon import current
current.W2P_TASK = W2P_TASK current.W2P_TASK = W2P_TASK
@@ -357,8 +369,7 @@ class MetaScheduler(threading.Thread):
start = time.time() start = time.time()
while p.is_alive() and ( while p.is_alive() and (not task.timeout or time.time() - start < task.timeout):
not task.timeout or time.time() - start < task.timeout):
if tout: if tout:
try: try:
logger.debug(' partial output saved') logger.debug(' partial output saved')
@@ -568,7 +579,7 @@ class Scheduler(MetaScheduler):
queue=0, queue=0,
distribution=None, distribution=None,
workers=0) workers=0)
) #dict holding statistics ) # dict holding statistics
from gluon import current from gluon import current
current._scheduler = self current._scheduler = self
@@ -740,7 +751,7 @@ class Scheduler(MetaScheduler):
contention and retries `assign_task` after 0.5 seconds contention and retries `assign_task` after 0.5 seconds
""" """
logger.debug('Assigning tasks...') logger.debug('Assigning tasks...')
db.commit() #db.commit() only for Mysql db.commit() # db.commit() only for Mysql
x = 0 x = 0
while x < 10: while x < 10:
try: try:
@@ -761,7 +772,7 @@ class Scheduler(MetaScheduler):
contention and retries `pop_task` after 0.5 seconds contention and retries `pop_task` after 0.5 seconds
""" """
db = self.db db = self.db
db.commit() #another nifty db.commit() only for Mysql db.commit() # another nifty db.commit() only for Mysql
x = 0 x = 0
while x < 10: while x < 10:
try: try:
@@ -1077,6 +1088,8 @@ class Scheduler(MetaScheduler):
#build workers as dict of groups #build workers as dict of groups
wkgroups = {} wkgroups = {}
for w in all_workers: for w in all_workers:
if w.worker_stats['status'] == 'RUNNING':
continue
group_names = w.group_names group_names = w.group_names
for gname in group_names: for gname in group_names:
if gname not in wkgroups: if gname not in wkgroups:
@@ -1090,8 +1103,9 @@ class Scheduler(MetaScheduler):
db( db(
(st.status.belongs((QUEUED, ASSIGNED))) & (st.status.belongs((QUEUED, ASSIGNED))) &
(st.stop_time < now) (st.stop_time < now)
).update(status=EXPIRED) ).update(status=EXPIRED)
#calculate dependencies
deps_with_no_deps = db( deps_with_no_deps = db(
(sd.can_visit == False) & (sd.can_visit == False) &
(~sd.task_child.belongs( (~sd.task_child.belongs(
@@ -1163,7 +1177,7 @@ class Scheduler(MetaScheduler):
if not task.task_name: if not task.task_name:
d['task_name'] = task.function_name d['task_name'] = task.function_name
db( db(
(st.id==task.id) & (st.id == task.id) &
(st.status.belongs((QUEUED, ASSIGNED))) (st.status.belongs((QUEUED, ASSIGNED)))
).update(**d) ).update(**d)
wkgroups[gname]['workers'][myw]['c'] += 1 wkgroups[gname]['workers'][myw]['c'] += 1
@@ -1207,8 +1221,8 @@ class Scheduler(MetaScheduler):
else: else:
for group in group_names: for group in group_names:
workers = self.db( workers = self.db(
(ws.group_names.contains(group)) & (ws.group_names.contains(group)) &
(~ws.status.belongs(exclusion)) (~ws.status.belongs(exclusion))
)._select(ws.id, limitby=(0,limit)) )._select(ws.id, limitby=(0,limit))
self.db(ws.id.belongs(workers)).update(status=action) self.db(ws.id.belongs(workers)).update(status=action)
@@ -1339,7 +1353,7 @@ class Scheduler(MetaScheduler):
**dict(orderby=orderby, **dict(orderby=orderby,
left=left, left=left,
limitby=(0, 1)) limitby=(0, 1))
).first() ).first()
if row and output: if row and output:
row.result = row.scheduler_run.run_result and \ row.result = row.scheduler_run.run_result and \
loads(row.scheduler_run.run_result, loads(row.scheduler_run.run_result,
+2
View File
@@ -100,6 +100,8 @@ def custom_json(o):
return str(o) return str(o)
elif isinstance(o, XmlComponent): elif isinstance(o, XmlComponent):
return str(o) return str(o)
elif isinstance(o, set):
return list(o)
elif hasattr(o, 'as_list') and callable(o.as_list): elif hasattr(o, 'as_list') and callable(o.as_list):
return o.as_list() return o.as_list()
elif hasattr(o, 'as_dict') and callable(o.as_dict): elif hasattr(o, 'as_dict') and callable(o.as_dict):
+15 -5
View File
@@ -1988,9 +1988,9 @@ class SQLFORM(FORM):
buttonback='icon leftarrow icon-arrow-left glyphicon glyphicon-arrow-left', buttonback='icon leftarrow icon-arrow-left glyphicon glyphicon-arrow-left',
buttonexport='icon downarrow icon-download glyphicon glyphicon-download', buttonexport='icon downarrow icon-download glyphicon glyphicon-download',
buttondelete='icon trash icon-trash glyphicon glyphicon-trash', buttondelete='icon trash icon-trash glyphicon glyphicon-trash',
buttonedit='icon pen icon-pencil glyphicon glyphicon-arrow-pencil', buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil',
buttontable='icon rightarrow icon-arrow-right glyphicon glyphicon-arrow-right', buttontable='icon rightarrow icon-arrow-right glyphicon glyphicon-arrow-right',
buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-arrow-zoom-in', buttonview='icon magnifier icon-zoom-in glyphicon glyphicon-zoom-in',
) )
elif not isinstance(ui, dict): elif not isinstance(ui, dict):
raise RuntimeError('SQLFORM.grid ui argument must be a dictionary') raise RuntimeError('SQLFORM.grid ui argument must be a dictionary')
@@ -2333,8 +2333,12 @@ class SQLFORM(FORM):
#fields but not virtual fields #fields but not virtual fields
sfields = reduce(lambda a, b: a + b, sfields = reduce(lambda a, b: a + b,
[[f for f in t if f.readable and not isinstance(f, Field.Virtual)] for t in tables]) [[f for f in t if f.readable and not isinstance(f, Field.Virtual)] for t in tables])
dbset = dbset(SQLFORM.build_query( #use custom_query using searchable
sfields, keywords)) if callable(searchable):
dbset = dbset(searchable(sfields, keywords))
else:
dbset = dbset(SQLFORM.build_query(
sfields, keywords))
rows = dbset.select(left=left, orderby=orderby, rows = dbset.select(left=left, orderby=orderby,
cacheable=True, *selectable_columns) cacheable=True, *selectable_columns)
except Exception, e: except Exception, e:
@@ -2349,7 +2353,9 @@ class SQLFORM(FORM):
# expcolumns is all cols to be exported including virtual fields # expcolumns is all cols to be exported including virtual fields
rows.colnames = expcolumns rows.colnames = expcolumns
oExp = clazz(rows) oExp = clazz(rows)
filename = '.'.join(('rows', oExp.file_ext)) export_filename = \
request.vars.get('_export_filename') or 'rows'
filename = '.'.join((export_filename, oExp.file_ext))
response.headers['Content-Type'] = oExp.content_type response.headers['Content-Type'] = oExp.content_type
response.headers['Content-Disposition'] = \ response.headers['Content-Disposition'] = \
'attachment;filename=' + filename + ';' 'attachment;filename=' + filename + ';'
@@ -2386,6 +2392,8 @@ class SQLFORM(FORM):
spanel_id = '%s_query_fields' % prefix spanel_id = '%s_query_fields' % prefix
sfields_id = '%s_query_panel' % prefix sfields_id = '%s_query_panel' % prefix
skeywords_id = '%s_keywords' % prefix skeywords_id = '%s_keywords' % prefix
## hidden fields to presever keywords in url after the submit
hidden_fields = [INPUT(_type='hidden', _value=value, _name=key) for key, value in request.get_vars.items() if key not in ['keywords', 'page']]
search_widget = lambda sfield, url: CAT(FORM( search_widget = lambda sfield, url: CAT(FORM(
INPUT(_name='keywords', _value=keywords, INPUT(_name='keywords', _value=keywords,
_id=skeywords_id,_class='form-control', _id=skeywords_id,_class='form-control',
@@ -2394,7 +2402,9 @@ class SQLFORM(FORM):
INPUT(_type='submit', _value=T('Search'), _class="btn btn-default"), INPUT(_type='submit', _value=T('Search'), _class="btn btn-default"),
INPUT(_type='submit', _value=T('Clear'), _class="btn btn-default", INPUT(_type='submit', _value=T('Clear'), _class="btn btn-default",
_onclick="jQuery('#%s').val('');" % skeywords_id), _onclick="jQuery('#%s').val('');" % skeywords_id),
*hidden_fields,
_method="GET", _action=url), search_menu) _method="GET", _action=url), search_menu)
# TODO vars from the url should be removed, they are not used by the submit
form = search_widget and search_widget(sfields, url()) or '' form = search_widget and search_widget(sfields, url()) or ''
console.append(add) console.append(add)
console.append(form) console.append(form)
+1 -1
View File
@@ -195,7 +195,7 @@ class Messages(Settings):
def __getattr__(self, key): def __getattr__(self, key):
value = self[key] value = self[key]
if isinstance(value, str): if isinstance(value, str):
return str(self.T(value)) return self.T(value)
return value return value
class FastStorage(dict): class FastStorage(dict):
+1
View File
@@ -17,6 +17,7 @@ from test_utils import *
from test_contribs import * from test_contribs import *
from test_web import * from test_web import *
from test_dal import * from test_dal import *
from test_tools import *
if sys.version[:3] == '2.7': if sys.version[:3] == '2.7':
from test_old_doctests import * from test_old_doctests import *
+8 -4
View File
@@ -37,7 +37,6 @@ class TestCache(unittest.TestCase):
def testCacheInRam(self): def testCacheInRam(self):
# defaults to mode='http' # defaults to mode='http'
cache = CacheInRam() cache = CacheInRam()
self.assertEqual(cache('a', lambda: 1, 0), 1) self.assertEqual(cache('a', lambda: 1, 0), 1)
self.assertEqual(cache('a', lambda: 2, 100), 1) self.assertEqual(cache('a', lambda: 2, 100), 1)
@@ -66,7 +65,6 @@ class TestCache(unittest.TestCase):
def testCacheOnDisk(self): def testCacheOnDisk(self):
# defaults to mode='http' # defaults to mode='http'
s = Storage({'application': 'admin', s = Storage({'application': 'admin',
'folder': 'applications/admin'}) 'folder': 'applications/admin'})
cache = CacheOnDisk(s) cache = CacheOnDisk(s)
@@ -102,8 +100,14 @@ class TestCache(unittest.TestCase):
self.assertEqual(prefix('a', lambda: 2, 100), 1) self.assertEqual(prefix('a', lambda: 2, 100), 1)
self.assertEqual(cache.ram('prefixa', lambda: 2, 100), 1) self.assertEqual(cache.ram('prefixa', lambda: 2, 100), 1)
def testRegex(self):
cache = CacheInRam()
self.assertEqual(cache('a1', lambda: 1, 0), 1)
self.assertEqual(cache('a2', lambda: 2, 100), 2)
cache.clear(regex=r'a*')
self.assertEqual(cache('a1', lambda: 2, 0), 2)
self.assertEqual(cache('a2', lambda: 3, 100), 3)
return
if __name__ == '__main__': if __name__ == '__main__':
setUpModule() # pre-python-2.7 setUpModule() # pre-python-2.7
+35 -3
View File
@@ -4,14 +4,24 @@
""" Unit tests for contribs """ """ Unit tests for contribs """
import unittest import unittest
import os
from fix_path import fix_sys_path from fix_path import fix_sys_path
fix_sys_path(__file__) fix_sys_path(__file__)
from gluon.storage import Storage
import gluon.contrib.fpdf as fpdf
import gluon.contrib.pyfpdf as pyfpdf
from gluon.contrib.appconfig import AppConfig
from utils import md5_hash
import contrib.fpdf as fpdf def setUpModule():
import contrib.pyfpdf as pyfpdf pass
def tearDownModule():
if os.path.isfile('appconfig.json'):
os.unlink('appconfig.json')
class TestContribs(unittest.TestCase): class TestContribs(unittest.TestCase):
@@ -35,6 +45,28 @@ class TestContribs(unittest.TestCase):
self.assertTrue(fpdf.FPDF_VERSION in pdf_out, 'version string') self.assertTrue(fpdf.FPDF_VERSION in pdf_out, 'version string')
self.assertTrue('hello world' in pdf_out, 'sample message') self.assertTrue('hello world' in pdf_out, 'sample message')
def test_appconfig(self):
"""
Test for the appconfig module
"""
from gluon import current
s = Storage({'application': 'admin',
'folder': 'applications/admin'})
current.request = s
simple_config = '{"config1" : "abc", "config2" : "bcd", "config3" : { "key1" : 1, "key2" : 2} }'
with open('appconfig.json', 'w') as g:
g.write(simple_config)
myappconfig = AppConfig('appconfig.json')
self.assertEqual(myappconfig['config1'], 'abc')
self.assertEqual(myappconfig['config2'], 'bcd')
self.assertEqual(myappconfig.take('config1'), 'abc')
self.assertEqual(myappconfig.take('config3.key1', cast=str), '1')
# once parsed, can't be casted to other types
self.assertEqual(myappconfig.take('config3.key1', cast=int), '1')
self.assertEqual(myappconfig.take('config3.key2'), 2)
current.request = {}
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+83
View File
@@ -0,0 +1,83 @@
#!/bin/python
# -*- coding: utf-8 -*-
"""
Unit tests for gluon.tools
"""
import os
import sys
if sys.version < "2.7":
import unittest2 as unittest
else:
import unittest
from fix_path import fix_sys_path
fix_sys_path(__file__)
DEFAULT_URI = os.getenv('DB', 'sqlite:memory')
from gluon.dal import DAL, Field
from dal.objects import Table
from tools import Auth
from gluon.globals import Request, Response, Session
from storage import Storage
from languages import translator
from gluon.http import HTTP
python_version = sys.version[:3]
IS_IMAP = "imap" in DEFAULT_URI
@unittest.skipIf(IS_IMAP, "TODO: Imap raises 'Connection refused'")
class testAuth(unittest.TestCase):
def testRun(self):
# setup
request = Request(env={})
request.application = 'a'
request.controller = 'c'
request.function = 'f'
request.folder = 'applications/admin'
response = Response()
session = Session()
T = translator('', 'en')
session.connect(request, response)
from gluon.globals import current
current.request = request
current.response = response
current.session = session
current.T = T
db = DAL(DEFAULT_URI, check_reserved=['all'])
auth = Auth(db)
auth.define_tables(username=True, signature=False)
self.assertTrue('auth_user' in db)
self.assertTrue('auth_group' in db)
self.assertTrue('auth_membership' in db)
self.assertTrue('auth_permission' in db)
self.assertTrue('auth_event' in db)
db.define_table('t0', Field('tt'), auth.signature)
auth.enable_record_versioning(db)
self.assertTrue('t0_archive' in db)
for f in ['login', 'register', 'retrieve_password',
'retrieve_username']:
html_form = getattr(auth, f)().xml()
self.assertTrue('name="_formkey"' in html_form)
for f in ['logout', 'verify_email', 'reset_password',
'change_password', 'profile', 'groups']:
self.assertRaisesRegexp(HTTP, "303*", getattr(auth, f))
self.assertRaisesRegexp(HTTP, "401*", auth.impersonate)
try:
for t in ['t0_archive', 't0', 'auth_cas', 'auth_event',
'auth_membership', 'auth_permission', 'auth_group',
'auth_user']:
db[t].drop()
except SyntaxError as e:
# GAE doesn't support drop
pass
return
if __name__ == '__main__':
unittest.main()
+9 -5
View File
@@ -136,6 +136,7 @@ class Mail(object):
mail.settings.server mail.settings.server
mail.settings.sender mail.settings.sender
mail.settings.login mail.settings.login
mail.settings.timeout = 60 # seconds (default)
When server is 'logging', email is logged but not sent (debug mode) When server is 'logging', email is logged but not sent (debug mode)
@@ -265,6 +266,7 @@ class Mail(object):
settings.sender = sender settings.sender = sender
settings.login = login settings.login = login
settings.tls = tls settings.tls = tls
settings.timeout = 60 # seconds
settings.hostname = None settings.hostname = None
settings.ssl = False settings.ssl = False
settings.cipher_type = None settings.cipher_type = None
@@ -787,10 +789,11 @@ class Mail(object):
subject=subject, body=text, **xcc) subject=subject, body=text, **xcc)
else: else:
smtp_args = self.settings.server.split(':') smtp_args = self.settings.server.split(':')
kwargs = dict(timeout = self.settings.timeout)
if self.settings.ssl: if self.settings.ssl:
server = smtplib.SMTP_SSL(*smtp_args) server = smtplib.SMTP_SSL(*smtp_args, **kwargs)
else: else:
server = smtplib.SMTP(*smtp_args) server = smtplib.SMTP(*smtp_args, **kwargs)
if self.settings.tls and not self.settings.ssl: if self.settings.tls and not self.settings.ssl:
server.ehlo(self.settings.hostname) server.ehlo(self.settings.hostname)
server.starttls() server.starttls()
@@ -842,6 +845,7 @@ class Recaptcha(DIV):
comment = '', comment = '',
ajax=False ajax=False
): ):
request = request or current.request
self.request_vars = request and request.vars or current.request.vars self.request_vars = request and request.vars or current.request.vars
self.remote_addr = request.env.remote_addr self.remote_addr = request.env.remote_addr
self.public_key = public_key self.public_key = public_key
@@ -1257,7 +1261,8 @@ class Auth(object):
def __init__(self, environment=None, db=None, mailer=True, def __init__(self, environment=None, db=None, mailer=True,
hmac_key=None, controller='default', function='user', hmac_key=None, controller='default', function='user',
cas_provider=None, signature=True, secure=False, cas_provider=None, signature=True, secure=False,
csrf_prevention=True, propagate_extension=None): csrf_prevention=True, propagate_extension=None,
url_index=None):
## next two lines for backward compatibility ## next two lines for backward compatibility
if not db and environment and isinstance(environment, DAL): if not db and environment and isinstance(environment, DAL):
@@ -1294,7 +1299,7 @@ class Auth(object):
del session.auth del session.auth
# ## what happens after login? # ## what happens after login?
url_index = URL(controller, 'index') url_index = url_index or URL(controller, 'index')
url_login = URL(controller, function, args='login', url_login = URL(controller, function, args='login',
extension = propagate_extension) extension = propagate_extension)
# ## what happens after registration? # ## what happens after registration?
@@ -6164,4 +6169,3 @@ class Config(object):
if __name__ == '__main__': if __name__ == '__main__':
import doctest import doctest
doctest.testmod() doctest.testmod()
-1
View File
@@ -350,4 +350,3 @@ def getipaddrinfo(host):
and isinstance(addrinfo[4][0], basestring)] and isinstance(addrinfo[4][0], basestring)]
except socket.error: except socket.error:
return [] return []
-1
View File
@@ -110,4 +110,3 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()
-1
View File
@@ -218,4 +218,3 @@ class LinuxService(ServiceBase):
else: else:
cmd = env['update-rc.d'] cmd = env['update-rc.d']
return [cmd, self.name, 'remove'] return [cmd, self.name, 'remove']