Commit 0abce583 authored by Vermaat's avatar Vermaat
Browse files

Port website from web.py to Flask

This includes changing a lot of routes and parameter names to be more
consistent. We try to remain backwards compatible as much as possible
by providing redirects from old routes and parameter names.
parent 08794275
include AUTHORS README.md
recursive-include mutalyzer/templates *
recursive-include mutalyzer/website/templates *
......@@ -112,59 +112,8 @@ class File() :
@return: list of lists
@rtype: list
"""
# If we naively assume the input file uses \n characters as
# newlines, the CSV parser can trip over e.g. Windows style
# newlines. It will probably complain with a message like:
#
# new-line character seen in unquoted field - do you need to open
# the file in universal-newline mode?
#
# Here we try to support multiplatform newline modes in the input
# file, so \n, \r, \r\n are all recognized as a newline.
#
# This can be done by opening the file in 'U' mode, but in this case
# we already have an opened file (probably, if the call originated
# from a web request, opened by the web.py input handler, which uses
# the Python cgi module for opening uploaded files).
#
# A fix is to get the handle's file descriptor and create a new
# handle for it, using 'U' mode.
#
# However, sometimes our handler has no .fileno(), for example when
# the input file is quite small (< 1kb). In that case, the cgi
# module seems to optimize things and use a StringIO for the file,
# which of course has no .fileno().
#
# So our solution is:
# - We have .fileno(): Create a new handle, using 'U' mode.
# - We have no .fileno(): Replace all newlines by \n (in-memory)
# and wrap the result in a new StringIO.
if hasattr(handle, 'fileno'):
# Todo: We get the following error in our logs (exact origin
# unknown):
#
# close failed in file object destructor:
# IOError: [Errno 9] Bad file descriptor
#
# I am unable to find the reason for this. Everything seems to
# be working though.
new_handle = os.fdopen(handle.fileno(), 'rU')
elif hasattr(handle, 'getvalue'):
data = handle.getvalue()
data = data.replace('\r\n', '\n').replace('\r', '\n')
new_handle = StringIO(data)
else:
self.__output.addMessage(__file__, 4, "EBPARSE",
"Fatal error parsing input file, please"
" report this as a bug including the"
" input file.")
return None
# I don't think the .seek(0) is needed now we created a new handle
new_handle.seek(0)
buf = new_handle.read(BUFFER_SIZE)
handle.seek(0)
buf = handle.read(BUFFER_SIZE)
# Default dialect
dialect = 'excel'
......@@ -186,14 +135,14 @@ class File() :
# if dialect.delimiter == ":":
# dialect.delimiter = "\t"
new_handle.seek(0)
reader = csv.reader(new_handle, dialect)
handle.seek(0)
reader = csv.reader(handle, dialect)
ret = []
for i in reader:
ret.append(i)
new_handle.close()
handle.close()
return ret
#__parseCsvFile
......
......@@ -374,9 +374,7 @@ Mutalyzer batch scheduler""" % url)
else:
print ('Job %s finished, email %s file %s'
% (batch_job.id, batch_job.email, batch_job.id))
self.__sendMail(
batch_job.email, '%sResults_%s.txt'
% (batch_job.download_url_prefix, batch_job.result_id))
self.__sendMail(batch_job.email, batch_job.download_url)
session.delete(batch_job)
session.commit()
#process
......@@ -430,7 +428,7 @@ Mutalyzer batch scheduler""" % url)
outputline += batchOutput[0]
#Output
filename = "%s/Results_%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
filename = "%s/batch-job-%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
if not os.path.exists(filename) :
# If the file does not yet exist, create it with the correct
# header above it. The header is read from the config file as
......@@ -505,7 +503,7 @@ Mutalyzer batch scheduler""" % url)
result = "|".join(output.getBatchMessages(2))
#Output
filename = "%s/Results_%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
filename = "%s/batch-job-%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
if not os.path.exists(filename) :
# If the file does not yet exist, create it with the correct
# header above it. The header is read from the config file as
......@@ -608,7 +606,7 @@ Mutalyzer batch scheduler""" % url)
error = "%s" % "|".join(O.getBatchMessages(2))
#Output
filename = "%s/Results_%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
filename = "%s/batch-job-%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
if not os.path.exists(filename) :
# If the file does not yet exist, create it with the correct
# header above it. The header is read from the config file as
......@@ -671,7 +669,7 @@ Mutalyzer batch scheduler""" % url)
outputline += "%s\t" % "|".join(O.getBatchMessages(2))
#Output
filename = "%s/Results_%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
filename = "%s/batch-job-%s.txt" % (settings.CACHE_DIR, batch_job.result_id)
if not os.path.exists(filename) :
# If the file does not yet exist, create it with the correct
# header above it. The header is read from the config file as
......@@ -696,30 +694,33 @@ Mutalyzer batch scheduler""" % url)
"Finished SNP converter batch rs%s" % cmd)
#_processSNP
def addJob(self, eMail, queue, columns, fromHost, jobType,
Arg1):
def addJob(self, email, queue, columns, job_type, argument=None,
create_download_url=None):
"""
Add a job to the Database and start the BatchChecker.
@arg eMail: e-mail address of batch supplier
@type eMail: string
@arg email: e-mail address of batch supplier
@type email: string
@arg queue: A list of jobs
@type queue: list
@arg columns: The number of columns.
@type columns: int
@arg fromHost: From where is the request made
@type fromHost:
@arg jobType: The type of Batch Job that should be run
@type jobType:
@arg Arg1: Batch Arguments, for now only build info
@type Arg1:
@return: resultID
@arg job_type: The type of Batch Job that should be run
@type job_type:
@arg argument: Batch Arguments, for now only build info
@type argument:
@arg create_download_url: Function accepting a result_id and returning
the URL for downloading the batch job
result. Can be None.
@type create_download_url: function
@return: result_id
@rtype:
"""
# Add jobs to the database
batch_job = BatchJob(jobType, email=eMail, download_url_prefix=fromHost, argument=Arg1)
batch_job = BatchJob(job_type, email=email, argument=argument)
if create_download_url:
batch_job.download_url = create_download_url(batch_job.result_id)
session.add(batch_job)
for i, inputl in enumerate(queue):
......@@ -746,16 +747,7 @@ Mutalyzer batch scheduler""" % url)
item = BatchQueueItem(batch_job, inputl, flags=flag)
session.add(item)
# Spawn child
# Todo: Executable should be in bin/ directory.
#p = subprocess.Popen(["MutalyzerBatch",
# "bin/batch_daemon"], executable="python")
#Wait for the BatchChecker to fork of the Daemon
#p.communicate()
session.commit()
return batch_job.result_id
#addJob
#Scheduler
......@@ -62,6 +62,9 @@ class LazySettings(util.LazyObject):
self._wrapped = Settings()
self._wrapped.from_object('mutalyzer.config.default_settings')
if from_environment:
# Todo: We crash if the environment variable is not set. Perhaps
# it is more user-friendly if we fall back on ./settings.py or
# ~/mutalyzer_settings.py or something if it exists.
self._wrapped.from_envvar(ENVIRONMENT_VARIABLE)
def configure(self, settings):
......
......@@ -4,6 +4,11 @@ pointed-to by the `MUTALYZER_SETTINGS` environment variable.
"""
# Todo: Find an alternative to the temporary directories/files defined here,
# since they are created even if the setting is reset from another
# configuration file, and they are never removed.
# Use Mutalyzer in debug mode.
DEBUG = False
......@@ -68,6 +73,14 @@ PROTEIN_LINK_EXPIRATION = 60 * 60 * 24 * 30
# seconds).
NEGATIVE_PROTEIN_LINK_EXPIRATION = 60 * 60 * 24 * 5
# URL to the SOAP webservice WSDL document. Used for linking to it from the
# documentation page on the website.
SOAP_WSDL_URL = 'https://mutalyzer.nl/services/?wsdl'
# URL to the HTTP/RPC+JSON webservice root (without trailing slash). Used for
# linking to it from the documentation page on the website.
JSON_ROOT_URL = 'https://mutalyzer.nl/json'
# Is Piwik enabled?
PIWIK = False
......
......@@ -72,9 +72,9 @@ class BatchJob(db.Base):
#: Email address of user who submitted the job.
email = Column(String(200))
#: Prefix of URL for downloading the job result file. This would usually
#: be the Mutalyzer website base URL.
download_url_prefix = Column(String(200))
#: URL for downloading the job result file. This would usually be a view
#: on the Mutalyzer website.
download_url = Column(String(200))
#: Type of batch job.
job_type = Column(Enum(*BATCH_JOB_TYPES, name='job_type'), nullable=False)
......@@ -91,11 +91,11 @@ class BatchJob(db.Base):
#: Date and time of creation.
added = Column(DateTime)
def __init__(self, job_type, email=None, download_url_prefix=None,
def __init__(self, job_type, email=None, download_url=None,
argument=None):
self.job_type = job_type
self.email = email
self.download_url_prefix = download_url_prefix
self.download_url = download_url
self.argument = argument
self.result_id = str(uuid.uuid4())
self.added = datetime.now()
......
......@@ -9,6 +9,7 @@ This program is intended to be run daily from cron. Example:
# Todo: Merge this script with mapping_import, the difference between the two
# makes no sense.
# Todo: Check if the input file is sorted as required and abort if not.
import argparse
......
......@@ -36,39 +36,32 @@ also serve the static files.
import argparse
import os
import pkg_resources
from .. import website
application = website.app.wsgifunc()
application = website.create_app()
def debugserver():
def debugserver(port):
"""
Run the website with the Python built-in HTTP server.
"""
# There's really no sane way to make web.py serve static files other than
# providing it with a `static` directory, so we just jump to the template
# directory where it can find this.
os.chdir(pkg_resources.resource_filename('mutalyzer', 'templates'))
website.app.run()
application.run(port=port, debug=True, use_reloader=False)
def main():
"""
Command-line interface to the website.
Command-line interface to the website..
"""
parser = argparse.ArgumentParser(
description='Mutalyzer website.')
parser.add_argument(
'port', metavar='NUMBER', type=int, nargs='?', default=8080,
help='port to run the website on (default: 8080)')
'-p', '--port', metavar='NUMBER', dest='port', type=int,
default=8089, help='port to run the website on (default: 8080)')
args = parser.parse_args()
debugserver()
debugserver(args.port)
if __name__ == '__main__':
......
......@@ -96,7 +96,6 @@ class MutalyzerService(ServiceBase):
# Todo: Set maximum request size by specifying the max_content_length
# argument for spyne.server.wsgi.WsgiApplication in all webservice
# instantiations.
max_size = settings.MAX_FILE_SIZE
batch_file = tempfile.TemporaryFile()
......@@ -120,7 +119,7 @@ class MutalyzerService(ServiceBase):
raise Fault('EPARSE', 'Could not parse input file, please check your file format.')
result_id = scheduler.addJob('job@webservice', job, columns,
'webservice', batch_types[process], argument)
batch_types[process], argument)
return result_id
@srpc(Mandatory.String, _returns=Integer)
......@@ -159,7 +158,7 @@ class MutalyzerService(ServiceBase):
if left > 0:
raise Fault('EBATCHNOTREADY', 'Batch job result is not yet ready.')
filename = 'Results_%s.txt' % job_id
filename = 'batch-job-%s.txt' % job_id
handle = open(os.path.join(settings.CACHE_DIR, filename))
return handle
......
function updateVisibility() {
document.getElementById('file_label').style.display = "none";
document.getElementById('url_label').style.display = "none";
document.getElementById('gene_label').style.display = "none";
document.getElementById('range_label').style.display = "none";
document.getElementById('chrname_label').style.display = "none";
for (i = 0; i < document.invoer.invoermethode.length; i++) {
if (document.invoer.invoermethode[i].checked) {
if (document.invoer.invoermethode[i].value == 'file') {
document.getElementById('file_label').style.display = "";
}
else if (document.invoer.invoermethode[i].value == 'url') {
document.getElementById('url_label').style.display = "";
}
else if (document.invoer.invoermethode[i].value == 'gene') {
document.getElementById('gene_label').style.display = "";
}
else if (document.invoer.invoermethode[i].value == 'chr') {
document.getElementById('range_label').style.display = "";
}
else if (document.invoer.invoermethode[i].value == 'chrname') {
document.getElementById('chrname_label').style.display = "";
}
}//if
}//for
}//updateVisibility
//Toggle the build option in the batch.html page
function changeBatch(sel) {
var opt = sel.options[sel.selectedIndex].value;
if(opt=='PositionConverter') {
document.getElementById('build').style.display = "";
} else {
document.getElementById('build').style.display = "none";
}
}
function toggle_visibility(id) {
var e = document.getElementById(id);
if (e.style.display == 'block') {
e.style.display = 'none';
} else {
e.style.display = 'block';
}
}
var doInterval;
function onloadBatch() {
changeBatch(document.getElementById('batchType'));
doInterval = setInterval(updatePercentage, 3000);
}
// Get the HTTP Object
function getHTTPObject(){
if (window.ActiveXObject)
return new ActiveXObject("Microsoft.XMLHTTP");
else if (window.XMLHttpRequest)
return new XMLHttpRequest();
else {
alert("Your browser does not support AJAX.");
return null;
}
}
function updatePercentage() {
if (!document.getElementById('resultID')){ return; };
var id = document.getElementById('resultID').value;
var total = document.getElementById('totalJobs').value;
var url = 'progress?resultID='+id+'&totalJobs='+total+'&ajax=1';
var val = "";
http = getHTTPObject();
if (http == null){
val = "Your browser does not support Ajax, swith to a different ";
val +="or wait for the email to arrive."
document.getElementById('percent').innerHTML = val;
clearInterval(doInterval);
return null;
}
http.open("GET", url, true);
http.onreadystatechange=function() {
if(http.readyState == 4) {
if (http.responseText == "OK"){
val = "Your job is finished, results can be downloaded from: ";
val += "<a href='Results_"+id+".txt'>here</a>";
clearInterval(doInterval);
} else if (isNaN(http.responseText)){
//Something went wrong
val = "Updating went wrong please wait for your email";
clearInterval(doInterval);
} else {
val = "Your job is in progress and currently at ";
val += parseInt(http.responseText);
val += "%.";
}
}
document.getElementById('percent').innerHTML = val;
}
http.send(null);
}
function clearField(form, fieldName) {
for (var i = 0; i < form.elements.length; i++) {
if (form.elements[i].name == fieldName) {
form.elements[i].value = '';
}
}
}
......@@ -1698,8 +1698,9 @@ def check_variant(description, output):
assembly = Assembly.query.filter_by(alias='hg19').first()
if assembly:
converter = Converter(assembly, output)
version = int(parsed_description.Version) if parsed_description.Version else None
chromosomal_positions = converter.chromosomal_positions(
locations, parsed_description.RefSeqAcc, parsed_description.Version or None)
locations, parsed_description.RefSeqAcc, version)
if chromosomal_positions:
output.addOutput('rawVariantsChromosomal',
(chromosomal_positions[0], chromosomal_positions[1],
......
This diff is collapsed.
"""
Mutalyzer website interface using the Flask framework.
"""
import pkg_resources
from flask import Flask
from mutalyzer.config import settings
from mutalyzer.db import session
# Todo: Perhaps we also need this for the RPC services?
class ReverseProxied(object):
"""
Wrap the application in this middleware and configure the front-end server
to add these headers, to let you quietly bind this to a URL other than /
and to an HTTP scheme that is different than what is used locally.
Example for nginx::
location /myprefix {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /myprefix;
}
`Flask Snippet <http://flask.pocoo.org/snippets/35/>`_ from Peter Hansen.
"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name:
environ['SCRIPT_NAME'] = script_name
path_info = environ['PATH_INFO']
if path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
return self.app(environ, start_response)
def create_app():
"""
Create a Flask instance for Mutalyzer.
"""
template_folder = pkg_resources.resource_filename(
'mutalyzer', 'website/templates')
static_folder = pkg_resources.resource_filename(
'mutalyzer', 'website/templates/static')
app = Flask('mutalyzer',
template_folder=template_folder, static_folder=static_folder)
app.config.update(DEBUG=settings.DEBUG,
TESTING=settings.TESTING,
MAX_CONTENT_LENGTH=settings.MAX_FILE_SIZE)
from mutalyzer.website.views import website
app.register_blueprint(website)
@app.teardown_appcontext
def shutdown_session(exception=None):
session.remove()
return app
def create_reverse_proxied_app():
"""
Create a Flask instance for Mutalyzer running behind a reverse proxy.
See :func:`create_app` and :class:`ReverseProxied`.
"""
app = create_app()
app.wsgi_app = ReverseProxied(app.wsgi_app)
return app
......@@ -6,8 +6,8 @@
{% block content %}
<p>
Mutalyzer {{ version|e }} is designed and developed by Jeroen F.J. Laros, with
the following exceptions:
Mutalyzer {{ mutalyzer_version }} is designed and developed by Jeroen
F.J. Laros, with the following exceptions:
</p>
<ul>
......
......@@ -6,6 +6,11 @@
<link rel="stylesheet"
type="text/css"
href="static/css/style.css">
<script
type="text/javascript"
language="javascript"
src="static/js/jquery-1.10.2.min.js">
</script>
<script
type="text/javascript"
language="javascript"
......@@ -18,7 +23,7 @@
</script>
<meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1">
<title>Mutalyzer {{ version|e }} &mdash; {{ page_title|e }}</title>
<title>Mutalyzer {{ mutalyzer_version }} &mdash; {{ page_title }}</title>
</head>
<body
style="background-image: url('static/images/background.gif');
......@@ -39,7 +44,7 @@
<td rowspan="2"
valign="bottom"
width="180">
<a href="index"
<a href="{{ url_for('.homepage') }}"
target="_top"><img
src="static/images/mutalyzer_logo_bw.png" width="180" height="112"
alt="Rauzy fractal" border="0" hspace="0" vspace="0"></a></td>
......@@ -54,7 +59,7 @@
width="98%">
<!-- Banner -->
<center>
<a href="index"><img
<a href="{{ url_for('.homepage') }}"><img
src="static/images/mutalyzer_logo.png"
width="90%"
height="90"
......@@ -75,11 +80,11 @@
class="hornav">previous page</a>&nbsp;&nbsp;&nbsp;
</td>
<td align="right">
<a href="index"
<a href="{{ url_for('.homepage') }}"
class="hornav">home</a>&nbsp;&nbsp;&nbsp;
<a href="<