Allow deploying to pythonanywhere from the web2py admin that you're running locally.

This commit is contained in:
Leonel Câmara
2015-09-08 00:51:09 +01:00
parent d4270373e1
commit 125cbd93a0
4 changed files with 669 additions and 377 deletions
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
import base64
import os
import re
import gzip
import tarfile
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from xmlrpclib import ProtocolError
from gluon.contrib.simplejsonrpc import ServerProxy
def deploy():
response.title = T('Deploy to pythonanywhere')
return {}
def crossdomainproxy():
if not request.vars:
raise HTTP(400)
import urllib, urllib2
url = 'https://www.pythonanywhere.com/api/web2py/create_account'
data = urllib.urlencode(request.vars)
req = urllib2.Request(url, data)
try:
reply = urllib2.urlopen(req)
except urllib2.HTTPError as error:
if error.code == 400:
reply = error
else:
raise
response.headers['Content-Type'] = 'application/json'
return reply.read()
def list_apps():
""" Get a list of apps both remote and local """
if not request.vars.username or not request.vars.password:
raise HTTP(400)
client = ServerProxy('https://%(username)s:%(password)s@%(username)s.pythonanywhere.com/admin/webservices/call/jsonrpc' % request.vars)
regex = re.compile('^\w+$')
local = [f for f in os.listdir(apath(r=request)) if regex.match(f)]
try:
pythonanywhere = client.list_apps()
except ProtocolError as error:
raise HTTP(error.errcode)
return response.json({'local': local, 'pythonanywhere': pythonanywhere})
def bulk_install():
""" Install a list of apps """
def b64pack(app):
"""
Given an app's name, return the base64 representation of its packed version.
"""
folder = apath(app, r=request)
tmpfile = StringIO()
tar = tarfile.TarFile(fileobj=tmpfile, mode='w')
try:
filenames = listdir(folder, '^[\w\.\-]+$', add_dirs=True,
exclude_content_from=['cache', 'sessions', 'errors'])
for fname in filenames:
tar.add(os.path.join(folder, fname), fname, False)
finally:
tar.close()
tmpfile.seek(0)
gzfile = StringIO()
w2pfp = gzip.GzipFile(fileobj=gzfile, mode='wb')
w2pfp.write(tmpfile.read())
w2pfp.close()
gzfile.seek(0)
return base64.b64encode(gzfile.read())
request.vars.apps = request.vars['apps[]']
if not request.vars.apps or not request.vars.username or not request.vars.password:
raise HTTP(400)
if not isinstance(request.vars.apps, list):
request.vars.apps = [request.vars.apps] # Only one app selected
client = ServerProxy('https://%(username)s:%(password)s@%(username)s.pythonanywhere.com/admin/webservices/call/jsonrpc' % request.vars)
for app in request.vars.apps:
try:
client.install(app, app+'.w2p', b64pack(app))
except ProtocolError as error:
raise HTTP(error.errcode)
return response.json({'status': 'ok'})
+27
View File
@@ -17,6 +17,7 @@
'A new version of web2py is available: %s': 'Está disponível uma nova versão do web2py: %s', 'A new version of web2py is available: %s': 'Está disponível uma nova versão do web2py: %s',
'About': 'sobre', 'About': 'sobre',
'About application': 'Sobre a aplicação', 'About application': 'Sobre a aplicação',
'Accept Terms': 'Accept Terms',
'additional code for your application': 'código adicional para sua aplicação', 'additional code for your application': 'código adicional para sua aplicação',
'Additional code for your application': 'Código adicional para a sua aplicação', '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 no admin password': ' admin desabilitado por falta de senha definida',
@@ -50,6 +51,7 @@
'Autocomplete Python Code': 'Autocompletar Código Python', 'Autocomplete Python Code': 'Autocompletar Código Python',
'Available databases and tables': 'Bancos de dados e tabelas disponíveis', 'Available databases and tables': 'Bancos de dados e tabelas disponíveis',
'back': 'voltar', 'back': 'voltar',
'Begin': 'Begin',
'browse': 'buscar', 'browse': 'buscar',
'cache': 'cache', 'cache': 'cache',
'cache, errors and sessions cleaned': 'cache, erros e sessões eliminadas', 'cache, errors and sessions cleaned': 'cache, erros e sessões eliminadas',
@@ -112,11 +114,15 @@
'Deploy': 'publicar', 'Deploy': 'publicar',
'Deploy on Google App Engine': 'Publicar no Google App Engine', 'Deploy on Google App Engine': 'Publicar no Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift', 'Deploy to OpenShift': 'Deploy to OpenShift',
'Deploy to pythonanywhere': 'Deploy to pythonanywhere',
'Deploy to PythonAnywhere': 'Deploy to PythonAnywhere',
'Deployment Interface': 'Deployment Interface',
'Description': 'Descrição', 'Description': 'Descrição',
'design': 'modificar', 'design': 'modificar',
'DESIGN': 'Projeto', 'DESIGN': 'Projeto',
'Design for': 'Projeto de', 'Design for': 'Projeto de',
'Detailed traceback description': 'Detailed traceback description', 'Detailed traceback description': 'Detailed traceback description',
'details': 'details',
'direction: ltr': 'direção: ltr', 'direction: ltr': 'direção: ltr',
'Disable': 'Disable', 'Disable': 'Disable',
'docs': 'docs', 'docs': 'docs',
@@ -137,6 +143,7 @@
'Editing file': 'Editando arquivo', 'Editing file': 'Editando arquivo',
'Editing file "%s"': 'Editando arquivo "%s"', 'Editing file "%s"': 'Editando arquivo "%s"',
'Editing Language file': 'Editando arquivo de linguagem', 'Editing Language file': 'Editando arquivo de linguagem',
'Email Address': 'Email Address',
'Enterprise Web Framework': 'Framework web empresarial', 'Enterprise Web Framework': 'Framework web empresarial',
'Error': 'Erro', 'Error': 'Erro',
'Error logs for "%(app)s"': 'Logs de erro para "%(app)s"', 'Error logs for "%(app)s"': 'Logs de erro para "%(app)s"',
@@ -148,6 +155,7 @@
'Expand Abbreviation (html files only)': 'Expandir Abreviação (só para ficheiros html)', 'Expand Abbreviation (html files only)': 'Expandir Abreviação (só para ficheiros html)',
'export as csv file': 'exportar como arquivo CSV', 'export as csv file': 'exportar como arquivo CSV',
'exposes': 'expõe', 'exposes': 'expõe',
'exposes:': 'exposes:',
'extends': 'estende', 'extends': 'estende',
'failed to reload module': 'Falha ao recarregar o módulo', 'failed to reload module': 'Falha ao recarregar o módulo',
'failed to reload module because:': 'falha ao recarregar o módulo por:', 'failed to reload module because:': 'falha ao recarregar o módulo por:',
@@ -165,6 +173,7 @@
'Find Next': 'Localizar Seguinte', 'Find Next': 'Localizar Seguinte',
'Find Previous': 'Localizar Anterior', 'Find Previous': 'Localizar Anterior',
'First name': 'Nome', 'First name': 'Nome',
'Form has errors': 'Form has errors',
'Frames': 'Frames', 'Frames': 'Frames',
'Functions with no doctests will result in [passed] tests.': 'Funções sem doctests resultarão em testes [aceitos].', 'Functions with no doctests will result in [passed] tests.': 'Funções sem doctests resultarão em testes [aceitos].',
'graph model': 'graph model', 'graph model': 'graph model',
@@ -198,11 +207,16 @@
'Last name': 'Sobrenome', 'Last name': 'Sobrenome',
'Last saved on:': 'Salvo em:', 'Last saved on:': 'Salvo em:',
'License for': 'Licença para', 'License for': 'Licença para',
'lists by ticket': 'lists by ticket',
'Loading...': 'Loading...',
'loading...': 'carregando...', 'loading...': 'carregando...',
'Local Apps': 'Local Apps',
'locals': 'locals', 'locals': 'locals',
'Login': 'Entrar', 'Login': 'Entrar',
'login': 'inicio de sessão', 'login': 'inicio de sessão',
'Login successful': 'Login successful',
'Login to the Administrative Interface': 'Entrar na interface adminitrativa', 'Login to the Administrative Interface': 'Entrar na interface adminitrativa',
'Login/Register': 'Login/Register',
'Logout': 'finalizar sessão', 'Logout': 'finalizar sessão',
'Lost Password': 'Senha perdida', 'Lost Password': 'Senha perdida',
'Manage': 'Manage', 'Manage': 'Manage',
@@ -214,6 +228,7 @@
'modules': 'módulos', 'modules': 'módulos',
'Name': 'Nome', 'Name': 'Nome',
'new application "%s" created': 'nova aplicação "%s" criada', 'new application "%s" created': 'nova aplicação "%s" criada',
'New Application Wizard': 'New Application Wizard',
'New application wizard': 'Assistente para novas aplicações ', 'New application wizard': 'Assistente para novas aplicações ',
'new plugin installed': 'novo plugin instalado', 'new plugin installed': 'novo plugin instalado',
'New Record': 'Novo registro', 'New Record': 'Novo registro',
@@ -224,6 +239,7 @@
'No databases in this application': 'Não existem bancos de dados nesta aplicação', 'No databases in this application': 'Não existem bancos de dados nesta aplicação',
'no match': 'não encontrado', 'no match': 'não encontrado',
'no package selected': 'nenhum pacote selecionado', 'no package selected': 'nenhum pacote selecionado',
'No ticket_storage.txt found under /private folder': 'No ticket_storage.txt found under /private folder',
'online designer': 'online designer', 'online designer': 'online designer',
'or alternatively': 'or alternatively', 'or alternatively': 'or alternatively',
'Or Get from URL:': 'Ou Obtenha do URL:', 'Or Get from URL:': 'Ou Obtenha do URL:',
@@ -241,6 +257,7 @@
'Password': 'Senha', 'Password': 'Senha',
'password changed': 'senha alterada', 'password changed': 'senha alterada',
'Peeking at file': 'Visualizando arquivo', 'Peeking at file': 'Visualizando arquivo',
'Please wait, giving pythonanywhere a moment...': 'Please wait, giving pythonanywhere a moment...',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" eliminado', 'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" eliminado',
'Plugin "%s" in application': 'Plugin "%s" na aplicação', 'Plugin "%s" in application': 'Plugin "%s" na aplicação',
'plugins': 'plugins', 'plugins': 'plugins',
@@ -250,6 +267,8 @@
'previous 100 rows': '100 registros anteriores', 'previous 100 rows': '100 registros anteriores',
'Private files': 'Private files', 'Private files': 'Private files',
'private files': 'private files', 'private files': 'private files',
'PythonAnywhere Apps': 'PythonAnywhere Apps',
'PythonAnywhere Password': 'PythonAnywhere Password',
'Query:': 'Consulta:', 'Query:': 'Consulta:',
'Rapid Search': 'Rapid Search', 'Rapid Search': 'Rapid Search',
'record': 'registro', 'record': 'registro',
@@ -263,6 +282,7 @@
'Replace': 'Substituir', 'Replace': 'Substituir',
'Replace All': 'Substituir Tudo', 'Replace All': 'Substituir Tudo',
'request': 'request', 'request': 'request',
'requires python-git, but not installed': 'requires python-git, but not installed',
'Resolve Conflict file': 'Arquivo de resolução de conflito', 'Resolve Conflict file': 'Arquivo de resolução de conflito',
'response': 'response', 'response': 'response',
'restore': 'restaurar', 'restore': 'restaurar',
@@ -285,6 +305,9 @@
'shell': 'Terminal', 'shell': 'Terminal',
'Site': 'site', 'Site': 'site',
'some files could not be removed': 'alguns arquicos não puderam ser removidos', 'some files could not be removed': 'alguns arquicos não puderam ser removidos',
'Something went wrong please wait a few minutes before retrying': 'Something went wrong please wait a few minutes before retrying',
'source : filesystem': 'source : filesystem',
'Start a new app': 'Start a new app',
'Start searching': 'Start searching', 'Start searching': 'Start searching',
'Start wizard': 'iniciar assistente', 'Start wizard': 'iniciar assistente',
'state': 'estado', 'state': 'estado',
@@ -294,6 +317,7 @@
'Submit': 'Submit', 'Submit': 'Submit',
'submit': 'enviar', 'submit': 'enviar',
'Sure you want to delete this object?': 'Tem certeza que deseja apaagr este objeto?', 'Sure you want to delete this object?': 'Tem certeza que deseja apaagr este objeto?',
'switch to : db': 'switch to : db',
'table': 'tabela', 'table': 'tabela',
'Table name': 'Nome da tabela', 'Table name': 'Nome da tabela',
'test': 'testar', 'test': 'testar',
@@ -348,6 +372,7 @@
'update': 'atualizar', 'update': 'atualizar',
'update all languages': 'atualizar todas as linguagens', 'update all languages': 'atualizar todas as linguagens',
'Update:': 'Atualizar:', 'Update:': 'Atualizar:',
'upgrade now to %s': 'upgrade now to %s',
'upgrade web2py now': 'atualize o web2py agora', 'upgrade web2py now': 'atualize o web2py agora',
'upload': 'upload', 'upload': 'upload',
'Upload': 'Upload', 'Upload': 'Upload',
@@ -361,6 +386,7 @@
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para criar consultas mais complexas.', 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) para AND, (...)|(...) para OR, y ~(...) para NOT, para criar consultas mais complexas.',
'Use an url:': 'Use uma url:', 'Use an url:': 'Use uma url:',
'User ID': 'ID do Usuario', 'User ID': 'ID do Usuario',
'Username': 'Username',
'variables': 'variáveis', 'variables': 'variáveis',
'Version': 'Versão', 'Version': 'Versão',
'versioning': 'versionamento', 'versioning': 'versionamento',
@@ -369,6 +395,7 @@
'Views': 'Visões', 'Views': 'Visões',
'views': 'visões', 'views': 'visões',
'Web Framework': 'Web Framework', 'Web Framework': 'Web Framework',
'web2py Admin Password': 'web2py Admin Password',
'web2py is up to date': 'web2py está atualizado', 'web2py is up to date': 'web2py está atualizado',
'web2py Recent Tweets': 'Tweets Recentes de @web2py', 'web2py Recent Tweets': 'Tweets Recentes de @web2py',
'web2py upgraded; please restart it': 'web2py atualizado; favor reiniciar', 'web2py upgraded; please restart it': 'web2py atualizado; favor reiniciar',
@@ -138,6 +138,7 @@
<p class="row-buttons"> <p class="row-buttons">
{{=button(URL('gae','deploy'), T('Deploy on Google App Engine'))}} {{=button(URL('gae','deploy'), T('Deploy on Google App Engine'))}}
{{=button(URL('openshift','deploy'),T('Deploy to OpenShift'))}} {{=button(URL('openshift','deploy'),T('Deploy to OpenShift'))}}
{{=button(URL('pythonanywhere','deploy'), T('Deploy to PythonAnywhere'))}}
</p> </p>
</div> <!-- /DEPLOY ON GAE --> </div> <!-- /DEPLOY ON GAE -->
<!-- APP WIZARD --> <!-- APP WIZARD -->
@@ -0,0 +1,173 @@
{{extend 'layout.html'}}
<h2><span style="color:#139FD7">python</span>anywhere {{=T('Deployment Interface')}}</h2>
<div id="register_form">
<h3>{{=T('Login/Register')}}</h3>
<form class="form-horizontal" id="palogin">
<div class="control-group" id="username__row">
<label class="control-label" for="username">{{=T('Username')}}</label>
<div class="controls">
<input type="text" name="username" id="username">
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="email_address__row">
<label class="control-label" for="email_address">{{=T('Email Address')}}</label>
<div class="controls">
<input type="text" name="email_address" id="email_address">
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="pythonanywhere_password__row">
<label class="control-label" for="pythonanywhere_password">{{=T('PythonAnywhere Password')}}</label>
<div class="controls">
<input type="password" name="pythonanywhere_password" id="pythonanywhere_password">
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="web2py_admin_password__row">
<label class="control-label" for="web2py_admin_password">{{=T('web2py Admin Password')}}</label>
<div class="controls">
<input type="password" name="web2py_admin_password" id="web2py_admin_password">
<span class="help-block"></span>
</div>
</div>
<div class="control-group" id="accepts_terms__row">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="accepts_terms" id="accepts_terms"> {{=T('Accept Terms')}}
</label>
<span class="help-block"></span>
</div>
</div>
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-primary" id="submit_palogin">{{=T('Submit')}}</button>
</div>
</div>
</form>
</div>
<div class="row-fluid" id="app_manager" style="display:none;">
<div class="span6">
<h3>{{=T('Local Apps')}}</h3>
<form id="apppicker">
<select name="apps" class="form-control" id="local" multiple>
<option>{{=T('Loading...')}}</option>
</select>
<input type="submit" value="Deploy" id="deploy_button" class="btn btn-primary">
</form>
</div>
<div class="span6">
<h3>{{=T('PythonAnywhere Apps')}}</h3>
<ul id="pythonanywhere">
<li>{{=T('Loading...')}}</li>
</ul>
</div>
</div>
<script>
$(document).ready(function() {
$('#palogin').off('submit');
$('#palogin').submit(function(event) {
var data = $('#palogin').serialize();
$.web2py.disableElement($('#submit_palogin'));
$.web2py.disableFormElements($('#palogin'));
$.ajax({
url: '{{=URL("pythonanywhere", "crossdomainproxy")}}',
type: 'POST',
data: data,
dataType: 'json',
}).done(function(data, textStatus, jqXHR) {
console.log(data);
$('#palogin .error').removeClass('error');
$('#palogin .help-block').text('');
if(data.status == 'error') {
for(var error in data.errors) {
$('#' + error + '__row').addClass('error');
$('#' + error + '__row .help-block').text(data.errors[error][0]);
}
$.web2py.enableElement($('#submit_palogin'));
$.web2py.enableFormElements($('#palogin'));
$.web2py.flash("{{=T('Form has errors')}}");
} else {
$.web2py.flash("{{=T('Login successful')}}");
$('#register_form').hide();
$('#app_manager').show();
refresh_apps();
}
}).fail(function(){
$.web2py.flash("{{=T('Something went wrong please wait a few minutes before retrying')}}");
$.web2py.enableElement($('#submit_palogin'));
$.web2py.enableFormElements($('#palogin'));
});
event.preventDefault();
});
$('#apppicker').off('submit');
$('#apppicker').submit(function(event) {
var data = $('#apppicker').serialize();
$.web2py.disableElement($('#deploy_button'));
$.ajax({
url: '{{=URL("pythonanywhere", "bulk_install")}}',
type: 'POST',
data: {username: $('#username').val(), password: $('#web2py_admin_password').val(), apps: $('#local').val()},
dataType: 'json',
}).done(function(data, textStatus, jqXHR) {
console.log(data);
refresh_apps();
$.web2py.enableElement($('#deploy_button'));
}).fail(function(){
$.web2py.flash("{{=T('Something went wrong please wait a few minutes before retrying')}}");
$.web2py.enableElement($('#deploy_button'));
});
event.preventDefault();
});
});
function refresh_apps() {
// Refresh List of Apps
$('#deploy_button').prop('disabled', true);
$.ajax({
url: '{{=URL("pythonanywhere", "list_apps")}}',
type: 'GET',
data: {username: $('#username').val(), password: $('#web2py_admin_password').val()},
dataType: 'json',
}).done(function(data, textStatus, jqXHR) {
var i = 0;
$('#local').html('')
for(i = 0; i < data.local.length; i++) {
$('#local').append($('<option>', {
value: data.local[i],
text: data.local[i]
}));
}
$('#local').multiSelect('refresh');
$('#pythonanywhere').html('')
for(i = 0; i < data.pythonanywhere.length; i++) {
$('#pythonanywhere').append($('<li>', {
text: data.pythonanywhere[i]
}));
}
$('#deploy_button').prop('disabled', false);
$.web2py.hide_flash();
}).fail(function(){
// Mostly this happens if it's a new account, just waiting a bit should be enough.
$.get('http://' + $('#username').val() + '.pythonanywhere.com'); // Kickstart the instance
$.web2py.flash("{{=T('Please wait, giving pythonanywhere a moment...')}}");
setTimeout(refresh_apps, 30000);
});
}
</script>