about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Dockerfile2
-rw-r--r--bh20seqanalyzer/main.py41
-rw-r--r--bh20sequploader/main.py27
-rw-r--r--bh20sequploader/qc_metadata.py15
-rw-r--r--bh20simplewebuploader/main.py119
-rw-r--r--bh20simplewebuploader/static/main.css26
-rw-r--r--bh20simplewebuploader/templates/footer.html14
-rw-r--r--bh20simplewebuploader/templates/form.html49
-rw-r--r--bh20simplewebuploader/templates/home.html50
-rw-r--r--bh20simplewebuploader/templates/menu.html4
-rw-r--r--bh20simplewebuploader/templates/resource.html27
-rw-r--r--bh20simplewebuploader/templates/status.html17
12 files changed, 296 insertions, 95 deletions
diff --git a/Dockerfile b/Dockerfile
index 43fa8f2..1901ac7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,7 +13,7 @@ ADD bh20seqanalyzer /app/bh20simplewebuploader
 ADD bh20sequploader /app/bh20sequploader
 ADD bh20simplewebuploader /app/bh20simplewebuploader
 
-RUN pip3 install -e .
+RUN pip3 install -e .[web]
 
 ENV PORT 8080
 CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8080", "bh20simplewebuploader.main:app"]
diff --git a/bh20seqanalyzer/main.py b/bh20seqanalyzer/main.py
index 1746587..0b52e6b 100644
--- a/bh20seqanalyzer/main.py
+++ b/bh20seqanalyzer/main.py
@@ -17,10 +17,11 @@ logging.basicConfig(format="[%(asctime)s] %(levelname)s %(message)s", datefmt="%
 logging.getLogger("googleapiclient.discovery").setLevel(logging.WARN)
 
 def validate_upload(api, collection, validated_project,
-                    fastq_project, fastq_workflow_uuid):
+                    fastq_project, fastq_workflow_uuid,
+                    revalidate):
     col = arvados.collection.Collection(collection["uuid"])
 
-    if collection.get("status") in ("validated", "rejected"):
+    if not revalidate and collection["properties"].get("status") in ("validated", "rejected"):
         return False
 
     # validate the collection here.  Check metadata, etc.
@@ -28,11 +29,12 @@ def validate_upload(api, collection, validated_project,
 
     errors = []
 
-    dup = api.collections().list(filters=[["owner_uuid", "=", validated_project],
-                                          ["portable_data_hash", "=", col.portable_data_hash()]]).execute()
-    if dup["items"]:
-        # This exact collection has been uploaded before.
-        errors.append("Duplicate of %s" % ([d["uuid"] for d in dup["items"]]))
+    if collection["owner_uuid"] != validated_project:
+        dup = api.collections().list(filters=[["owner_uuid", "=", validated_project],
+                                              ["portable_data_hash", "=", col.portable_data_hash()]]).execute()
+        if dup["items"]:
+            # This exact collection has been uploaded before.
+            errors.append("Duplicate of %s" % ([d["uuid"] for d in dup["items"]]))
 
     if not errors:
         if "metadata.yaml" not in col:
@@ -70,12 +72,15 @@ def validate_upload(api, collection, validated_project,
 
 
     if not errors:
-        logging.info("Added '%s' to validated sequences" % collection["name"])
         # Move it to the "validated" project to be included in the next analysis
+        if "errors" in collection["properties"]:
+            del collection["properties"]["errors"]
         collection["properties"]["status"] = "validated"
         api.collections().update(uuid=collection["uuid"], body={
             "owner_uuid": validated_project,
-            "name": "%s (%s)" % (collection["name"], time.asctime(time.gmtime()))}).execute()
+            "name": "%s (%s)" % (collection["name"], time.asctime(time.gmtime())),
+            "properties": collection["properties"]}).execute()
+        logging.info("Added '%s' to validated sequences" % collection["name"])
         return True
     else:
         # It is invalid
@@ -155,7 +160,9 @@ def start_pangenome_analysis(api,
                              validated_project,
                              schema_ref,
                              exclude_list):
-    validated = arvados.util.list_all(api.collections().list, filters=[["owner_uuid", "=", validated_project]])
+    validated = arvados.util.list_all(api.collections().list, filters=[
+        ["owner_uuid", "=", validated_project],
+        ["properties.status", "=", "validated"]])
     inputobj = {
         "inputReads": [],
         "metadata": [],
@@ -187,14 +194,15 @@ def get_workflow_output_from_project(api, uuid):
     cr = api.container_requests().list(filters=[['owner_uuid', '=', uuid],
                                                 ["requesting_container_uuid", "=", None]]).execute()
     if cr["items"] and cr["items"][0]["output_uuid"]:
-        return cr["items"][0]
-    else:
-        return None
+        container = api.containers().get(uuid=cr["items"][0]["container_uuid"]).execute()
+        if container["state"] == "Complete" and container["exit_code"] == 0:
+            return cr["items"][0]
+    return None
 
 
 def copy_most_recent_result(api, analysis_project, latest_result_uuid):
     most_recent_analysis = api.groups().list(filters=[['owner_uuid', '=', analysis_project]],
-                                                  order="created_at desc", limit=1).execute()
+                                                  order="created_at desc").execute()
     for m in most_recent_analysis["items"]:
         wf = get_workflow_output_from_project(api, m["uuid"])
         if wf:
@@ -220,6 +228,7 @@ def move_fastq_to_fasta_results(api, analysis_project, uploader_project):
                                      body={"owner_uuid": uploader_project}).execute()
             p["properties"]["moved_output"] = True
             api.groups().update(uuid=p["uuid"], body={"properties": p["properties"]}).execute()
+            break
 
 
 def upload_schema(api, workflow_def_project):
@@ -297,6 +306,7 @@ def main():
     parser.add_argument('--no-start-analysis', action="store_true")
     parser.add_argument('--once', action="store_true")
     parser.add_argument('--print-status', type=str, default=None)
+    parser.add_argument('--revalidate', action="store_true", default=None)
     args = parser.parse_args()
 
     api = arvados.api()
@@ -330,7 +340,8 @@ def main():
             at_least_one_new_valid_seq = validate_upload(api, c,
                                                          args.validated_project,
                                                          args.fastq_project,
-                                                         args.fastq_workflow_uuid) or at_least_one_new_valid_seq
+                                                         args.fastq_workflow_uuid,
+                                                         args.revalidate) or at_least_one_new_valid_seq
 
         if at_least_one_new_valid_seq and not args.no_start_analysis:
             start_pangenome_analysis(api,
diff --git a/bh20sequploader/main.py b/bh20sequploader/main.py
index fd0278d..f744a8c 100644
--- a/bh20sequploader/main.py
+++ b/bh20sequploader/main.py
@@ -19,8 +19,10 @@ log = logging.getLogger(__name__ )
 log.debug("Entering sequence uploader")
 
 ARVADOS_API_HOST='lugli.arvadosapi.com'
-ARVADOS_API_TOKEN='2fbebpmbo3rw3x05ueu2i6nx70zhrsb1p22ycu3ry34m4x4462'
+UPLOADER_API_TOKEN='2fbebpmbo3rw3x05ueu2i6nx70zhrsb1p22ycu3ry34m4x4462'
+ANONYMOUS_API_TOKEN='5o42qdxpxp5cj15jqjf7vnxx5xduhm4ret703suuoa3ivfglfh'
 UPLOAD_PROJECT='lugli-j7d0g-n5clictpuvwk8aa'
+VALIDATED_PROJECT='lugli-j7d0g-5ct8p1i1wrgyjvp'
 
 def qc_stuff(metadata, sequence_p1, sequence_p2, do_qc=True):
     failed = False
@@ -67,9 +69,14 @@ def main():
     parser.add_argument('sequence_p2', type=argparse.FileType('rb'), default=None, nargs='?', help='sequence FASTQ pair')
     parser.add_argument("--validate", action="store_true", help="Dry run, validate only")
     parser.add_argument("--skip-qc", action="store_true", help="Skip local qc check")
+    parser.add_argument("--trusted", action="store_true", help="Trust local validation and add directly to validated project")
     args = parser.parse_args()
 
-    api = arvados.api(host=ARVADOS_API_HOST, token=ARVADOS_API_TOKEN, insecure=True)
+    if args.trusted:
+        # Use credentials from environment
+        api = arvados.api()
+    else:
+        api = arvados.api(host=ARVADOS_API_HOST, token=UPLOADER_API_TOKEN, insecure=True)
 
     target = qc_stuff(args.metadata, args.sequence_p1, args.sequence_p2, not args.skip_qc)
     seqlabel = target[0][1]
@@ -106,7 +113,21 @@ def main():
         "upload_user": "%s@%s" % (username, socket.gethostname())
     }
 
-    col.save_new(owner_uuid=UPLOAD_PROJECT, name="%s uploaded by %s from %s" %
+    api2 = arvados.api(host=ARVADOS_API_HOST, token=ANONYMOUS_API_TOKEN, insecure=True)
+    dup = api2.collections().list(filters=[["owner_uuid", "in", [VALIDATED_PROJECT, UPLOAD_PROJECT]],
+                                           ["portable_data_hash", "=", col.portable_data_hash()]]).execute()
+    if dup["items"]:
+        # This exact collection has been uploaded before.
+        print("Duplicate of %s" % ([d["uuid"] for d in dup["items"]]))
+        exit(1)
+
+    if args.trusted:
+        properties["status"] = "validated"
+        owner_uuid = VALIDATED_PROJECT
+    else:
+        owner_uuid = UPLOAD_PROJECT
+
+    col.save_new(owner_uuid=owner_uuid, name="%s uploaded by %s from %s" %
                  (seqlabel, properties['upload_user'], properties['upload_ip']),
                  properties=properties, ensure_unique_name=True)
 
diff --git a/bh20sequploader/qc_metadata.py b/bh20sequploader/qc_metadata.py
index 2b57991..27657b1 100644
--- a/bh20sequploader/qc_metadata.py
+++ b/bh20sequploader/qc_metadata.py
@@ -8,15 +8,20 @@ import traceback
 from rdflib import Graph, Namespace
 from pyshex.evaluate import evaluate
 
+metadata_schema = None
 
 def qc_metadata(metadatafile):
+    global metadata_schema
     log = logging.getLogger(__name__ )
-    schema_resource = pkg_resources.resource_stream(__name__, "bh20seq-schema.yml")
-    cache = {"https://raw.githubusercontent.com/arvados/bh20-seq-resource/master/bh20sequploader/bh20seq-schema.yml": schema_resource.read().decode("utf-8")}
+    if metadata_schema is None:
+        schema_resource = pkg_resources.resource_stream(__name__, "bh20seq-schema.yml")
+        cache = {"https://raw.githubusercontent.com/arvados/bh20-seq-resource/master/bh20sequploader/bh20seq-schema.yml": schema_resource.read().decode("utf-8")}
+        metadata_schema = schema_salad.schema.load_schema("https://raw.githubusercontent.com/arvados/bh20-seq-resource/master/bh20sequploader/bh20seq-schema.yml", cache=cache)
+
     (document_loader,
      avsc_names,
      schema_metadata,
-     metaschema_loader) = schema_salad.schema.load_schema("https://raw.githubusercontent.com/arvados/bh20-seq-resource/master/bh20sequploader/bh20seq-schema.yml", cache=cache)
+     metaschema_loader) = metadata_schema
 
     shex = pkg_resources.resource_stream(__name__, "bh20seq-shex.rdf").read().decode("utf-8")
 
@@ -27,6 +32,10 @@ def qc_metadata(metadatafile):
     g = schema_salad.jsonld_context.makerdf("workflow", doc, document_loader.ctx)
     rslt, reason = evaluate(g, shex, doc["id"], "https://raw.githubusercontent.com/arvados/bh20-seq-resource/master/bh20sequploader/bh20seq-shex.rdf#submissionShape")
 
+    # As part of QC make sure serialization works too, this will raise
+    # an exception if there are invalid URIs.
+    g.serialize(format="ntriples")
+
     if not rslt:
         raise Exception(reason)
 
diff --git a/bh20simplewebuploader/main.py b/bh20simplewebuploader/main.py
index 3100dfd..9132453 100644
--- a/bh20simplewebuploader/main.py
+++ b/bh20simplewebuploader/main.py
@@ -13,12 +13,20 @@ import pkg_resources
 from flask import Flask, request, redirect, send_file, send_from_directory, render_template, jsonify
 import os.path
 import requests
+import io
+import arvados
+from markupsafe import Markup
+
+ARVADOS_API = 'lugli.arvadosapi.com'
+ANONYMOUS_TOKEN = '5o42qdxpxp5cj15jqjf7vnxx5xduhm4ret703suuoa3ivfglfh'
+UPLOADER_PROJECT = 'lugli-j7d0g-n5clictpuvwk8aa'
+VALIDATED_PROJECT = 'lugli-j7d0g-5ct8p1i1wrgyjvp'
 
 logging.basicConfig(level=logging.DEBUG)
 log = logging.getLogger(__name__ )
 log.debug("Entering web uploader")
 
-if not os.path.isfile('bh20sequploader/mainx.py'):
+if not os.path.isfile('bh20sequploader/main.py'):
     print("WARNING: run FLASK from the root of the source repository!", file=sys.stderr)
 
 app = Flask(__name__, static_url_path='/static', static_folder='static')
@@ -224,12 +232,21 @@ METADATA_OPTION_DEFINITIONS = yaml.safe_load(pkg_resources.resource_stream("bh20
 FORM_ITEMS = generate_form(METADATA_SCHEMA, METADATA_OPTION_DEFINITIONS)
 
 @app.route('/')
+def send_home():
+    """
+    Send the front page.
+    """
+
+    return render_template('home.html', menu='HOME')
+
+
+@app.route('/upload')
 def send_form():
     """
     Send the file upload form/front page.
     """
 
-    return render_template('form.html', fields=FORM_ITEMS, menu='HOME')
+    return render_template('form.html', fields=FORM_ITEMS, menu='UPLOAD')
 
 class FileTooBigError(RuntimeError):
     """
@@ -405,7 +422,7 @@ def receive_files():
 
         # Try and upload files to Arvados using the sequence uploader CLI
 
-        cmd = ['python3','bh20sequploader/main.py', fasta_dest, metadata_dest]
+        cmd = ['python3','bh20sequploader/main.py', metadata_dest, fasta_dest]
         print(" ".join(cmd),file=sys.stderr)
         result = subprocess.run(cmd,
             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -439,7 +456,83 @@ def get_html_body(fn):
 @app.route('/download')
 def download_page():
     buf = get_html_body('doc/web/download.html')
-    return render_template('about.html',menu='DOWNLOAD',embed=buf)
+    return render_template('resource.html',menu='DOWNLOAD',embed=buf)
+
+def pending_table(output, items):
+    output.write(
+"""
+<table>
+<tr><th>Collection</th>
+<th>Sequence label</th></tr>
+""")
+    for r in items:
+        if r["status"] != "pending":
+            continue
+        output.write("<tr>")
+        output.write("<td><a href='https://workbench.lugli.arvadosapi.com/collections/%s'>%s</a></td>" % (r["uuid"], r["uuid"]))
+        output.write("<td>%s</td>" % Markup.escape(r["sequence_label"]))
+        output.write("</tr>")
+    output.write(
+"""
+</table>
+""")
+
+def rejected_table(output, items):
+    output.write(
+"""
+<table>
+<tr><th>Collection</th>
+<th>Sequence label</th>
+<th>Errors</th></tr>
+""")
+    for r in items:
+        if r["status"] != "rejected":
+            continue
+        output.write("<tr>")
+        output.write("<td><a href='https://workbench.lugli.arvadosapi.com/collections/%s'>%s</a></td>" % (r["uuid"], r["uuid"]))
+        output.write("<td>%s</td>" % Markup.escape(r["sequence_label"]))
+        output.write("<td><pre>%s</pre></td>" % Markup.escape("\n".join(r.get("errors", []))))
+        output.write("</tr>")
+    output.write(
+"""
+</table>
+""")
+
+
+@app.route('/status')
+def status_page():
+    """
+    Processing status
+    """
+
+    api = arvados.api(host=ARVADOS_API, token=ANONYMOUS_TOKEN)
+    pending = arvados.util.list_all(api.collections().list, filters=[["owner_uuid", "=", UPLOADER_PROJECT]])
+    out = []
+    status = {}
+    for p in pending:
+        prop = p["properties"]
+        out.append(prop)
+        if "status" not in prop:
+            prop["status"] = "pending"
+        prop["created_at"] = p["created_at"]
+        prop["uuid"] = p["uuid"]
+        status[prop["status"]] = status.get(prop["status"], 0) + 1
+
+    output = io.StringIO()
+
+    validated = api.collections().list(filters=[["owner_uuid", "=", VALIDATED_PROJECT]], limit=1).execute()
+    status["passed"] = validated["items_available"]
+
+    for s in (("passed", "/download"), ("pending", "#pending"), ("rejected", "#rejected")):
+        output.write("<p><a href='%s'>%s sequences QC %s</a></p>" % (s[1], status.get(s[0], 0), s[0]))
+
+    output.write("<a id='pending'><h1>Pending</h1>")
+    pending_table(output, out)
+
+    output.write("<a id='rejected'><h1>Rejected</h1>")
+    rejected_table(output, out)
+
+    return render_template('status.html', table=Markup(output.getvalue()), menu='STATUS')
 
 @app.route('/demo')
 def demo_page():
@@ -474,20 +567,10 @@ baseURL='http://sparql.genenetwork.org/sparql/'
 
 @app.route('/api/getCount', methods=['GET'])
 def getCount():
-    query="""
-PREFIX pubseq: <http://biohackathon.org/bh20-seq-schema#MainSchema/>
-select (COUNT(distinct ?dataset) as ?num)
-{
-   ?dataset pubseq:submitter ?id .
-   ?id ?p ?submitter
-}
-"""
-    payload = {'query': query, 'format': 'json'}
-    r = requests.get(baseURL, params=payload)
-    result = r.json()['results']['bindings']
-    # [{'num': {'type': 'typed-literal', 'datatype': 'http://www.w3.org/2001/XMLSchema#integer', 'value': '1352'}}]
-    # print(result, file=sys.stderr)
-    return jsonify({'sequences': int(result[0]["num"]["value"])})
+    api = arvados.api(host=ARVADOS_API, token=ANONYMOUS_TOKEN)
+    c = api.collections().list(filters=[["owner_uuid", "=", VALIDATED_PROJECT]], limit=1).execute()
+
+    return jsonify({'sequences': c["items_available"]})
 
 @app.route('/api/getAllaccessions', methods=['GET'])
 def getAllaccessions():
diff --git a/bh20simplewebuploader/static/main.css b/bh20simplewebuploader/static/main.css
index 5a9f231..b9b27f4 100644
--- a/bh20simplewebuploader/static/main.css
+++ b/bh20simplewebuploader/static/main.css
@@ -168,11 +168,11 @@ span.dropt:hover {text-decoration: none; background: #ffffff; z-index: 6; }
     grid-template-rows: auto;
     row-gap:5px;
     grid-template-areas:
-        "a a b b"
-            "a a c c"
-            "a a d d"
-            "e e e e"
-            "f f f f";
+        "b b a a"
+	"b b c c"
+        "b b d d"
+        "e e e e"
+        "f f f f";
     grid-auto-flow: column;
 }
 
@@ -361,3 +361,19 @@ footer {
 .blog-table-body {
     display: table-row-group;
 }
+
+div.status {
+    margin: 1em;
+}
+
+.status table {
+    display: table;
+    width: 100%;
+}
+
+.status td, th {
+    padding-left: 1em;
+    padding-right: 1em;
+    vertical-align: top;
+    border-bottom: 1px solid #ddd;
+}
diff --git a/bh20simplewebuploader/templates/footer.html b/bh20simplewebuploader/templates/footer.html
index 9326b1e..a1dd4fd 100644
--- a/bh20simplewebuploader/templates/footer.html
+++ b/bh20simplewebuploader/templates/footer.html
@@ -41,3 +41,17 @@
   </div>
 </section>
 <script type="text/javascript" src="/static/main.js"></script>
+
+<script type="text/javascript">
+  document.addEventListener("DOMContentLoaded", function(){
+      var count = fetch("/api/getCount")
+          .then((resp) => resp.json())
+          .then(function (data) {
+              count = data["sequences"];
+              console.log(count);
+              span = document.getElementById("Counter");
+              txt = document.createTextNode(count);
+              span.appendChild(txt);
+      });
+  });
+</script>
diff --git a/bh20simplewebuploader/templates/form.html b/bh20simplewebuploader/templates/form.html
index 0ad2080..b9b3776 100644
--- a/bh20simplewebuploader/templates/form.html
+++ b/bh20simplewebuploader/templates/form.html
@@ -7,43 +7,6 @@
 
         <section>
             <form action="/submit" method="POST" enctype="multipart/form-data" id="main_form" class="grid-container">
-                <div class="intro">
-                    <p>
-                        Make your sequence
-                        data <a href="https://en.wikipedia.org/wiki/FAIR_data">FAIR</a>. Upload
-                        your SARS-CoV-2 sequence (FASTA or FASTQ
-                        formats) with metadata (JSONLD) to
-                        the <a href="/about">public sequence
-                        resource</a>. The upload will trigger a
-                        recompute with all available sequences into a
-                        Pangenome available for
-                        <a href="/download">download</a>!
-                    </p>
-                    <p>
-                        Your uploaded sequence will automatically be
-                        processed and incorporated into the public
-                        pangenome with metadata using worklows from
-                        the High Performance Open Biology Lab
-                        defined <a href="https://github.com/hpobio-lab/viral-analysis/tree/master/cwl/pangenome-generate">here</a>. All
-                        data is published under
-                        a <a href="https://creativecommons.org/licenses/by/4.0/">Creative
-                        Commons license</a> You can take the published
-                        (GFA/RDF/FASTA) data and store it in a triple
-                        store for further processing.  Clinical
-                        data can be stored
-                        securely
-                        at <a href="https://redcap-covid19.elixir-luxembourg.org/redcap/">REDCap</a>.
-                    </p>
-                    <p>
-                      Note that form fields contain
-                      web <a href="https://en.wikipedia.org/wiki/Web_Ontology_Language">ontology
-                      URI's</a>
-                      for <a href="https://en.wikipedia.org/wiki/Wikipedia:Disambiguation">disambiguation</a>
-                      and machine readable metadata. For examples of
-                      use, see the <a href="/blog">BLOG</a>.
-                    </p>
-                </div>
-
                 <div class="fasta-file-select">
                     <h2><svg class="bi bi-cloud-upload" width="1.2em" height="1.2em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
                         <path d="M4.887 6.2l-.964-.165A2.5 2.5 0 103.5 11H6v1H3.5a3.5 3.5 0 11.59-6.95 5.002 5.002 0 119.804 1.98A2.501 2.501 0 0113.5 12H10v-1h3.5a1.5 1.5 0 00.237-2.981L12.7 7.854l.216-1.028a4 4 0 10-7.843-1.587l-.185.96z"/>
@@ -160,18 +123,6 @@
 
   setMode()
 
-  document.addEventListener("DOMContentLoaded", function(){
-      var count = fetch("/api/getCount")
-          .then((resp) => resp.json())
-          .then(function (data) {
-              count = data["sequences"];
-              console.log(count);
-              span = document.getElementById("Counter");
-              txt = document.createTextNode(count);
-              span.appendChild(txt);
-      });
-});
-
 </script>
 
    </body>
diff --git a/bh20simplewebuploader/templates/home.html b/bh20simplewebuploader/templates/home.html
new file mode 100644
index 0000000..b90a18d
--- /dev/null
+++ b/bh20simplewebuploader/templates/home.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+  {% include 'header.html' %}
+    <body>
+      {% include 'banner.html' %}
+      {% include 'menu.html' %}
+
+      <section>
+                <div class="intro">
+                    <p>
+                        Make your sequence
+                        data <a href="https://en.wikipedia.org/wiki/FAIR_data">FAIR</a>. Upload
+                        your SARS-CoV-2 sequence (FASTA or FASTQ
+                        formats) with metadata (JSONLD) to
+                        the <a href="/about">public sequence
+                        resource</a>. The upload will trigger a
+                        recompute with all available sequences into a
+                        Pangenome available for
+                        <a href="/download">download</a>!
+                    </p>
+                    <p>
+                        Your uploaded sequence will automatically be
+                        processed and incorporated into the public
+                        pangenome with metadata using worklows from
+                        the High Performance Open Biology Lab
+                        defined <a href="https://github.com/hpobio-lab/viral-analysis/tree/master/cwl/pangenome-generate">here</a>. All
+                        data is published under
+                        a <a href="https://creativecommons.org/licenses/by/4.0/">Creative
+                        Commons license</a> You can take the published
+                        (GFA/RDF/FASTA) data and store it in a triple
+                        store for further processing.  Clinical
+                        data can be stored
+                        securely
+                        at <a href="https://redcap-covid19.elixir-luxembourg.org/redcap/">REDCap</a>.
+                    </p>
+                    <p>
+                      Note that form fields contain
+                      web <a href="https://en.wikipedia.org/wiki/Web_Ontology_Language">ontology
+                      URI's</a>
+                      for <a href="https://en.wikipedia.org/wiki/Wikipedia:Disambiguation">disambiguation</a>
+                      and machine readable metadata. For examples of
+                      use, see the <a href="/blog">BLOG</a>.
+                    </p>
+                </div>
+        </section>
+
+{% include 'footer.html' %}
+
+   </body>
+</html>
diff --git a/bh20simplewebuploader/templates/menu.html b/bh20simplewebuploader/templates/menu.html
index 6f97e19..0f6003f 100644
--- a/bh20simplewebuploader/templates/menu.html
+++ b/bh20simplewebuploader/templates/menu.html
@@ -1,7 +1,9 @@
 <section class="menu">
   <div class="topnav" id="myTopnav">
-    <a href="/" class="{{ 'active' if menu=='HOME' }}">COVID-19</a>
+    <a href="/" class="{{ 'active' if menu=='HOME' }}">PUBSEQ</a>
     <a href="/download" class="{{ 'active' if menu=='DOWNLOAD' }}">DOWNLOAD</a>
+    <a href="/upload" class="{{ 'active' if menu=='UPLOAD' }}">UPLOAD</a>
+    <a href="/status" class="{{ 'active' if menu=='STATUS' }}">STATUS</a>
     <a href="/demo" class="{{ 'active' if menu=='DEMO' }}">DEMO</a>
     <a href="/blog" class="{{ 'active' if menu=='BLOG' }}">BLOG</a>
     <a href="/about" class="{{ 'active' if menu=='ABOUT' }}">ABOUT</a>
diff --git a/bh20simplewebuploader/templates/resource.html b/bh20simplewebuploader/templates/resource.html
new file mode 100644
index 0000000..91b6c20
--- /dev/null
+++ b/bh20simplewebuploader/templates/resource.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+  {% include 'header.html' %}
+    <body>
+      {% include 'banner.html' %}
+      {% include 'menu.html' %}
+
+      <div class="status">
+	<p><img src="https://workbench.lugli.arvadosapi.com/collections/lugli-4zz18-z513nlpqm03hpca/relabeledSeqs_dedup_relabeledSeqs_dedup.png" height="300px"></p>
+	<p><a href="https://workbench.lugli.arvadosapi.com/projects/lugli-j7d0g-5ct8p1i1wrgyjvp#Data_collections">All sequences project</a></p>
+	<p><a href="https://workbench.lugli.arvadosapi.com/collections/lugli-4zz18-z513nlpqm03hpca/relabeledSeqs_dedup.fasta">All sequences (FASTA) relabled and deduplicated</a></p>
+	<p><a href="https://workbench.lugli.arvadosapi.com/collections/lugli-4zz18-z513nlpqm03hpca/mergedmetadata.ttl">Metadata (RDF) for all sequences</a></p>
+	<p><a href="https://workbench.lugli.arvadosapi.com/collections/lugli-4zz18-z513nlpqm03hpca/relabeledSeqs_dedup_relabeledSeqs_dedup.gfa">All sequences in Graphical Fragment Assembly (GFA)</a> - <a href="https://github.com/GFA-spec/GFA-spec">More about GFA</a></p>
+	<p><a href="https://workbench.lugli.arvadosapi.com/collections/lugli-4zz18-z513nlpqm03hpca/relabeledSeqs_dedup_relabeledSeqs_dedup.gfa">All sequences in Optimized Dynamic Genome/Graph Implementation (ODGI)</a> - <a href="https://github.com/vgteam/odgi">More about ODGI</a></p>
+	<p><a href="https://workbench.lugli.arvadosapi.com/collections/lugli-4zz18-z513nlpqm03hpca/relabeledSeqs_dedup_relabeledSeqs_dedup.ttl.xz">All sequences in RDF using spodgi</a> - <a href="https://github.com/pangenome/spodgi">More about spodgi</a></p>
+
+
+	<p><a href="http://sparql.genenetwork.org/sparql/">SPARQL endpoint</a> - <a href="http://sparql.genenetwork.org/sparql/?default-graph-uri=&query=SELECT+DISTINCT+%3Ffasta+%3Fvalue+WHERE+%7B%3Ffasta+%3Fx%5B+%3Chttp%3A%2F%2Fedamontology.org%2Fdata_2091%3E+%3Fvalue+%5D%7D%0D%0A&format=text%2Fhtml&timeout=0&debug=on&run=+Run+Query+">Sample query for accessions</a>
+
+	{{ embed|safe }}
+
+	</div>
+
+{% include 'footer.html' %}
+
+   </body>
+</html>
diff --git a/bh20simplewebuploader/templates/status.html b/bh20simplewebuploader/templates/status.html
new file mode 100644
index 0000000..a1cf28f
--- /dev/null
+++ b/bh20simplewebuploader/templates/status.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  {% include 'header.html' %}
+    <body>
+      {% include 'banner.html' %}
+      {% include 'menu.html' %}
+
+      <h1>Sequence upload processing status</h1>
+
+        <div class="status">
+	  {{ table }}
+        </div>
+
+{% include 'footer.html' %}
+
+   </body>
+</html>