diff --git a/.gitignore b/.gitignore index 4b13a236..4aa1d556 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,8 @@ applications/*/errors/* applications/*/cache/* applications/*/uploads/* applications/*/*.py[oc] +applications/*/static/temp +applications/*/progress.log applications/examples/static/epydoc applications/examples/static/sphinx applications/admin/cron/cron.master diff --git a/.travis.yml b/.travis.yml index 8cf53e1b..ac5914e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,8 @@ before_script: - if [[ $TRAVIS_PYTHON_VERSION == '2.5' ]]; then pip install pysqlite; fi - if [[ $DB == mysql* ]]; then mysql -e 'create database test_w2p;'; fi - if [[ $DB == postgres* ]]; then psql -c 'create database test_w2p;' -U postgres; fi + - if [[ $DB == postgres* ]]; then psql -c 'create extension postgis;' -U postgres -d test_w2p; fi + # Install last sdk for app engine (update only whenever a new release is available) - if [[ $DB == google* ]]; then wget http://googleappengine.googlecode.com/files/google_appengine_1.8.9.zip -nv; fi diff --git a/Makefile b/Makefile index fb0a6590..8bf38b2c 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,13 @@ epydoc: cp applications/examples/static/title.png applications/examples/static/epydoc tests: python web2py.py --run_system_tests +coverage: + coverage erase --rcfile=gluon/tests/coverage.ini + export COVERAGE_PROCESS_START=gluon/tests/coverage.ini + python web2py.py --run_system_tests --with_coverage + coverage combine --rcfile=gluon/tests/coverage.ini + sleep 1 + coverage html --rcfile=gluon/tests/coverage.ini update: wget -O gluon/contrib/feedparser.py http://feedparser.googlecode.com/svn/trunk/feedparser/feedparser.py wget -O gluon/contrib/simplejsonrpc.py http://rad2py.googlecode.com/hg/ide2py/simplejsonrpc.py diff --git a/VERSION b/VERSION index 6126c887..ce54ce20 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.10.0-beta+timestamp.2014.09.24.13.35.58 +Version 2.10.0-beta+timestamp.2014.10.16.15.58.50 diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py index 3b78a019..50263b35 100644 --- a/applications/admin/controllers/appadmin.py +++ b/applications/admin/controllers/appadmin.py @@ -461,34 +461,24 @@ def ccache(): if value[0] < ram['oldest']: ram['oldest'] = value[0] ram['keys'].append((key, GetInHMS(time.time() - value[0]))) - folder = os.path.join(request.folder,'cache') - if not os.path.exists(folder): - os.mkdir(folder) - locker = open(os.path.join(folder, 'cache.lock'), 'a') - portalocker.lock(locker, portalocker.LOCK_EX) - disk_storage = shelve.open( - os.path.join(folder, 'cache.shelve')) - try: - for key, value in disk_storage.items(): - if isinstance(value, dict): - disk['hits'] = value['hit_total'] - value['misses'] - disk['misses'] = value['misses'] - try: - disk['ratio'] = disk['hits'] * 100 / value['hit_total'] - except (KeyError, ZeroDivisionError): - disk['ratio'] = 0 - else: - if hp: - disk['bytes'] += hp.iso(value[1]).size - disk['objects'] += hp.iso(value[1]).count - disk['entries'] += 1 - if value[0] < disk['oldest']: - disk['oldest'] = value[0] - disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - finally: - portalocker.unlock(locker) - locker.close() - disk_storage.close() + + for key in cache.disk.storage: + value = cache.disk.storage[key] + if isinstance(value, dict): + disk['hits'] = value['hit_total'] - value['misses'] + disk['misses'] = value['misses'] + try: + disk['ratio'] = disk['hits'] * 100 / value['hit_total'] + except (KeyError, ZeroDivisionError): + disk['ratio'] = 0 + else: + if hp: + disk['bytes'] += hp.iso(value[1]).size + disk['objects'] += hp.iso(value[1]).count + disk['entries'] += 1 + if value[0] < disk['oldest']: + disk['oldest'] = value[0] + disk['keys'].append((key, GetInHMS(time.time() - value[0]))) total['entries'] = ram['entries'] + disk['entries'] total['bytes'] = ram['bytes'] + disk['bytes'] diff --git a/applications/admin/languages/pt.py b/applications/admin/languages/pt.py index da379dc4..3f850c3c 100644 --- a/applications/admin/languages/pt.py +++ b/applications/admin/languages/pt.py @@ -7,8 +7,10 @@ '%s %%{row} updated': '%s registros atualizados', '%Y-%m-%d': '%d/%m/%Y', '%Y-%m-%d %H:%M:%S': '%d/%m/%Y %H:%M:%S', -'(requires internet access)': '(requer acesso a internet)', +'(requires internet access)': '(requer acesso à internet)', +'(requires internet access, experimental)': '(requer acesso à internet, experimental)', '(something like "it-it")': '(algo como "it-it")', +'@markmin\x01(file **gluon/contrib/plural_rules/%s.py** is not found)': '(file **gluon/contrib/plural_rules/%s.py** is not found)', '@markmin\x01An error occured, please [[reload %s]] the page': 'An error occured, please [[reload %s]] the page', '@markmin\x01Searching: **%s** %%{file}': 'Searching: **%s** files', 'A new version of web2py is available': 'Está disponível uma nova versão do web2py', @@ -16,7 +18,7 @@ 'About': 'sobre', 'About application': 'Sobre a aplicação', 'additional code for your application': 'código adicional para sua aplicação', -'Additional code for your application': 'Additional code for your application', +'Additional code for your application': 'Código adicional para a sua aplicação', 'admin disabled because no admin password': ' admin desabilitado por falta de senha definida', 'admin disabled because not supported on google app engine': 'admin dehabilitado, não é soportado no GAE', 'admin disabled because unable to access password file': 'admin desabilitado, não foi possível ler o arquivo de senha', @@ -33,6 +35,8 @@ 'application compiled': 'aplicação compilada', 'application is compiled and cannot be designed': 'A aplicação está compilada e não pode ser modificada', 'Application name:': 'Nome da aplicação:', +'are not used': 'não usadas', +'are not used yet': 'ainda não usadas', 'Are you sure you want to delete file "%s"?': 'Tem certeza que deseja apagar o arquivo "%s"?', 'Are you sure you want to delete plugin "%s"?': 'Tem certeza que deseja apagar o plugin "%s"?', 'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?', @@ -43,17 +47,20 @@ 'ATTENTION: Login requires a secure (HTTPS) connection or running on localhost.': 'ATENÇÃO o login requer uma conexão segura (HTTPS) ou executar de localhost.', 'ATTENTION: TESTING IS NOT THREAD SAFE SO DO NOT PERFORM MULTIPLE TESTS CONCURRENTLY.': 'ATENÇÃO OS TESTES NÃO THREAD SAFE, NÃO EFETUE MÚLTIPLOS TESTES AO MESMO TEMPO.', 'ATTENTION: you cannot edit the running application!': 'ATENÇÃO: Não pode modificar a aplicação em execução!', +'Autocomplete Python Code': 'Autocompletar Código Python', 'Available databases and tables': 'Bancos de dados e tabelas disponíveis', 'back': 'voltar', 'browse': 'buscar', 'cache': 'cache', 'cache, errors and sessions cleaned': 'cache, erros e sessões eliminadas', +'can be a git repo': 'can be a git repo', 'Cannot be empty': 'Não pode ser vazio', 'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Não é possível compilar: Existem erros em sua aplicação. Depure, corrija os errros e tente novamente', 'Cannot compile: there are errors in your app:': 'Não é possível compilar: Existem erros em sua aplicação', 'cannot create file': 'Não é possível criar o arquivo', 'cannot upload file "%(filename)s"': 'não é possível fazer upload do arquivo "%(filename)s"', 'Change admin password': 'mudar senha de administrador', +'change editor settings': 'mudar definições do editor', 'Change Password': 'Trocar Senha', 'check all': 'marcar todos', 'Check for upgrades': 'checar por atualizações', @@ -67,7 +74,7 @@ 'click to open': 'clique para abrir', 'Client IP': 'IP do cliente', 'code': 'código', -'collapse/expand all': 'collapse/expand all', +'collapse/expand all': 'colapsar/expandir tudo', 'commit (mercurial)': 'commit (mercurial)', 'Compile': 'compilar', 'compiled application removed': 'aplicação compilada removida', @@ -79,6 +86,7 @@ 'Create new application using the Wizard': 'Criar nova aplicação utilizando o assistente', 'create new application:': 'nome da nova aplicação:', 'Create new simple application': 'Crie uma nova aplicação', +'Create/Upload': 'Create/Upload', 'created by': 'criado por', 'crontab': 'crontab', 'Current request': 'Requisição atual', @@ -99,18 +107,24 @@ 'delete': 'apagar', 'delete all checked': 'apagar marcados', 'delete plugin': 'apagar plugin', +'Delete this file (you will be asked to confirm deletion)': 'Delete this file (you will be asked to confirm deletion)', 'Delete:': 'Apague:', 'Deploy': 'publicar', 'Deploy on Google App Engine': 'Publicar no Google App Engine', +'Deploy to OpenShift': 'Deploy to OpenShift', 'Description': 'Descrição', -'DESIGN': 'Projeto', 'design': 'modificar', +'DESIGN': 'Projeto', 'Design for': 'Projeto de', 'Detailed traceback description': 'Detailed traceback description', 'direction: ltr': 'direção: ltr', +'Disable': 'Disable', +'docs': 'docs', 'done!': 'feito!', 'download layouts': 'download layouts', +'Download layouts from repository': 'Download layouts from repository', 'download plugins': 'download plugins', +'Download plugins from repository': 'Download plugins from repository', 'E-mail': 'E-mail', 'EDIT': 'EDITAR', 'Edit': 'editar', @@ -119,6 +133,7 @@ 'Edit current record': 'Editar o registro atual', 'Edit Profile': 'Editar Perfil', 'edit views:': 'editar visões:', +'Editing %s': 'A Editar %s', 'Editing file': 'Editando arquivo', 'Editing file "%s"': 'Editando arquivo "%s"', 'Editing Language file': 'Editando arquivo de linguagem', @@ -129,6 +144,8 @@ 'Error ticket': 'Error ticket', 'Errors': 'erros', 'Exception instance attributes': 'Atributos da instancia de excessão', +'Exit Fullscreen': 'Sair de Ecrã Inteiro', +'Expand Abbreviation (html files only)': 'Expandir Abreviação (só para ficheiros html)', 'export as csv file': 'exportar como arquivo CSV', 'exposes': 'expõe', 'extends': 'estende', @@ -144,20 +161,24 @@ 'file does not exist': 'arquivo não existe', 'file saved on %(time)s': 'arquivo salvo em %(time)s', 'file saved on %s': 'arquivo salvo em %s', -'filter': 'filter', +'filter': 'filtro', +'Find Next': 'Localizar Seguinte', +'Find Previous': 'Localizar Anterior', 'First name': 'Nome', 'Frames': 'Frames', 'Functions with no doctests will result in [passed] tests.': 'Funções sem doctests resultarão em testes [aceitos].', +'graph model': 'graph model', 'Group ID': 'ID do Grupo', 'Hello World': 'Olá Mundo', 'Help': 'ajuda', +'Hide/Show Translated strings': '', 'htmledit': 'htmledit', -'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Se o relatório acima contém um número de ticket, isso indica uma falha no controlador em execução, antes de tantar executar os doctests. Isto acontece geralmente por erro de endentação ou erro fora do código da função.\nO titulo em verde indica que os testes (se definidos) passaram. Neste caso os testes não são mostrados.', +'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Se o relatório acima contém um número de ticket, isso indica uma falha no controlador em execução, antes de tantar executar os doctests. Isto acontece geralmente por erro de endentação ou erro fora do código da função.\r\nO titulo em verde indica que os testes (se definidos) passaram. Neste caso os testes não são mostrados.', 'Import/Export': 'Importar/Exportar', 'includes': 'inclui', 'insert new': 'inserir novo', 'insert new %s': 'inserir novo %s', -'inspect attributes': 'inspect attributes', +'inspect attributes': 'inspecionar atributos', 'Install': 'instalar', 'Installed applications': 'Aplicações instaladas', 'internal error': 'erro interno', @@ -168,6 +189,7 @@ 'Invalid Query': 'Consulta inválida', 'invalid request': 'solicitação inválida', 'invalid ticket': 'ticket inválido', +'Keyboard shortcuts': 'Atalhos de teclado', 'language file "%(filename)s" created/updated': 'arquivo de linguagem "%(filename)s" criado/atualizado', 'Language files (static strings) updated': 'Arquivos de linguagem (textos estáticos) atualizados', 'languages': 'linguagens', @@ -178,11 +200,12 @@ 'License for': 'Licença para', 'loading...': 'carregando...', 'locals': 'locals', -'login': 'inicio de sessão', 'Login': 'Entrar', +'login': 'inicio de sessão', 'Login to the Administrative Interface': 'Entrar na interface adminitrativa', 'Logout': 'finalizar sessão', 'Lost Password': 'Senha perdida', +'Manage': 'Manage', 'manage': 'gerenciar', 'merge': 'juntar', 'Models': 'Modelos', @@ -200,7 +223,10 @@ 'NO': 'NÃO', 'No databases in this application': 'Não existem bancos de dados nesta aplicação', 'no match': 'não encontrado', -'no package selected': 'no package selected', +'no package selected': 'nenhum pacote selecionado', +'online designer': 'online designer', +'or alternatively': 'or alternatively', +'Or Get from URL:': 'Ou Obtenha do URL:', 'or import from csv file': 'ou importar de um arquivo CSV', 'or provide app url:': 'ou forneça a url de uma aplicação:', 'or provide application url:': 'ou forneça a url de uma aplicação:', @@ -209,6 +235,7 @@ 'Overwrite installed app': 'sobrescrever aplicação instalada', 'Pack all': 'criar pacote', 'Pack compiled': 'criar pacote compilado', +'Pack custom': 'Pack custom', 'pack plugin': 'empacotar plugin', 'PAM authenticated user, cannot change password here': 'usuario autenticado por PAM, não pode alterar a senha por aqui', 'Password': 'Senha', @@ -218,16 +245,23 @@ 'Plugin "%s" in application': 'Plugin "%s" na aplicação', 'plugins': 'plugins', 'Plugins': 'Plugins', +'Plural-Forms:': 'Plural-Forms:', 'Powered by': 'Este site utiliza', 'previous 100 rows': '100 registros anteriores', +'Private files': 'Private files', +'private files': 'private files', 'Query:': 'Consulta:', +'Rapid Search': 'Rapid Search', 'record': 'registro', 'record does not exist': 'o registro não existe', 'record id': 'id do registro', 'Record ID': 'ID do Registro', 'Register': 'Registrar-se', 'Registration key': 'Chave de registro', +'Reload routes': 'Reload routes', 'Remove compiled': 'eliminar compilados', +'Replace': 'Substituir', +'Replace All': 'Substituir Tudo', 'request': 'request', 'Resolve Conflict file': 'Arquivo de resolução de conflito', 'response': 'response', @@ -236,7 +270,14 @@ 'Role': 'Papel', 'Rows in table': 'Registros na tabela', 'Rows selected': 'Registros selecionados', +'rules are not defined': 'rules are not defined', +"Run tests in this file (to run all files, you may also use the button labelled 'test')": "Run tests in this file (to run all files, you may also use the button labelled 'test')", +'Running on %s': 'A correr em %s', +'Save': 'Save', 'save': 'salvar', +'Save file:': 'Gravar ficheiro:', +'Save file: %s': 'Gravar ficheiro: %s', +'Save via Ajax': 'Gravar via Ajax', 'Saved file hash:': 'Hash do arquivo salvo:', 'selected': 'selecionado(s)', 'session': 'session', @@ -244,10 +285,13 @@ 'shell': 'Terminal', 'Site': 'site', 'some files could not be removed': 'alguns arquicos não puderam ser removidos', +'Start searching': 'Start searching', 'Start wizard': 'iniciar assistente', 'state': 'estado', +'Static': 'Static', 'static': 'estáticos', 'Static files': 'Arquivos estáticos', +'Submit': 'Submit', 'submit': 'enviar', 'Sure you want to delete this object?': 'Tem certeza que deseja apaagr este objeto?', 'table': 'tabela', @@ -255,21 +299,23 @@ 'test': 'testar', 'Testing application': 'Testando a aplicação', 'The "query" is a condition like "db.table1.field1==\'value\'". Something like "db.table1.field1==db.table2.field2" results in a SQL JOIN.': 'A "consulta" é uma condição como "db.tabela.campo1==\'valor\'". Algo como "db.tabela1.campo1==db.tabela2.campo2" resulta em um JOIN SQL.', -'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'the application logic, each URL path is mapped in one exposed function in the controller': 'A lógica da aplicação, cada URL é mapeada para uma função exposta pelo controlador', -'The data representation, define database tables and sets': 'The data representation, define database tables and sets', +'The application logic, each URL path is mapped in one exposed function in the controller': 'The application logic, each URL path is mapped in one exposed function in the controller', 'the data representation, define database tables and sets': 'A representação dos dadps, define tabelas e estruturas de dados', +'The data representation, define database tables and sets': 'The data representation, define database tables and sets', 'The presentations layer, views are also known as templates': 'The presentations layer, views are also known as templates', 'the presentations layer, views are also known as templates': 'A camada de apresentação, As visões também são chamadas de templates', 'There are no controllers': 'Não existem controllers', 'There are no models': 'Não existem modelos', 'There are no modules': 'Não existem módulos', 'There are no plugins': 'There are no plugins', +'There are no private files': '', 'There are no static files': 'Não existem arquicos estáticos', 'There are no translators, only default language is supported': 'Não há traduções, somente a linguagem padrão é suportada', 'There are no views': 'Não existem visões', -'these files are served without processing, your images go here': 'Estes arquivos são servidos sem processamento, suas imagens ficam aqui', +'These files are not served, they are only available from within your app': 'These files are not served, they are only available from within your app', 'These files are served without processing, your images go here': 'These files are served without processing, your images go here', +'these files are served without processing, your images go here': 'Estes arquivos são servidos sem processamento, suas imagens ficam aqui', 'This is the %(filename)s template': 'Este é o template %(filename)s', 'Ticket': 'Ticket', 'Ticket ID': 'Ticket ID', @@ -277,11 +323,15 @@ 'TM': 'MR', 'to previous version.': 'para a versão anterior.', 'To create a plugin, name a file/folder plugin_[name]': 'Para criar um plugin, nomeio um arquivo/pasta como plugin_[nome]', +'toggle breakpoint': 'toggle breakpoint', +'Toggle comment': 'Toggle comment', +'Toggle Fullscreen': 'Toggle Fullscreen', 'Traceback': 'Traceback', 'translation strings for the application': 'textos traduzidos para a aplicação', 'Translation strings for the application': 'Translation strings for the application', 'try': 'tente', 'try something like': 'tente algo como', +'Try the mobile interface': 'Try the mobile interface', 'Unable to check for upgrades': 'Não é possível checar as atualizações', 'unable to create application "%s"': 'não é possível criar a aplicação "%s"', 'unable to delete file "%(filename)s"': 'não é possível criar o arquico "%(filename)s"', @@ -300,8 +350,10 @@ 'Update:': 'Atualizar:', 'upgrade web2py now': 'atualize o web2py agora', 'upload': 'upload', +'Upload': 'Upload', 'Upload & install packed application': 'Faça upload e instale uma aplicação empacotada', 'Upload a package:': 'Faça upload de um pacote:', +'Upload and install packed application': 'Upload and install packed application', 'upload application:': 'Fazer upload de uma aplicação:', 'Upload existing application': 'Faça upload de uma aplicação existente', 'upload file:': 'Enviar arquivo:', diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js index ffb6baa8..e6e62386 100644 --- a/applications/admin/static/js/web2py.js +++ b/applications/admin/static/js/web2py.js @@ -141,7 +141,7 @@ }, /* manage errors in forms */ manage_errors: function(target) { - $('.error', target).hide().slideDown('slow'); + $('div.error', target).hide().slideDown('slow'); }, after_ajax: function(xhr) { /* called whenever an ajax request completes */ @@ -684,19 +684,21 @@ } }); $.web2py.component_handler(target); + }, + main_hook : function() { + var flash = $('.flash'); + flash.hide(); + if(flash.html()) web2py.flash(flash.html()); + web2py.ajax_init(document); + web2py.event_handlers(); + web2py.a_handlers(); + web2py.form_handlers(); } } - /*end of functions */ /*main hook*/ - $(function() { - var flash = $('.flash'); - flash.hide(); - if(flash.html()) web2py.flash(flash.html()); - web2py.ajax_init(document); - web2py.event_handlers(); - web2py.a_handlers(); - web2py.form_handlers(); + $(function () { + web2py.main_hook(); }); })(jQuery); diff --git a/applications/admin/views/default/git_push.html b/applications/admin/views/default/git_push.html index 36722c9e..c8ad74d4 100644 --- a/applications/admin/views/default/git_push.html +++ b/applications/admin/views/default/git_push.html @@ -4,12 +4,10 @@ frm = form smt_button = frm.element(_type="submit") smt_button['_class'] = 'btn' smt_button['_style'] = 'margin-right:4px;' -ccl_button = frm.element(_type="button") -ccl_button['_class'] = 'btn' }}

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

{{=form}}
- \ No newline at end of file + diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js index ffb6baa8..e6e62386 100644 --- a/applications/examples/static/js/web2py.js +++ b/applications/examples/static/js/web2py.js @@ -141,7 +141,7 @@ }, /* manage errors in forms */ manage_errors: function(target) { - $('.error', target).hide().slideDown('slow'); + $('div.error', target).hide().slideDown('slow'); }, after_ajax: function(xhr) { /* called whenever an ajax request completes */ @@ -684,19 +684,21 @@ } }); $.web2py.component_handler(target); + }, + main_hook : function() { + var flash = $('.flash'); + flash.hide(); + if(flash.html()) web2py.flash(flash.html()); + web2py.ajax_init(document); + web2py.event_handlers(); + web2py.a_handlers(); + web2py.form_handlers(); } } - /*end of functions */ /*main hook*/ - $(function() { - var flash = $('.flash'); - flash.hide(); - if(flash.html()) web2py.flash(flash.html()); - web2py.ajax_init(document); - web2py.event_handlers(); - web2py.a_handlers(); - web2py.form_handlers(); + $(function () { + web2py.main_hook(); }); })(jQuery); diff --git a/applications/welcome/controllers/appadmin.py b/applications/welcome/controllers/appadmin.py index 3b78a019..50263b35 100644 --- a/applications/welcome/controllers/appadmin.py +++ b/applications/welcome/controllers/appadmin.py @@ -461,34 +461,24 @@ def ccache(): if value[0] < ram['oldest']: ram['oldest'] = value[0] ram['keys'].append((key, GetInHMS(time.time() - value[0]))) - folder = os.path.join(request.folder,'cache') - if not os.path.exists(folder): - os.mkdir(folder) - locker = open(os.path.join(folder, 'cache.lock'), 'a') - portalocker.lock(locker, portalocker.LOCK_EX) - disk_storage = shelve.open( - os.path.join(folder, 'cache.shelve')) - try: - for key, value in disk_storage.items(): - if isinstance(value, dict): - disk['hits'] = value['hit_total'] - value['misses'] - disk['misses'] = value['misses'] - try: - disk['ratio'] = disk['hits'] * 100 / value['hit_total'] - except (KeyError, ZeroDivisionError): - disk['ratio'] = 0 - else: - if hp: - disk['bytes'] += hp.iso(value[1]).size - disk['objects'] += hp.iso(value[1]).count - disk['entries'] += 1 - if value[0] < disk['oldest']: - disk['oldest'] = value[0] - disk['keys'].append((key, GetInHMS(time.time() - value[0]))) - finally: - portalocker.unlock(locker) - locker.close() - disk_storage.close() + + for key in cache.disk.storage: + value = cache.disk.storage[key] + if isinstance(value, dict): + disk['hits'] = value['hit_total'] - value['misses'] + disk['misses'] = value['misses'] + try: + disk['ratio'] = disk['hits'] * 100 / value['hit_total'] + except (KeyError, ZeroDivisionError): + disk['ratio'] = 0 + else: + if hp: + disk['bytes'] += hp.iso(value[1]).size + disk['objects'] += hp.iso(value[1]).count + disk['entries'] += 1 + if value[0] < disk['oldest']: + disk['oldest'] = value[0] + disk['keys'].append((key, GetInHMS(time.time() - value[0]))) total['entries'] = ram['entries'] + disk['entries'] total['bytes'] = ram['bytes'] + disk['bytes'] diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js index ffb6baa8..e6e62386 100644 --- a/applications/welcome/static/js/web2py.js +++ b/applications/welcome/static/js/web2py.js @@ -141,7 +141,7 @@ }, /* manage errors in forms */ manage_errors: function(target) { - $('.error', target).hide().slideDown('slow'); + $('div.error', target).hide().slideDown('slow'); }, after_ajax: function(xhr) { /* called whenever an ajax request completes */ @@ -684,19 +684,21 @@ } }); $.web2py.component_handler(target); + }, + main_hook : function() { + var flash = $('.flash'); + flash.hide(); + if(flash.html()) web2py.flash(flash.html()); + web2py.ajax_init(document); + web2py.event_handlers(); + web2py.a_handlers(); + web2py.form_handlers(); } } - /*end of functions */ /*main hook*/ - $(function() { - var flash = $('.flash'); - flash.hide(); - if(flash.html()) web2py.flash(flash.html()); - web2py.ajax_init(document); - web2py.event_handlers(); - web2py.a_handlers(); - web2py.form_handlers(); + $(function () { + web2py.main_hook(); }); })(jQuery); diff --git a/docs/dal.adapters.rst b/docs/dal.adapters.rst new file mode 100644 index 00000000..bef54380 --- /dev/null +++ b/docs/dal.adapters.rst @@ -0,0 +1,150 @@ +gluon.dal.adapters package +========================== + +Submodules +---------- + +gluon.dal.adapters.base module +------------------------------ + +.. automodule:: gluon.dal.adapters.base + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.couchdb module +--------------------------------- + +.. automodule:: gluon.dal.adapters.couchdb + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.cubrid module +-------------------------------- + +.. automodule:: gluon.dal.adapters.cubrid + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.db2 module +----------------------------- + +.. automodule:: gluon.dal.adapters.db2 + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.firebird module +---------------------------------- + +.. automodule:: gluon.dal.adapters.firebird + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.google module +-------------------------------- + +.. automodule:: gluon.dal.adapters.google + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.imap module +------------------------------ + +.. automodule:: gluon.dal.adapters.imap + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.informix module +---------------------------------- + +.. automodule:: gluon.dal.adapters.informix + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.ingres module +-------------------------------- + +.. automodule:: gluon.dal.adapters.ingres + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.mongo module +------------------------------- + +.. automodule:: gluon.dal.adapters.mongo + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.mssql module +------------------------------- + +.. automodule:: gluon.dal.adapters.mssql + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.mysql module +------------------------------- + +.. automodule:: gluon.dal.adapters.mysql + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.oracle module +-------------------------------- + +.. automodule:: gluon.dal.adapters.oracle + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.postgre module +--------------------------------- + +.. automodule:: gluon.dal.adapters.postgre + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.sapdb module +------------------------------- + +.. automodule:: gluon.dal.adapters.sapdb + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.sqlite module +-------------------------------- + +.. automodule:: gluon.dal.adapters.sqlite + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.adapters.teradata module +---------------------------------- + +.. automodule:: gluon.dal.adapters.teradata + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: gluon.dal.adapters + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/dal.helpers.rst b/docs/dal.helpers.rst new file mode 100644 index 00000000..bc146429 --- /dev/null +++ b/docs/dal.helpers.rst @@ -0,0 +1,38 @@ +gluon.dal.helpers package +========================= + +Submodules +---------- + +gluon.dal.helpers.classes module +-------------------------------- + +.. automodule:: gluon.dal.helpers.classes + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.helpers.methods module +-------------------------------- + +.. automodule:: gluon.dal.helpers.methods + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.helpers.regex module +------------------------------ + +.. automodule:: gluon.dal.helpers.regex + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: gluon.dal.helpers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/dal.rst b/docs/dal.rst index 5208ed87..90c1564e 100644 --- a/docs/dal.rst +++ b/docs/dal.rst @@ -1,6 +1,44 @@ +gluon.dal package +================= -:mod:`dal` Module ------------------ +Subpackages +----------- + +.. toctree:: + + dal.adapters + dal.helpers + +Submodules +---------- + +gluon.dal.base module +--------------------- + +.. automodule:: gluon.dal.base + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.connection module +--------------------------- + +.. automodule:: gluon.dal.connection + :members: + :undoc-members: + :show-inheritance: + +gluon.dal.objects module +------------------------ + +.. automodule:: gluon.dal.objects + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- .. automodule:: gluon.dal :members: diff --git a/gluon/contrib/websocket_messaging.py b/gluon/contrib/websocket_messaging.py index e0418d25..89a90897 100644 --- a/gluon/contrib/websocket_messaging.py +++ b/gluon/contrib/websocket_messaging.py @@ -70,8 +70,8 @@ Here is a complete sample web2py action: 'http://127.0.0.1:8888', form.vars.message, 'mykey', 'mygroup') return form -https is possible too using 'https://127.0.0.1:8888' instead of 'http://127.0.0.1:8888', -but needs to be started with +https is possible too using 'https://127.0.0.1:8888' instead of 'http://127.0.0.1:8888', but need to +be started with python gluon/contrib/websocket_messaging.py -k mykey -p 8888 -s keyfile.pem -c certfile.pem @@ -113,7 +113,7 @@ class PostHandler(tornado.web.RequestHandler): """ def post(self): if hmac_key and not 'signature' in self.request.arguments: - return None + self.send_error(401) if 'message' in self.request.arguments: message = self.request.arguments['message'][0] group = self.request.arguments.get('group', ['default'])[0] @@ -121,11 +121,9 @@ class PostHandler(tornado.web.RequestHandler): if hmac_key: signature = self.request.arguments['signature'][0] if not hmac.new(hmac_key, message).hexdigest() == signature: - return None + self.send_error(401) for client in listeners.get(group, []): client.write_message(message) - return None - return 'false' class TokenHandler(tornado.web.RequestHandler): @@ -136,15 +134,14 @@ class TokenHandler(tornado.web.RequestHandler): """ def post(self): if hmac_key and not 'message' in self.request.arguments: - return None + self.send_error(401) if 'message' in self.request.arguments: message = self.request.arguments['message'][0] if hmac_key: signature = self.request.arguments['signature'][0] if not hmac.new(hmac_key, message).hexdigest() == signature: - return None + self.send_error(401) tokens[message] = None - return None class DistributeHandler(tornado.websocket.WebSocketHandler): @@ -180,6 +177,12 @@ class DistributeHandler(tornado.websocket.WebSocketHandler): client.write_message('-' + self.name) print '%s:DISCONNECT from %s' % (time.time(), self.group) +# if your webserver is different from tornado server uncomment this +# or override using something more restrictive: +# http://tornado.readthedocs.org/en/latest/websocket.html#tornado.websocket.WebSocketHandler.check_origin +# def check_origin(self, origin): +# return True + if __name__ == "__main__": usage = __doc__ version = "" @@ -205,6 +208,16 @@ if __name__ == "__main__": default=False, dest='tokens', help='require tockens to join') + parser.add_option('-s', + '--sslkey', + default=False, + dest='keyfile', + help='require ssl keyfile full path') + parser.add_option('-c', + '--sslcert', + default=False, + dest='certfile', + help='require ssl certfile full path') (options, args) = parser.parse_args() hmac_key = options.hmac_key DistributeHandler.tokens = options.tokens @@ -213,6 +226,10 @@ if __name__ == "__main__": (r'/token', TokenHandler), (r'/realtime/(.*)', DistributeHandler)] application = tornado.web.Application(urls, auto_reload=True) - http_server = tornado.httpserver.HTTPServer(application) + if options.keyfile and options.certfile: + ssl_options = dict(certfile=options.certfile, keyfile=options.keyfile) + else: + ssl_options = None + http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options) http_server.listen(int(options.port), address=options.address) tornado.ioloop.IOLoop.instance().start() diff --git a/gluon/dal/adapters/__init__.py b/gluon/dal/adapters/__init__.py index 4bad5bc8..54927648 100644 --- a/gluon/dal/adapters/__init__.py +++ b/gluon/dal/adapters/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from .sqlite import SQLiteAdapter, SpatiaLiteAdapter, JDBCSQLiteAdapter from .mysql import MySQLAdapter -from .postgre import PostgreSQLAdapter, NewPostgreSQLAdapter, JDBCPostgreSQLAdapter +from .postgres import PostgreSQLAdapter, NewPostgreSQLAdapter, JDBCPostgreSQLAdapter from .oracle import OracleAdapter from .mssql import MSSQLAdapter, MSSQL2Adapter, MSSQL3Adapter, MSSQL4Adapter, \ VerticaAdapter, SybaseAdapter diff --git a/gluon/dal/adapters/google.py b/gluon/dal/adapters/google.py index ea64ed87..86c3ef3c 100644 --- a/gluon/dal/adapters/google.py +++ b/gluon/dal/adapters/google.py @@ -515,24 +515,26 @@ class GoogleDatastoreAdapter(NoSQLAdapter): """ This is the GAE version of select. Some notes to consider: - db['_lastsql'] is not set because there is not SQL statement string - for a GAE query + for a GAE query - 'nativeRef' is a magical fieldname used for self references on GAE - optional attribute 'projection' when set to True will trigger - use of the GAE projection queries. note that there are rules for - what is accepted imposed by GAE: each field must be indexed, - projection queries cannot contain blob or text fields, and you - cannot use == and also select that same field. see https://developers.google.com/appengine/docs/python/datastore/queries#Query_Projection + use of the GAE projection queries. note that there are rules for + what is accepted imposed by GAE: each field must be indexed, + projection queries cannot contain blob or text fields, and you + cannot use == and also select that same field. + see https://developers.google.com/appengine/docs/python/datastore/queries#Query_Projection - optional attribute 'filterfields' when set to True web2py will only - parse the explicitly listed fields into the Rows object, even though - all fields are returned in the query. This can be used to reduce - memory usage in cases where true projection queries are not - usable. + parse the explicitly listed fields into the Rows object, even though + all fields are returned in the query. This can be used to reduce + memory usage in cases where true projection queries are not + usable. - optional attribute 'reusecursor' allows use of cursor with queries - that have the limitby attribute. Set the attribute to True for the - first query, set it to the value of db['_lastcursor'] to continue - a previous query. The user must save the cursor value between - requests, and the filters must be identical. It is up to the user - to follow google's limitations: https://developers.google.com/appengine/docs/python/datastore/queries#Query_Cursors + that have the limitby attribute. Set the attribute to True for the + first query, set it to the value of db['_lastcursor'] to continue + a previous query. The user must save the cursor value between + requests, and the filters must be identical. It is up to the user + to follow google's limitations: + https://developers.google.com/appengine/docs/python/datastore/queries#Query_Cursors """ (items, tablename, fields) = self.select_raw(query,fields,attributes) diff --git a/gluon/dal/adapters/imap.py b/gluon/dal/adapters/imap.py index 922698c4..a63ac61b 100644 --- a/gluon/dal/adapters/imap.py +++ b/gluon/dal/adapters/imap.py @@ -12,7 +12,6 @@ from .base import NoSQLAdapter class IMAPAdapter(NoSQLAdapter): - drivers = ('imaplib',) """ IMAP server adapter @@ -44,29 +43,31 @@ class IMAPAdapter(NoSQLAdapter): Here is a list of supported fields: - Field Type Description - ################################################################ - uid string - answered boolean Flag - created date - content list:string A list of dict text or html parts - to string - cc string - bcc string - size integer the amount of octets of the message* - deleted boolean Flag - draft boolean Flag - flagged boolean Flag - sender string - recent boolean Flag - seen boolean Flag - subject string - mime string The mime header declaration - email string The complete RFC822 message** - attachments Each non text part as dict - encoding string The main detected encoding - - *At the application side it is measured as the length of the RFC822 + =========== ============== =========== + Field Type Description + =========== ============== =========== + uid string + answered boolean Flag + created date + content list:string A list of dict text or html parts + to string + cc string + bcc string + size integer the amount of octets of the message* + deleted boolean Flag + draft boolean Flag + flagged boolean Flag + sender string + recent boolean Flag + seen boolean Flag + subject string + mime string The mime header declaration + email string The complete RFC822 message (*) + attachments list Each non text part as dict + encoding string The main detected encoding + =========== ============== =========== + + (*) At the application side it is measured as the length of the RFC822 message string WARNING: As row id's are mapped to email sequence numbers, @@ -141,7 +142,7 @@ class IMAPAdapter(NoSQLAdapter): imapdb(q).select(imapdb.INBOX.sender, imapdb.INBOX.subject) """ - + drivers = ('imaplib',) types = { 'string': str, 'text': str, diff --git a/gluon/dal/adapters/postgre.py b/gluon/dal/adapters/postgres.py similarity index 96% rename from gluon/dal/adapters/postgre.py rename to gluon/dal/adapters/postgres.py index c675f39e..7e81d6c8 100644 --- a/gluon/dal/adapters/postgre.py +++ b/gluon/dal/adapters/postgres.py @@ -173,9 +173,12 @@ class PostgreSQLAdapter(BaseAdapter): def try_json(self): # check JSON data type support # (to be added to after_connection) - if self.driver_name == "pg8000": - supports_json = self.connection.server_version >= "9.2.0" - elif (self.driver_name == "psycopg2" and + + # until pg8000 supports json, leave this commented + #if self.driver_name == "pg8000": + # supports_json = self.connection.server_version >= "9.2.0" + + if (self.driver_name == "psycopg2" and self.driver.__version__ >= "2.0.12"): supports_json = self.connection.server_version >= 90200 elif self.driver_name == "zxJDBC": @@ -211,14 +214,6 @@ class PostgreSQLAdapter(BaseAdapter): return '(%s ~ %s)' % (self.expand(first), self.expand(second,'string')) - def STARTSWITH(self,first,second): - return '(%s LIKE %s)' % (self.expand(first), - self.expand(second+'%','string')) - - def ENDSWITH(self,first,second): - return '(%s LIKE %s)' % (self.expand(first), - self.expand('%'+second,'string')) - # GIS functions def ST_ASGEOJSON(self, first, second): diff --git a/gluon/dal/base.py b/gluon/dal/base.py index eb9c95ce..1f6b69db 100644 --- a/gluon/dal/base.py +++ b/gluon/dal/base.py @@ -234,6 +234,7 @@ class DAL(object): """ + Table = Table def __new__(cls, uri='sqlite://dummy.db', *args, **kwargs): if not hasattr(THREAD_LOCAL,'db_instances'): diff --git a/gluon/dal/objects.py b/gluon/dal/objects.py index c0cd0001..44730709 100644 --- a/gluon/dal/objects.py +++ b/gluon/dal/objects.py @@ -1208,7 +1208,7 @@ class Expression(object): return Query(db, op, self, value) def ilike(self, value): - return self.like(case_sensitive=False) + return self.like(value, case_sensitive=False) def regexp(self, value): db = self.db diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index a85b256a..728c4bee 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -10,8 +10,7 @@ Cross-site scripting (XSS) defense ----------------------------------- """ - -from htmllib import HTMLParser +from HTMLParser import HTMLParser from cgi import escape from urlparse import urlparse from formatter import AbstractFormatter @@ -48,11 +47,10 @@ class XssCleaner(HTMLParser): ], allowed_attributes={'a': ['href', 'title'], 'img': ['src', 'alt' ], 'blockquote': ['type']}, - fmt=AbstractFormatter, strip_disallowed=False ): - HTMLParser.__init__(self, fmt) + HTMLParser.__init__(self) self.result = '' self.open_tags = [] self.permitted_tags = [i for i in permitted_tags if i[-1] != '/'] @@ -77,7 +75,7 @@ class XssCleaner(HTMLParser): def handle_charref(self, ref): if self.in_disallowed: return - elif len(ref) < 7 and ref.isdigit(): + elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe self.result += '&#%s;' % ref else: self.result += xssescape('&#%s' % ref) @@ -99,8 +97,7 @@ class XssCleaner(HTMLParser): def handle_starttag( self, tag, - method, - attrs, + attrs ): if tag not in self.permitted_tags: if self.strip_disallowed: @@ -130,7 +127,7 @@ class XssCleaner(HTMLParser): self.result += bt self.open_tags.insert(0, tag) - def handle_endtag(self, tag, attrs): + def handle_endtag(self, tag): bracketed = '' % tag if tag not in self.permitted_tags: if self.strip_disallowed: @@ -141,12 +138,6 @@ class XssCleaner(HTMLParser): self.result += bracketed self.open_tags.remove(tag) - def unknown_starttag(self, tag, attributes): - self.handle_starttag(tag, None, attributes) - - def unknown_endtag(self, tag): - self.handle_endtag(tag, None) - def url_is_acceptable(self, url): """ Accepts relative, absolute, and mailto urls diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 98d4395a..657ef814 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -14,13 +14,18 @@ Holds: """ +import datetime +import urllib +import re +import cStringIO + import os -from gluon.http import HTTP -from gluon.html import XmlComponent -from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG, SCRIPT, P +from gluon.http import HTTP, redirect +from gluon.html import XmlComponent, truncate_string +from gluon.html import XML, SPAN, TAG, A, DIV, CAT, UL, LI, TEXTAREA, BR, IMG from gluon.html import FORM, INPUT, LABEL, OPTION, SELECT, COL, COLGROUP -from gluon.html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE, DEFAULT_PASSWORD_DISPLAY -from gluon.html import URL, truncate_string, FIELDSET +from gluon.html import TABLE, THEAD, TBODY, TR, TD, TH, STYLE, SCRIPT +from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY from gluon.dal import DAL, Field from gluon.dal.base import DEFAULT from gluon.dal.objects import Table, Row, Expression @@ -29,16 +34,12 @@ from gluon.dal.helpers.methods import smart_query, bar_encode, sqlhtml_validator from gluon.dal.helpers.classes import Reference, SQLCustomType from gluon.storage import Storage from gluon.utils import md5_hash -from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \ - IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG +from gluon.validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE +from gluon.validators import IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE +from gluon.validators import IS_STRONG import gluon.serializers as serializers -import datetime -import urllib -import re -import cStringIO from gluon.globals import current -from gluon.http import redirect try: import gluon.settings as settings @@ -49,7 +50,7 @@ widget_class = re.compile('^\w*') def add_class(a, b): - return a+' '+b if a else b + return a + ' ' + b if a else b def represent(field, value, record): @@ -85,11 +86,11 @@ def show_if(cond): if not cond: return None base = "%s_%s" % (cond.first.tablename, cond.first.name) - if ((cond.op.__name__ == 'EQ' and cond.second == True) or - (cond.op.__name__ == 'NE' and cond.second == False)): + if ((cond.op.__name__ == 'EQ' and cond.second is True) or + (cond.op.__name__ == 'NE' and cond.second is False)): return base, ":checked" - if ((cond.op.__name__ == 'EQ' and cond.second == False) or - (cond.op.__name__ == 'NE' and cond.second == True)): + if ((cond.op.__name__ == 'EQ' and cond.second is False) or + (cond.op.__name__ == 'NE' and cond.second is True)): return base, ":not(:checked)" if cond.op.__name__ == 'EQ': return base, "[value='%s']" % cond.second @@ -305,7 +306,8 @@ class ListWidget(StringWidget): _class = 'string' requires = field.requires if isinstance( field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None - if isinstance(value, str): value = [value] + if isinstance(value, str): + value = [value] nvalue = value or [''] items = [LI(INPUT(_id=_id, _class=_class, _name=_name, value=v, hideerror=k < len(nvalue) - 1, @@ -351,7 +353,6 @@ class RadioWidget(OptionsWidget): else: value = str(value) - attr = cls._attributes(field, {}, **attributes) attr['_class'] = add_class(attr.get('_class'), 'web2py_radiowidget') @@ -626,7 +627,7 @@ class AutocompleteWidget(object): self.help_fields = help_fields or [] self.help_string = help_string if self.help_fields and not self.help_string: - self.help_string = ' '.join('%%(%s)s'%f.name + self.help_string = ' '.join('%%(%s)s' % f.name for f in self.help_fields) self.request = request @@ -900,7 +901,7 @@ def formstyle_bootstrap3_inline_factory(col_label_size=3): label['_for'] = None label.insert(0, controls) _controls = DIV(DIV(label, _help, _class="checkbox"), - _class="%s %s" % (offset_class, col_class)) + _class="%s %s" % (offset_class, col_class)) label = '' elif isinstance(controls, SELECT): controls.add_class('form-control') @@ -1132,7 +1133,7 @@ class SQLFORM(FORM): if fieldname.find('.') >= 0: continue - field = (self.table[fieldname] if fieldname in self.table.fields + field = (self.table[fieldname] if fieldname in self.table.fields else self.extra_fields[fieldname]) comment = None @@ -1184,7 +1185,7 @@ class SQLFORM(FORM): cond = readonly or \ (not ignore_rw and not field.writable and field.readable) - if default is not None and not cond: + if default is not None and not cond: default = field.formatter(default) dspval = default @@ -1192,10 +1193,6 @@ class SQLFORM(FORM): if cond: - # ## if field.re field.requires = sqlhtml_validators(field) field.requires = sqlhtml_validators(field)present is available else - # ## ignore blob and preview uploaded images - # ## format everything else - if field.represent: inp = represent(field, default, record) elif field.type in ['blob']: @@ -1272,7 +1269,7 @@ class SQLFORM(FORM): (olname.replace('.', '__') + SQLFORM.ID_ROW_SUFFIX, '', widget, col3.get(olname, ''))) self.custom.linkto[olname.replace('.', '__')] = widget -# + # # when deletable, add delete? checkbox self.custom.delete = self.custom.deletable = '' @@ -1291,16 +1288,15 @@ class SQLFORM(FORM): ) xfields.append( (self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_ROW_SUFFIX, - LABEL( + LABEL( T(delete_label), sep, _for=self.FIELDKEY_DELETE_RECORD, - _id=self.FIELDKEY_DELETE_RECORD + \ - SQLFORM.ID_LABEL_SUFFIX), + _id=self.FIELDKEY_DELETE_RECORD + + SQLFORM.ID_LABEL_SUFFIX), widget, col3.get(self.FIELDKEY_DELETE_RECORD, ''))) self.custom.delete = self.custom.deletable = widget - # when writable, add submit button self.custom.submit = '' if not readonly: @@ -1480,7 +1476,7 @@ class SQLFORM(FORM): # that does not pass validation, yet it should be deleted for fieldname in self.fields: - field = (self.table[fieldname] + field = (self.table[fieldname] if fieldname in self.table.fields else self.extra_fields[fieldname]) ### this is a workaround! widgets should always have default not None! @@ -1995,6 +1991,16 @@ class SQLFORM(FORM): details = details and not groupby rows = None + # see issue 1980. Basically we can have keywords in get_vars + # (i.e. when the search term is propagated through page=2&keywords=abc) + # but if there is keywords in post_vars (i.e. POSTing a search request) + # the one in get_vars should be replaced by the new one + keywords = '' + if 'keywords' in request.post_vars: + keywords = request.post_vars.keywords + elif 'keywords' in request.get_vars: + keywords = request.get_vars.keywords + def fetch_count(dbset): ##FIXME for google:datastore cache_count is ignored ## if it's not an integer @@ -2051,7 +2057,7 @@ class SQLFORM(FORM): '/'.join(str(a) for a in args) == '/'.join(request.args) or URL.verify(request, user_signature=user_signature, hash_vars=False) or - (request.args(len(args))=='view' and not logged)): + (request.args(len(args))=='view' and not logged)): session.flash = T('not authorized') redirect(referrer) @@ -2096,8 +2102,8 @@ class SQLFORM(FORM): else: fields = [] columns = [] - filter1 = lambda f:isinstance(f, Field) - filter2 = lambda f:isinstance(f, Field) and f.readable + filter1 = lambda f: isinstance(f, Field) + filter2 = lambda f: isinstance(f, Field) and f.readable for table in tables: fields += filter(filter1, table) columns += filter(filter2, table) @@ -2111,15 +2117,18 @@ class SQLFORM(FORM): if groupby is None: field_id = tables[0]._id elif groupby and isinstance(groupby, Field): - field_id = groupby #take the field passed as groupby + #take the field passed as groupby + field_id = groupby elif groupby and isinstance(groupby, Expression): - field_id = groupby.first #take the first groupby field - while not(isinstance(field_id, Field)): # Navigate to the first Field of the expression + #take the first groupby field + field_id = groupby.first + while not(isinstance(field_id, Field)): + # Navigate to the first Field of the expression field_id = field_id.first table = field_id.table tablename = table._tablename if not any(str(f) == str(field_id) for f in fields): - fields = [f for f in fields]+[field_id] + fields = [f for f in fields] + [field_id] if upload == '': upload = lambda filename: url(args=['download', filename]) if request.args(-2) == 'download': @@ -2283,8 +2292,10 @@ class SQLFORM(FORM): expcolumns = [str(f) for f in columns] selectable_columns = [str(f) for f in columns if not isinstance(f, Field.Virtual)] if export_type.endswith('with_hidden_cols'): - #expcolumns = [] start with the visible columns, which includes visible virtual fields - selectable_columns = [] #like expcolumns but excluding virtual + # expcolumns = [] start with the visible columns, which + # includes visible virtual fields + selectable_columns = [] + #like expcolumns but excluding virtual for table in tables: for field in table: if field.readable and field.tablename in tablenames: @@ -2292,19 +2303,21 @@ class SQLFORM(FORM): expcolumns.append(str(field)) if not(isinstance(field, Field.Virtual)): selectable_columns.append(str(field)) - #look for virtual fields not displayed (and virtual method fields to be added here?) + #look for virtual fields not displayed (and virtual method + #fields to be added here?) for (field_name, field) in table.iteritems(): if isinstance(field, Field.Virtual) and not str(field) in expcolumns: expcolumns.append(str(field)) if export_type in exportManager and exportManager[export_type]: - if request.vars.keywords: + if keywords: try: - #the query should be constructed using searchable fields but not virtual fields + #the query should be constructed using searchable + #fields but not virtual fields 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]) dbset = dbset(SQLFORM.build_query( - sfields, request.vars.get('keywords', ''))) + sfields, keywords)) rows = dbset.select(left=left, orderby=orderby, cacheable=True, *selectable_columns) except Exception, e: @@ -2316,7 +2329,8 @@ class SQLFORM(FORM): value = exportManager[export_type] clazz = value[0] if hasattr(value, '__getitem__') else value - rows.colnames = expcolumns # expcolumns is all cols to be exported including virtual fields + # expcolumns is all cols to be exported including virtual fields + rows.colnames = expcolumns oExp = clazz(rows) filename = '.'.join(('rows', oExp.file_ext)) response.headers['Content-Type'] = oExp.content_type @@ -2330,8 +2344,7 @@ class SQLFORM(FORM): elif not request.vars.records: request.vars.records = [] - session['_web2py_grid_referrer_' + formname] = \ - url2(vars=request.get_vars) + session['_web2py_grid_referrer_' + formname] = url2(vars=request.get_vars) console = DIV(_class='web2py_console %(header)s %(cornertop)s' % ui) error = None if create: @@ -2357,7 +2370,7 @@ class SQLFORM(FORM): sfields_id = '%s_query_panel' % prefix skeywords_id = '%s_keywords' % prefix search_widget = lambda sfield, url: CAT(FORM( - INPUT(_name='keywords', _value=request.vars.keywords, + INPUT(_name='keywords', _value=keywords, _id=skeywords_id,_class='form-control', _onfocus="jQuery('#%s').change();jQuery('#%s').slideDown();" % (spanel_id, sfields_id) if advanced_search else '' ), @@ -2368,7 +2381,6 @@ class SQLFORM(FORM): form = search_widget and search_widget(sfields, url()) or '' console.append(add) console.append(form) - keywords = request.vars.get('keywords', '') try: if callable(searchable): subquery = searchable(sfields, keywords) @@ -2428,7 +2440,7 @@ class SQLFORM(FORM): elif key == ordermatch[1:]: marker = sorter_icons[1] header = A(header, marker, _href=url(vars=dict( - keywords=request.vars.keywords or '', + keywords=keywords, order=key)), cid=request.cid) headcols.append(TH(header, _class=ui.get('default'))) @@ -2466,17 +2478,22 @@ class SQLFORM(FORM): if paginate and dbset._db._adapter.dbengine == 'google:datastore': cursor = request.vars.cursor or True limitby = (0, paginate) - try: page = int(request.vars.page or 1)-1 - except ValueError: page = 0 - elif paginate and paginate NPAGES + 1: @@ -2660,7 +2689,8 @@ class SQLFORM(FORM): _style='width:100%;overflow-x:auto;-ms-overflow-x:scroll') if selectable: if not callable(selectable): - #now expect that selectable and related parameters are iterator (list, tuple, etc) + #now expect that selectable and related parameters are + #iterator (list, tuple, etc) inputs = [] for i, submit_info in enumerate(selectable): submit_text = submit_info[0] @@ -2708,7 +2738,7 @@ class SQLFORM(FORM): link = url2(vars=dict( order=request.vars.order or '', _export_type=k, - keywords=request.vars.keywords or '')) + keywords=keywords or '')) export_links.append(A(T(label), _href=link, _title=title, _class='btn btn-default')) export_menu = \ DIV(T('Export:'), _class="w2p_export_menu", *export_links) @@ -2797,7 +2827,7 @@ class SQLFORM(FORM): elif callable(table._format): return table._format(row) else: - return '#'+str(row.id) + return '#' + str(row.id) try: nargs = len(args) + 1 previous_tablename, previous_fieldname, previous_id = \ @@ -2876,7 +2906,7 @@ class SQLFORM(FORM): opts = [OPTION(T('References')+':', _value='')] linked = [] if linked_tables: - for item in linked_tables: + for item in linked_tables: tb = None if isinstance(item, Table) and item._tablename in check: tablename = item._tablename @@ -3001,12 +3031,16 @@ class SQLTABLE(TABLE): return REGEX_TABLE_DOT_FIELD = sqlrows.db._adapter.REGEX_TABLE_DOT_FIELD if not columns: - columns = [c for c in sqlrows.colnames if REGEX_TABLE_DOT_FIELD.match(c)] + columns = list(sqlrows.colnames) if headers == 'fieldname:capitalize': headers = {} for c in columns: - (t, f) = REGEX_TABLE_DOT_FIELD.match(c).groups() - headers[t + '.' + f] = f.replace('_', ' ').title() + tfmatch=REGEX_TABLE_DOT_FIELD.match(c) + if tfmatch: + (t, f) = REGEX_TABLE_DOT_FIELD.match(c).groups() + headers[t + '.' + f] = f.replace('_', ' ').title() + else: + headers[c]=c elif headers == 'labels': headers = {} for c in columns: @@ -3025,7 +3059,7 @@ class SQLTABLE(TABLE): headers = {} else: for c in columns: # new implement dict - c = '.'.join(REGEX_TABLE_DOT_FIELD.match(c).groups()) + c = str(c) if isinstance(headers.get(c, c), dict): coldict = headers.get(c, c) attrcol = dict() diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 3ce59562..451d23bc 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -44,6 +44,9 @@ ALLOWED_DATATYPES = [ 'json', ] +IS_POSTGRESQL = 'postgres' in DEFAULT_URI + + def setUpModule(): pass @@ -404,6 +407,13 @@ class TestLike(unittest.TestCase): self.assertEqual(db(db.tt.aa.upper().like('A%')).count(), 1) self.assertEqual(db(db.tt.aa.upper().like('%B%')).count(),1) self.assertEqual(db(db.tt.aa.upper().like('%C')).count(), 1) + + # startswith endswith tests + self.assertEqual(db(db.tt.aa.startswith('a')).count(), 1) + self.assertEqual(db(db.tt.aa.endswith('c')).count(), 1) + self.assertEqual(db(db.tt.aa.startswith('c')).count(), 0) + self.assertEqual(db(db.tt.aa.endswith('a')).count(), 0) + db.tt.drop() db.define_table('tt', Field('aa', 'integer')) self.assertEqual(db.tt.insert(aa=1111111111), 1) @@ -1528,6 +1538,42 @@ class TestQuotesByDefault(unittest.TestCase): def testme(self): return + +class TestGis(unittest.TestCase): + + def testGeometry(self): + from gluon.dal import geoPoint, geoLine, geoPolygon + if not IS_POSTGRESQL: return + db = DAL(DEFAULT_URI, check_reserved=['all'], ignore_field_case=False) + t0 = db.define_table('t0', Field('point', 'geometry()')) + t1 = db.define_table('t1', Field('line', 'geometry(public, 4326, 2)')) + t2 = db.define_table('t2', Field('polygon', 'geometry(public, 4326, 2)')) + t0.insert(point=geoPoint(1,1)) + text = db(db.t0.id).select(db.t0.point.st_astext()).first()[db.t0.point.st_astext()] + self.assertEqual(text, "POINT(1 1)") + t1.insert(line=geoLine((1,1),(2,2))) + text = db(db.t1.id).select(db.t1.line.st_astext()).first()[db.t1.line.st_astext()] + self.assertEqual(text, "LINESTRING(1 1,2 2)") + t2.insert(polygon=geoPolygon((0,0),(2,0),(2,2),(0,2),(0,0))) + text = db(db.t2.id).select(db.t2.polygon.st_astext()).first()[db.t2.polygon.st_astext()] + self.assertEqual(text, "POLYGON((0 0,2 0,2 2,0 2,0 0))") + query = t0.point.st_intersects(geoLine((0,0),(2,2))) + output = db(query).select(db.t0.point).first()[db.t0.point] + self.assertEqual(output, "POINT(1 1)") + query = t2.polygon.st_contains(geoPoint(1,1)) + n = db(query).count() + self.assertEqual(n, 1) + x=t0.point.st_x() + y=t0.point.st_y() + point = db(t0.id).select(x, y).first() + self.assertEqual(point[x], 1) + self.assertEqual(point[y], 1) + t0.drop() + t1.drop() + t2.drop() + return + + if __name__ == '__main__': unittest.main() tearDownModule() diff --git a/gluon/tests/test_languages.py b/gluon/tests/test_languages.py index 13f13a9d..60172fe9 100644 --- a/gluon/tests/test_languages.py +++ b/gluon/tests/test_languages.py @@ -35,6 +35,10 @@ try: #due to http://bugs.python.org/issue10845, testing multiprocessing in python is impossible if sys.platform.startswith('win'): MP_WORKING = 0 + #multiprocessing is also not available on GAE. Since tests randomly + #fail, let's not make them on it too + if 'datastore' in os.getenv('DB', ''): + MP_WORKING = 0 except ImportError: pass diff --git a/gluon/tools.py b/gluon/tools.py index a5ec6ed7..5d44d015 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -3011,15 +3011,13 @@ class Auth(object): if self.settings.prevent_password_reset_attacks: key = request.vars.key - if not key and len(request.args)>1: - key = request.args[-1] if key: session._reset_password_key = key redirect(self.url(args='reset_password')) else: key = session._reset_password_key else: - key = request.vars.key or getarg(-1) + key = request.vars.key try: t0 = int(key.split('-')[0]) if time.time() - t0 > 60 * 60 * 24: @@ -3138,7 +3136,7 @@ class Auth(object): def email_reset_password(self, user): reset_password_key = str(int(time.time())) + '-' + web2py_uuid() link = self.url(self.settings.function, - args=('reset_password', reset_password_key), + args=('reset_password',), vars={'key': reset_password_key}, scheme=True) d = dict(user) d.update(dict(key=reset_password_key, link=link)) diff --git a/handlers/web2py_on_gevent.py b/handlers/web2py_on_gevent.py index 701e05e2..62be419c 100644 --- a/handlers/web2py_on_gevent.py +++ b/handlers/web2py_on_gevent.py @@ -5,10 +5,10 @@ import sys import os import optparse -if '__file__' in globals(): - path = os.path.dirname(os.path.abspath(__file__)) -elif hasattr(sys, 'frozen'): +if hasattr(sys, 'frozen'): path = os.path.dirname(os.path.abspath(sys.executable)) +elif '__file__' in globals(): + path = os.path.dirname(os.path.abspath(__file__)) else: path = os.getcwd() os.chdir(path) diff --git a/scripts/sessions2trash.py b/scripts/sessions2trash.py index e07e731b..e98dd9cc 100755 --- a/scripts/sessions2trash.py +++ b/scripts/sessions2trash.py @@ -32,7 +32,6 @@ from __future__ import with_statement import sys import os -print os.path.join(*__file__.split(os.sep)[:-2] or ['.']) sys.path.append(os.path.join(*__file__.split(os.sep)[:-2] or ['.'])) from gluon import current diff --git a/scripts/setup-web2py-centos7.sh b/scripts/setup-web2py-centos7.sh new file mode 100644 index 00000000..340ac05d --- /dev/null +++ b/scripts/setup-web2py-centos7.sh @@ -0,0 +1,302 @@ +echo "This script will: +1) Install modules needed to run web2py on Fedora and CentOS/RHEL +2) Install Python 2.6 to /opt and recompile wsgi if not provided +2) Install web2py in /opt/web-apps/ +3) Configure SELinux and iptables +5) Create a self signed ssl certificate +6) Setup web2py with mod_wsgi +7) Create virtualhost entries so that web2py responds for '/' +8) Restart Apache. + +You should probably read this script before running it. + +Although SELinux permissions changes have been made, +further SELinux changes will be required for your personal +apps. (There may also be additional changes required for the +bundled apps.) As a last resort, SELinux can be disabled. + +A simple iptables configuration has been applied. You may +want to review it to verify that it meets your needs. + +Finally, if you require a proxy to access the Internet, please +set up your machine to do so before running this script. + +(author: berubejd) + +Press ENTER to continue...[ctrl+C to abort]" + +read CONFIRM + +#!/bin/bash + +# (modified for centos7: Dragan (spamperakojotgenije@gmail.com) + +### +### Phase 0 - This may get messy. Lets work from a temporary directory +### + +current_dir=`pwd` + +if [ -d /tmp/setup-web2py/ ]; then + mv /tmp/setup-web2py/ /tmp/setup-web2py.old/ +fi + +mkdir -p /tmp/setup-web2py +cd /tmp/setup-web2py + +### +### Phase 1 - Requirements installation +### + +echo +echo " - Installing packages" +echo + +# Verify packages are up to date +yum update + +# Install required packages +yum install httpd mod_ssl mod_wsgi wget python + +### +### Phase 2 - Install web2py +### + +echo +echo " - Downloading, installing, and starting web2py" +echo + +# Create web-apps directory, if required +if [ ! -d "/opt/web-apps" ]; then + mkdir -p /opt/web-apps + + chmod 755 /opt + chmod 755 /opt/web-apps +fi + +cd /opt/web-apps + +# Download web2py +if [ -e web2py_src.zip* ]; then + rm web2py_src.zip* +fi + +wget http://web2py.com/examples/static/web2py_src.zip +unzip web2py_src.zip +mv web2py/handlers/wsgihandler.py web2py/wsgihandler.py +chown -R apache:apache web2py + +### +### Phase 3 - Setup SELinux context +### +### SELinux doesn't behave well with web2py, for details +### see https://groups.google.com/forum/?fromgroups#!searchin/web2py/selinux/web2py/_thPGA9YhK4/dSnvF3D_lswJ +### +### For now you'll have to disable SELinux + + +# Allow http_tmp_exec required for wsgi +RETV=`setsebool -P httpd_tmp_exec on > /dev/null 2>&1; echo $?` +if [ ! ${RETV} -eq 0 ]; then + # CentOS doesn't support httpd_tmp_exec + cd /tmp/setup-web2py + + # Create the SELinux policy +cat > httpd.te < /etc/httpd/ssl/self_signed.key +openssl req -new -x509 -nodes -sha1 -days 365 -key /etc/httpd/ssl/self_signed.key > /etc/httpd/ssl/self_signed.cert +openssl x509 -noout -fingerprint -text < /etc/httpd/ssl/self_signed.cert > /etc/httpd/ssl/self_signed.info + +chmod 400 /etc/httpd/ssl/self_signed.* + +### +### Phase 6 - Configure Apache +### + +echo +echo " - Configure Apache to use mod_wsgi" +echo + +# Create config +if [ -e /etc/httpd/conf.d/welcome.conf ]; then + mv /etc/httpd/conf.d/welcome.conf /etc/httpd/conf.d/welcome.conf.disabled +fi + +cat > /etc/httpd/conf.d/default.conf < + WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1 + WSGIProcessGroup web2py + WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py + WSGIPassAuthorization On + + + AllowOverride None + Order Allow,Deny + Deny from all + + Require all granted + Allow from all + + + + AliasMatch ^/([^/]+)/static/(?:_[\d]+.[\d]+.[\d]+/)?(.*) /opt/web-apps/web2py/applications/\$1/static/\$2 + + + Options -Indexes + Order Allow,Deny + Allow from all + Require all granted + + + + Deny from all + + + + Deny from all + + + CustomLog /var/log/httpd/access_log common + ErrorLog /var/log/httpd/error_log + + + + SSLEngine on + SSLCertificateFile /etc/httpd/ssl/self_signed.cert + SSLCertificateKeyFile /etc/httpd/ssl/self_signed.key + + WSGIProcessGroup web2py + WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py + WSGIPassAuthorization On + + + AllowOverride None + Order Allow,Deny + Deny from all + + Require all granted + Allow from all + + + + AliasMatch ^/([^/]+)/static/(?:_[\d]+.[\d]+.[\d]+/)?(.*) /opt/web-apps/web2py/applications/\$1/static/\$2 + + + Options -Indexes + ExpiresActive On + ExpiresDefault "access plus 1 hour" + Order Allow,Deny + Allow from all + Require all granted + + + CustomLog /var/log/httpd/access_log common + ErrorLog /var/log/httpd/error_log + + +EOF + +# Fix wsgi socket locations +echo "WSGISocketPrefix run/wsgi" >> /etc/httpd/conf.d/wsgi.conf + +# Restart Apache to pick up changes +systemctl restart httpd.service + +### +### Phase 7 - Setup web2py admin password +### + +echo +echo " - Setup web2py admin password" +echo + +cd /opt/web-apps/web2py +sudo -u apache python -c "from gluon.main import save_password; save_password(raw_input('admin password: '),443)" + +### +### Phase 8 - Verify that required services start at boot +### + +/sbin/chkconfig iptables on +/sbin/chkconfig httpd on + +### +### Phase 999 - Done! +### + +# Change back to original directory +cd ${current_directory} + +echo " - Complete!" +echo diff --git a/web2py.py b/web2py.py index 59c02f9d..42ec1c22 100755 --- a/web2py.py +++ b/web2py.py @@ -4,10 +4,10 @@ import os import sys -if '__file__' in globals(): - path = os.path.dirname(os.path.abspath(__file__)) -elif hasattr(sys, 'frozen'): +if hasattr(sys, 'frozen'): path = os.path.dirname(os.path.abspath(sys.executable)) # for py2exe +elif '__file__' in globals(): + path = os.path.dirname(os.path.abspath(__file__)) else: # should never happen path = os.getcwd() os.chdir(path)