about summary refs log tree commit diff
path: root/uploader
diff options
context:
space:
mode:
Diffstat (limited to 'uploader')
-rw-r--r--uploader/background_jobs.py18
-rw-r--r--uploader/base_routes.py5
-rw-r--r--uploader/oauth2/views.py14
-rw-r--r--uploader/phenotypes/views.py67
-rw-r--r--uploader/population/views.py38
-rw-r--r--uploader/samples/views.py4
-rw-r--r--uploader/species/views.py6
-rw-r--r--uploader/static/js/urls.js26
-rw-r--r--uploader/sui.py8
-rw-r--r--uploader/templates/background-jobs/sui-default-success-page.html17
-rw-r--r--uploader/templates/jobs/sui-job-error.html17
-rw-r--r--uploader/templates/jobs/sui-job-not-found.html11
-rw-r--r--uploader/templates/jobs/sui-job-status.html24
-rw-r--r--uploader/templates/phenotypes/macro-display-pheno-dataset-card.html28
-rw-r--r--uploader/templates/phenotypes/sui-add-phenotypes-base.html155
-rw-r--r--uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html829
-rw-r--r--uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html189
-rw-r--r--uploader/templates/phenotypes/sui-base.html25
-rw-r--r--uploader/templates/phenotypes/sui-job-status.html140
-rw-r--r--uploader/templates/phenotypes/sui-load-phenotypes-success.html26
-rw-r--r--uploader/templates/phenotypes/sui-review-job-data.html121
-rw-r--r--uploader/templates/populations/sui-base.html2
-rw-r--r--uploader/templates/populations/sui-view-population.html173
-rw-r--r--uploader/templates/publications/edit-publication.html4
-rw-r--r--uploader/templates/samples/sui-base.html19
-rw-r--r--uploader/templates/samples/sui-list-samples.html98
26 files changed, 1983 insertions, 81 deletions
diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py
index d33c498..fc59ec7 100644
--- a/uploader/background_jobs.py
+++ b/uploader/background_jobs.py
@@ -5,7 +5,7 @@ from typing import Callable
 from functools import partial
 
 from flask import (
-    url_for,
+    request,
     redirect,
     Response,
     Blueprint,
@@ -16,6 +16,10 @@ from gn_libs import jobs
 from gn_libs import sqlite3
 from gn_libs.jobs.jobs import JobNotFound
 
+
+from uploader.sui import sui_template
+
+from uploader.flask_extensions import url_for
 from uploader.authorisation import require_login
 
 background_jobs_bp = Blueprint("background-jobs", __name__)
@@ -76,7 +80,8 @@ def handler(job: dict, handler_type: str) -> HandlerType:
     ).get(handler_type)
     if bool(_handler):
         return _handler(job)
-    return render_template("background-jobs/default-success-page.html", job=job)
+    return render_template(sui_template("background-jobs/default-success-page.html"),
+                           job=job)
 
 
 error_handler = partial(handler, handler_type="error")
@@ -99,10 +104,10 @@ def job_status(job_id: uuid.UUID):
             if status == "completed":
                 return success_handler(job)
 
-            return render_template("jobs/job-status.html", job=job)
+            return render_template(sui_template("jobs/job-status.html"), job=job)
         except JobNotFound as _jnf:
             return render_template(
-                "jobs/job-not-found.html",
+                sui_template("jobs/job-not-found.html"),
                 job_id=job_id)
 
 
@@ -113,6 +118,7 @@ def job_error(job_id: uuid.UUID):
     with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
         try:
             job = jobs.job(conn, job_id, fulldetails=True)
-            return render_template("jobs/job-error.html", job=job)
+            return render_template(sui_template("jobs/job-error.html"), job=job)
         except JobNotFound as _jnf:
-            return render_template("jobs/job-not-found.html", job_id=job_id)
+            return render_template(sui_template("jobs/job-not-found.html"),
+                                   job_id=job_id)
diff --git a/uploader/base_routes.py b/uploader/base_routes.py
index 80a15a0..cc2a270 100644
--- a/uploader/base_routes.py
+++ b/uploader/base_routes.py
@@ -11,6 +11,9 @@ from flask import (flash,
                    current_app as app,
                    send_from_directory)
 
+
+from uploader.sui import sui_template
+
 from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
 from uploader.oauth2.client import user_logged_in
@@ -42,7 +45,7 @@ def index():
         print("We found a species ID. Processing...")
         if not bool(request.args.get("species_id")):
             return render_template(
-                "sui-index.html",# TODO: Rename: sui-index.html, sui_base.html
+                sui_template("index.html"),
                 gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], "/intro"),
                 species=all_species(conn),
                 streamlined_ui=streamlined_ui)
diff --git a/uploader/oauth2/views.py b/uploader/oauth2/views.py
index 05f8542..b1b740f 100644
--- a/uploader/oauth2/views.py
+++ b/uploader/oauth2/views.py
@@ -33,16 +33,16 @@ def authorisation_code():
         app.logger.debug("ERROR: (%s)", error_response.content)
         flash("There was an error retrieving the authorisation token.",
               "alert alert-danger")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     def __fail_set_user_details__(_failure):
         app.logger.debug("Fetching user details fails: %s", _failure)
         flash("Could not retrieve the user details", "alert alert-danger")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     def __success_set_user_details__(_success):
         app.logger.debug("Session info: %s", _success)
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     def __success__(token):
         session.set_user_token(token)
@@ -53,7 +53,7 @@ def authorisation_code():
     code = request.args.get("code", "").strip()
     if not bool(code):
         flash("AuthorisationError: No code was provided.", "alert alert-danger")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     baseurl = urlparse(request.base_url, scheme=request.scheme)
     return request_token(
@@ -87,7 +87,7 @@ def logout():
         _user_str = f"{_user['name']} ({_user['email']})"
         session.clear_session_info()
         flash("Successfully signed out.", "alert alert-success")
-        return redirect("/")
+        return redirect(url_for("base.index"))
 
     if user_logged_in():
         return session.user_token().then(
@@ -100,9 +100,9 @@ def logout():
                     "client_secret": oauth2_clientsecret()
                 })).either(
                     make_error_handler(
-                        redirect_to=redirect("/"),
+                        redirect_to=redirect(url_for("base.index")),
                         cleanup_thunk=lambda: __unset_session__(
                             session.session_info())),
                     lambda res: __unset_session__(session.session_info()))
     flash("There is no user that is currently logged in.", "alert alert-info")
-    return redirect("/")
+    return redirect(url_for("base.index"))
diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py
index 2afd8a3..5b32fc0 100644
--- a/uploader/phenotypes/views.py
+++ b/uploader/phenotypes/views.py
@@ -32,6 +32,8 @@ from r_qtl import r_qtl2_qc as rqc
 from r_qtl import exceptions as rqe
 
 
+from uploader.sui import sui_template
+
 from uploader import jobs
 from uploader import session
 from uploader.files import save_file
@@ -183,6 +185,12 @@ def with_dataset(
 def view_dataset(# pylint: disable=[unused-argument]
         species: dict, population: dict, dataset: dict, **kwargs):
     """View a specific dataset"""
+    if bool(request.args.get("streamlined_ui")):
+        # Redirect back to the "View Population" page for the time being.
+        return redirect(url_for("species.populations.view_population",
+                                species_id=species["SpeciesId"],
+                                population_id=population["Id"]))
+
     with database_connection(app.config["SQL_URI"]) as conn:
         dataset = dataset_by_id(
             conn, species["SpeciesId"], population["Id"], dataset["Id"])
@@ -418,8 +426,9 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
         if request.method == "GET":
             today = datetime.date.today()
             return render_template(
-                ("phenotypes/add-phenotypes-with-rqtl2-bundle.html"
-                 if use_bundle else "phenotypes/add-phenotypes-raw-files.html"),
+                sui_template("phenotypes/add-phenotypes-with-rqtl2-bundle.html"
+                             if use_bundle
+                             else f"phenotypes/add-phenotypes-raw-files.html"),
                 species=species,
                 population=population,
                 dataset=dataset,
@@ -505,7 +514,8 @@ def job_status(
             job = jobs.job(rconn, jobs.jobsnamespace(), str(job_id))
         except jobs.JobNotFound as _jnf:
             job = None
-        return render_template("phenotypes/job-status.html",
+
+        return render_template(sui_template("phenotypes/job-status.html"),
                                species=species,
                                population=population,
                                dataset=dataset,
@@ -585,7 +595,7 @@ def review_job_data(
             for filetype,meta in metadata.items()
         }
         _job_metadata = json.loads(job["job-metadata"])
-        return render_template("phenotypes/review-job-data.html",
+        return render_template(sui_template("phenotypes/review-job-data.html"),
                                species=species,
                                population=population,
                                dataset=dataset,
@@ -975,33 +985,34 @@ def load_data_success(
                               _publication["Authors"],
                               (_publication["Title"] or ""))
                              if item != "")
-            return render_template("phenotypes/load-phenotypes-success.html",
-                                   species=species,
-                                   population=population,
-                                   dataset=dataset,
-                                   job=job,
-                                   search_page_uri=urlunparse(ParseResult(
-                                       scheme=gn2_uri.scheme,
-                                       netloc=gn2_uri.netloc,
-                                       path="/search",
-                                       params="",
-                                       query=urlencode({
-                                           "species": species["Name"],
-                                           "group": population["Name"],
-                                           "type": "Phenotypes",
-                                           "dataset": dataset["Name"],
-                                           "search_terms_or": (
-                                               # Very long URLs will cause
-                                               # errors.
+            return render_template(
+                sui_template("phenotypes/load-phenotypes-success.html"),
+                species=species,
+                population=population,
+                dataset=dataset,
+                job=job,
+                search_page_uri=urlunparse(ParseResult(
+                    scheme=gn2_uri.scheme,
+                    netloc=gn2_uri.netloc,
+                    path="/search",
+                    params="",
+                    query=urlencode({
+                        "species": species["Name"],
+                        "group": population["Name"],
+                        "type": "Phenotypes",
+                        "dataset": dataset["Name"],
+                        "search_terms_or": (
+                            # Very long URLs will cause
+                            # errors.
                                                " ".join(_xref_ids)
                                                if len(_xref_ids) <= 100
                                                else ""),
-                                           "search_terms_and": " ".join(
-                                               _search_terms).strip(),
-                                           "accession_id": "None",
-                                           "FormID": "searchResult"
-                                       }),
-                                       fragment="")))
+                        "search_terms_and": " ".join(
+                            _search_terms).strip(),
+                        "accession_id": "None",
+                        "FormID": "searchResult"
+                    }),
+                    fragment="")))
         except JobNotFound as _jnf:
             return render_template("jobs/job-not-found.html", job_id=job_id)
 
diff --git a/uploader/population/views.py b/uploader/population/views.py
index 8d4ceb7..a6e2358 100644
--- a/uploader/population/views.py
+++ b/uploader/population/views.py
@@ -11,6 +11,8 @@ from flask import (flash,
                    Blueprint,
                    current_app as app)
 
+from uploader.sui import sui_template
+
 from uploader.samples.views import samplesbp
 from uploader.flask_extensions import url_for
 from uploader.oauth2.client import oauth2_post
@@ -19,11 +21,12 @@ from uploader.authorisation import require_login
 from uploader.genotypes.views import genotypesbp
 from uploader.datautils import enumerate_sequence
 from uploader.phenotypes.views import phenotypesbp
-from uploader.phenotypes.models import datasets_by_population
 from uploader.expression_data.views import exprdatabp
 from uploader.species.models import all_species, species_by_id
 from uploader.monadic_requests import make_either_error_handler
 from uploader.input_validation import is_valid_representative_name
+from uploader.phenotypes.models import (dataset_phenotypes,
+                                        datasets_by_population)
 
 from .models import (save_population,
                      population_families,
@@ -219,12 +222,27 @@ def view_population(species_id: int, population_id: int):
                                     species_id=species["SpeciesId"],
                                     streamlined_ui=streamlined_ui))
 
-        return render_template(("populations/sui-view-population.html"
-                                if bool(streamlined_ui)
-                                else "populations/view-population.html"),
-                               species=species,
-                               population=population,
-                               **({"dataset": datasets[0]} if len(datasets) == 1 else {}),
-                               activelink="view-population",
-                               streamlined_ui=streamlined_ui,
-                               view_under_construction=request.args.get("view_under_construction", False))
+        _datasets = datasets_by_population(
+            conn, species["SpeciesId"], population["Id"])
+        assert len(datasets) == 0 or len(datasets) == 1, (
+            "We expect only one phenotypes dataset per population.")
+        _kwargs = {
+            "species": species,
+            "population": population,
+            "activelink": "view-population",
+            "streamlined_ui": streamlined_ui,
+            "view_under_construction": request.args.get(
+                "view_under_construction", False)
+        }
+
+        if len(_datasets) == 1:
+            _dataset = _datasets[0]
+            _kwargs = {
+                **_kwargs,
+                "dataset": _dataset,
+                "phenotypes": enumerate_sequence(
+                    dataset_phenotypes(conn, population["Id"], _dataset["Id"]))
+            }
+
+        return render_template(sui_template("populations/view-population.html"),
+                               **_kwargs)
diff --git a/uploader/samples/views.py b/uploader/samples/views.py
index f8baf7e..fcb895d 100644
--- a/uploader/samples/views.py
+++ b/uploader/samples/views.py
@@ -11,6 +11,8 @@ from flask import (flash,
                    Blueprint,
                    current_app as app)
 
+from uploader.sui import sui_template
+
 from uploader import jobs
 from uploader.files import save_file
 from uploader.flask_extensions import url_for
@@ -86,7 +88,7 @@ def list_samples(species: dict, population: dict, **kwargs):# pylint: disable=[u
         total_samples = len(all_samples)
         offset = max(safe_int(request.args.get("from") or 0), 0)
         count = int(request.args.get("count") or 20)
-        return render_template("samples/list-samples.html",
+        return render_template(sui_template("samples/list-samples.html"),
                                species=species,
                                population=population,
                                samples=all_samples[offset:offset+count],
diff --git a/uploader/species/views.py b/uploader/species/views.py
index 20acd01..9b14d01 100644
--- a/uploader/species/views.py
+++ b/uploader/species/views.py
@@ -8,6 +8,8 @@ from flask import (flash,
                    Blueprint,
                    current_app as app)
 
+from uploader.sui import sui_template
+
 from uploader.population import popbp
 from uploader.platforms import platformsbp
 from uploader.flask_extensions import url_for
@@ -54,9 +56,7 @@ def view_species(species_id: int):
                                         species_id=species_id,
                                         population_id=population["Id"]))
             return render_template(
-                ("species/sui-view-species.html"
-                 if bool(streamlined_ui)
-                 else "species/view-species.html"),
+                sui_template("species/view-species.html"),
                 species=species,
                 activelink="view-species",
                 populations=populations_by_species(conn, species["SpeciesId"]))
diff --git a/uploader/static/js/urls.js b/uploader/static/js/urls.js
new file mode 100644
index 0000000..e3fb7c6
--- /dev/null
+++ b/uploader/static/js/urls.js
@@ -0,0 +1,26 @@
+function baseURL() {
+    return new URL(`${window.location.protocol}//${window.location.host}`);
+};
+
+function buildURLFromCurrentURL(pathname, searchParams = new URLSearchParams()) {
+    var uri = baseURL();
+    uri.pathname=pathname;
+    var _search = new URLSearchParams(window.location.search);
+    searchParams.forEach(function(value, key) {
+        _search.set(key, value);
+    });
+    uri.search = _search.toString();
+    return uri
+};
+
+function deleteSearchParams(url, listOfParams = []) {
+    _params = new URLSearchParams(url.search);
+    listOfParams.forEach(function(paramName) {
+        _params.delete(paramName);
+    });
+
+    
+    newUrl = new URL(url.toString());
+    newUrl.search = _params.toString();
+    return newUrl;
+}
diff --git a/uploader/sui.py b/uploader/sui.py
new file mode 100644
index 0000000..8eb863d
--- /dev/null
+++ b/uploader/sui.py
@@ -0,0 +1,8 @@
+"""Utilities for streamlined UI. This is a temporary module."""
+from flask import request
+
+def sui_template(template_path: str) -> str:
+    """Return the streamlined UI template for given template path."""
+    _sui="sui-" if request.args.get("streamlined_ui") else ""
+    _parts = template_path.split("/")
+    return "/".join(_parts[:-1] + [f"{_sui}{_parts[-1]}"])
diff --git a/uploader/templates/background-jobs/sui-default-success-page.html b/uploader/templates/background-jobs/sui-default-success-page.html
new file mode 100644
index 0000000..5732456
--- /dev/null
+++ b/uploader/templates/background-jobs/sui-default-success-page.html
@@ -0,0 +1,17 @@
+{%extends "phenotypes/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Background Jobs: Success{%endblock%}
+
+{%block pagetitle%}Background Jobs: Success{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <p>Job <strong>{{job.job_id}}</strong>,
+    {%if job.get("metadata", {}).get("job-type")%}
+    of type '<em>{{job.metadata["job-type"]}}</em>
+    {%endif%}' completed successfully.</p>
+</div>
+{%endblock%}
diff --git a/uploader/templates/jobs/sui-job-error.html b/uploader/templates/jobs/sui-job-error.html
new file mode 100644
index 0000000..1a839a6
--- /dev/null
+++ b/uploader/templates/jobs/sui-job-error.html
@@ -0,0 +1,17 @@
+{%extends "sui-base.html"%}
+
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Background Jobs: Error{%endblock%}
+
+{%block pagetitle%}Background Jobs: Error{%endblock%}
+
+{%block contents%}
+
+<h1>Background Jobs: Error</h1>
+<p>Job <strong>{{job["job_id"]}}</strong> failed!</p>
+<p>The error details are in the "STDERR" section below.</p>
+
+<h2>STDERR</h2>
+<pre>{{job["stderr"]}}</pre>
+{%endblock%}
diff --git a/uploader/templates/jobs/sui-job-not-found.html b/uploader/templates/jobs/sui-job-not-found.html
new file mode 100644
index 0000000..96c8586
--- /dev/null
+++ b/uploader/templates/jobs/sui-job-not-found.html
@@ -0,0 +1,11 @@
+{%extends "sui-base.html"%}
+
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block contents%}
+<p>Could not find job with ID: {{job_id}}</p>
+{%endblock%}
diff --git a/uploader/templates/jobs/sui-job-status.html b/uploader/templates/jobs/sui-job-status.html
new file mode 100644
index 0000000..fc5e532
--- /dev/null
+++ b/uploader/templates/jobs/sui-job-status.html
@@ -0,0 +1,24 @@
+{%extends "sui-base.html"%}
+
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block extrameta%}
+<meta http-equiv="refresh" content="5" />
+{%endblock%}
+
+{%block title%}Background Jobs{%endblock%}
+
+{%block pagetitle%}Background Jobs{%endblock%}
+
+{%block contents%}
+
+<p>Status: {{job["metadata"]["status"]}}</p>
+<p>Job Type: {{job["metadata"]["job-type"]}}</p>
+
+<h2>STDOUT</h2>
+<pre>{{job["stdout"]}}</pre>
+
+<h2>STDERR</h2>
+<pre>{{job["stderr"]}}</pre>
+
+{%endblock%}
diff --git a/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html b/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html
index 11b108b..641421d 100644
--- a/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html
+++ b/uploader/templates/phenotypes/macro-display-pheno-dataset-card.html
@@ -1,4 +1,4 @@
-{%from "populations/macro-display-population-card.html" import display_population_card%}
+{%from "populations/macro-display-population-card.html" import display_population_card, display_sui_population_card%}
 
 {%macro display_pheno_dataset_card(species, population, dataset)%}
 {{display_population_card(species, population)}}
@@ -29,3 +29,29 @@
   </div>
 </div>
 {%endmacro%}
+
+{%macro display_sui_pheno_dataset_card(species, population, dataset)%}
+{{display_sui_population_card(species, population)}}
+
+<div class="row">
+  <table class="table">
+    <caption>Current dataset</caption>
+    <tbody>
+      <tr>
+        <th>Name</th>
+        <td>{{dataset.Name}}</td>
+      </tr>
+
+      <tr>
+        <th>Full Name</th>
+        <td>{{dataset.FullName}}</td>
+      </tr>
+
+      <tr>
+        <th>Short Name</th>
+        <td>{{dataset.ShortName}}</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+{%endmacro%}
diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-base.html b/uploader/templates/phenotypes/sui-add-phenotypes-base.html
new file mode 100644
index 0000000..1e71267
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-add-phenotypes-base.html
@@ -0,0 +1,155 @@
+{%extends "phenotypes/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block contents%}
+{{super()}}
+{{flash_all_messages()}}
+
+<div class="row">
+  <form id="frm-add-phenotypes"
+        method="POST"
+        enctype="multipart/form-data"
+        action="{{url_for('species.populations.phenotypes.add_phenotypes',
+                species_id=species.SpeciesId,
+                population_id=population.Id,
+                dataset_id=dataset.Id,
+                use_bundle=use_bundle)}}"
+        data-resumable-target="{{url_for('files.resumable_upload_post')}}">
+    <legend>Add New Phenotypes</legend>
+
+    <div class="form-text help-block">
+      {%block frm_add_phenotypes_documentation%}{%endblock%}
+      <p><strong class="text-warning">This will not update any existing phenotypes!</strong></p>
+    </div>
+
+    {%block frm_add_phenotypes_elements%}{%endblock%}
+
+    <fieldset id="fldset-publication-info">
+      <legend>Publication Information</legend>
+      <input type="hidden" name="publication-id" id="txt-publication-id" />
+      <span class="form-text text-muted">
+        Select a publication for your data. <br />
+        Can't find a publication you can use? Go ahead and
+        <a href="{{url_for(
+                 'publications.create_publication',
+                 return_to='species.populations.phenotypes.add_phenotypes',
+                 species_id=species.SpeciesId,
+                 population_id=population.Id,
+                 dataset_id=dataset.Id)}}">create a new publication</a>.</span>
+      <table id="tbl-select-publication" class="table compact stripe">
+        <thead>
+          <tr>
+            <th>#</th>
+            <th>PubMed ID</th>
+            <th>Title</th>
+            <th>Authors</th>
+          </tr>
+        </thead>
+
+        <tbody></tbody>
+      </table>
+    </fieldset>
+
+    <div class="form-group">
+      <input type="submit"
+             value="upload phenotypes"
+             class="btn btn-primary" />
+    </div>
+  </form>
+</div>
+
+<div class="row">
+  {%block page_documentation%}{%endblock%}
+</div>
+
+{%endblock%}
+
+
+
+
+{%block javascript%}
+<script type="text/javascript">
+  $(function() {
+      var publicationsDataTable = buildDataTable(
+          "#tbl-select-publication",
+          [],
+          [
+              {data: "index"},
+              {
+                  searchable: true,
+                  data: (pub) => {
+                      if(pub.PubMed_ID) {
+                          return `<a href="https://pubmed.ncbi.nlm.nih.gov/` +
+                              `${pub.PubMed_ID}/" target="_blank" ` +
+                              `title="Link to publication on NCBI.">` +
+                              `${pub.PubMed_ID}</a>`;
+                      }
+                      return "";
+                  }
+              },
+              {
+                  searchable: true,
+                  data: (pub) => {
+                      var title = "⸻";
+                      if(pub.Title) {
+                          title = pub.Title
+                      }
+                      return `<a href="/publications/view/${pub.Id}" ` +
+                          `target="_blank" ` +
+                          `title="Link to view publication details">` +
+                          `${title}</a>`;
+                  }
+              },
+              {
+                  searchable: true,
+                  data: (pub) => {
+                      authors = pub.Authors.split(",").map(
+                          (item) => {return item.trim();});
+                      if(authors.length > 1) {
+                          return authors[0] + ", et. al.";
+                      }
+                      return authors[0];
+                  }
+              }
+          ],
+          {
+              serverSide: true,
+              ajax: {
+                  url: "/publications/list",
+                  dataSrc: "publications"
+              },
+              select: "single",
+              paging: true,
+              scrollY: 700,
+              deferRender: true,
+              scroller: true,
+              scrollCollapse: true,
+              layout: {
+                  topStart: "info",
+                  topEnd: "search"
+              }
+          });
+      publicationsDataTable.on("select", (event, datatable, type, indexes) => {
+          indexes.forEach((element, index, thearray) => {
+              let row = datatable.row(element).node();
+              console.debug(datatable.row(element).data());
+              $("#frm-add-phenotypes #txt-publication-id").val(
+                  datatable.row(element).data().Id);
+          });
+      });
+      publicationsDataTable.on("deselect", (event, datatable, type, indexes) => {
+          indexes.forEach((element, index, thearray) => {
+              let row = datatable.row(element).node();
+              $("#frm-add-phenotypes #txt-publication-id").val(null);
+          });
+      });
+  });
+</script>
+
+{%block more_javascript%}{%endblock%}
+{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html b/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html
new file mode 100644
index 0000000..6038617
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html
@@ -0,0 +1,829 @@
+{%extends "phenotypes/sui-add-phenotypes-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+{%from "phenotypes/macro-display-preview-table.html" import display_preview_table%}
+{%from "phenotypes/macro-display-resumable-elements.html" import display_resumable_elements%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block frm_add_phenotypes_documentation%}
+<p>This page will allow you to upload all the separate files that make up your
+  phenotypes. Here, you will have to upload each separate file individually. If
+  you want instead to upload all your files as a single ZIP file,
+  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id,
+           use_bundle=true)}}"
+     title="">click here</a>.</p>
+{%endblock%}
+
+{%block frm_add_phenotypes_elements%}
+<fieldset id="fldset-file-metadata">
+  <legend>File(s) Metadata</legend>
+  <div class="form-group">
+    <label for="txt-file-separator" class="form-label">File Separator</label>
+    <div class="input-group">
+      <input id="txt-file-separator"
+             name="file-separator"
+             type="text"
+             value="&#9;"
+             class="form-control"
+             maxlength="1" />
+      <span class="input-group-btn">
+        <button id="btn-reset-file-separator" class="btn btn-info">Reset Default</button>
+      </span>
+    </div>
+    <span class="form-text text-muted">
+      Provide the character that separates the fields in your file(s). It should
+      be the same character for all files (if more than one is provided).<br />
+      A tab character will be assumed if you leave this field blank. See
+      <a href="#docs-file-separator"
+         title="Documentation for file-separator characters">
+        documentation for more information</a>.
+    </span>
+  </div>
+
+  <div class="form-group">
+    <label for="txt-file-comment-character" class="form-label">File Comment-Character</label>
+    <div class="input-group">
+      <input id="txt-file-comment-character"
+             name="file-comment-character"
+             type="text"
+             value="#"
+             class="form-control"
+             maxlength="1" />
+      <span class="input-group-btn">
+        <button id="btn-reset-file-comment-character" class="btn btn-info">
+          Reset Default</button>
+      </span>
+    </div>
+    <span class="form-text text-muted">
+      This specifies that lines that begin with the character provided will be
+      considered comment lines and ignored in their entirety. See
+      <a href="#docs-file-comment-character"
+         title="Documentation for comment characters">
+        documentation for more information</a>.
+    </span>
+  </div>
+
+  <div class="form-group">
+    <label for="txt-file-na" class="form-label">File "No-Value" Indicators</label>
+    <div class="input-group">
+      <input id="txt-file-na"
+             name="file-na"
+             type="text"
+             value="- NA N/A"
+             class="form-control" />
+      <span class="input-group-btn">
+        <button id="btn-reset-file-na" class="btn btn-info">Reset Default</button>
+      </span>
+    </div>
+    <span class="form-text text-muted">
+      This specifies strings in your file indicate that there is no value for a
+      particular cell (a cell is where a column and row intersect). Provide a
+      space-separated list of strings if you have more than one way of
+      indicating no values. See
+      <a href="#docs-file-na" title="Documentation for no-value fields">
+        documentation for more information</a>.</span>
+  </div>
+</fieldset>
+
+<fieldset id="fldset-files">
+  <legend>Data File(s)</legend>
+
+  <fieldset id="fldset-descriptions-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-descriptions-transposed"
+               name="phenotype-descriptions-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-descriptions-transposed"
+               class="form-check-label">
+          Description file transposed?</label>
+      </div>
+
+      <div class="non-resumable-elements">
+        <label for="finput-phenotype-descriptions" class="form-label">
+          Phenotype Descriptions</label>
+        <input id="finput-phenotype-descriptions"
+               name="phenotype-descriptions"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-desc"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the phenotype descriptions,
+          <a href="#docs-file-phenotype-description"
+             title="Documentation of the phenotype data file format.">
+            the documentation for the expected format of the file</a>.</span>
+      </div>
+      {{display_resumable_elements(
+      "resumable-phenotype-descriptions",
+      "phenotype descriptions",
+      '<p>Drag and drop the CSV file that contains the descriptions of your
+        phenotypes here.</p>
+
+      <p>The CSV file should be a matrix of
+        <strong>phenotypes × descriptions</strong> i.e. The first column
+        contains the phenotype names/identifiers whereas the first row is a list
+        of metadata fields like, "description", "units", etc.</p>
+
+      <p>If the format is transposed (i.e.
+        <strong>descriptions × phenotypes</strong>) select the checkbox above.
+      </p>
+
+      <p>Please see the
+        <a href="#docs-file-phenotype-description"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Descriptions" documentation</a> section below for more
+        information on the expected format of the file provided here.</p>')}}
+      {{display_preview_table(
+      "tbl-preview-pheno-desc", "phenotype descriptions")}}
+    </div>
+  </fieldset>
+
+
+  <fieldset id="fldset-data-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-data-transposed"
+               name="phenotype-data-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-data-transposed" class="form-check-label">
+          Data file transposed?</label>
+      </div>
+
+      <div class="non-resumable-elements">
+        <label for="finput-phenotype-data" class="form-label">Phenotype Data</label>
+        <input id="finput-phenotype-data"
+               name="phenotype-data"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-data"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the phenotype data. See
+          <a href="#docs-file-phenotype-data"
+             title="Documentation of the phenotype data file format.">
+            the documentation for the expected format of the file</a>.</span>
+      </div>
+
+      {{display_resumable_elements(
+      "resumable-phenotype-data",
+      "phenotype data",
+      '<p>Drag and drop a CSV file that contains the phenotypes numerical data
+        here. You can click the "Browse" button (below and to the right) to
+        select the file from your computer.</p>
+
+      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
+        i.e. The first column contains the samples identifiers while the first
+        row is the list of phenotypes identifiers occurring in the phenotypes
+        descriptions file.</p>
+
+      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
+        select the checkbox above.</p>
+      <p>Please see the
+        <a href="#docs-file-phenotype-data"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Data" documentation</a> section below for more information
+        on the expected format for the file provided here.</p>')}}
+      {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}}
+    </div>
+  </fieldset>
+
+  
+  {%if population.Family in families_with_se_and_n%}
+  <fieldset id="fldset-se-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-se-transposed"
+               name="phenotype-se-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-se-transposed" class="form-check-label">
+          Standard-Errors file transposed?</label>
+      </div>
+      <div class="group non-resumable-elements">
+        <label for="finput-phenotype-se" class="form-label">Phenotype: Standard Errors</label>
+        <input id="finput-phenotype-se"
+               name="phenotype-se"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-se"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the standard errors for the phenotypes,
+          computed from the data above.</span>
+      </div>
+
+      {{display_resumable_elements(
+      "resumable-phenotype-se",
+      "standard errors",
+      '<p>Drag and drop a CSV file that contains the phenotypes standard-errors
+        data here. You can click the "Browse" button (below and to the right) to
+        select the file from your computer.</p>
+
+      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
+        i.e. The first column contains the samples identifiers while the first
+        row is the list of phenotypes identifiers occurring in the phenotypes
+        descriptions file.</p>
+
+      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
+        select the checkbox above.</p>
+
+      <p>Please see the
+        <a href="#docs-file-phenotype-se"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Data" documentation</a> section below for more information
+        on the expected format of the file provided here.</p>')}}
+
+      {{display_preview_table("tbl-preview-pheno-se", "standard errors")}}
+    </div>
+  </fieldset>
+
+
+  <fieldset id="fldset-n-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-n-transposed"
+               name="phenotype-n-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-n-transposed" class="form-check-label">
+          Counts file transposed?</label>
+      </div>
+      <div class="non-resumable-elements">
+        <label for="finput-phenotype-n" class="form-label">Phenotype: Number of Samples/Individuals</label>
+        <input id="finput-phenotype-n"
+               name="phenotype-n"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-n"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the number of samples/individuals used in
+          the computation of the standard errors above.</span>
+      </div>
+
+      {{display_resumable_elements(
+      "resumable-phenotype-n",
+      "number of samples/individuals",
+      '<p>Drag and drop a CSV file that contains the samples\' phenotypes counts
+        data here. You can click the "Browse" button (below and to the right) to
+        select the file from your computer.</p>
+
+      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
+        i.e. The first column contains the samples identifiers while the first
+        row is the list of phenotypes identifiers occurring in the phenotypes
+        descriptions file.</p>
+
+      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
+        select the checkbox above.</p>
+
+      <p>Please see the
+        <a href="#docs-file-phenotype-se"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Data" documentation</a> section below for more information
+        on the expected format of the file provided here.</p>')}}
+
+      {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}}
+    </div>
+  </fieldset>
+</fieldset>
+{%endif%}
+{%endblock%}
+
+
+{%block page_documentation%}
+<div class="row">
+  <h2 class="heading" id="docs-help">Help</h2>
+  <h3 class="subheading">Common Features</h3>
+  <p>The following are the common expectations for <strong>ALL</strong> the
+    files provided in the form above:
+    <ul>
+      <li>The file <strong>MUST</strong> be character-separated values (CSV)
+        text file</li>
+      <li>The first row in the file <strong>MUST</strong> be a heading row, and
+        will be composed of the list identifiers for all of
+        samples/individuals/cases involved in your study.</li>
+      <li>The first column of data in the file <strong>MUST</strong> be the
+        identifiers for all of the phenotypes you wish to upload.</li>
+    </ul>
+  </p>
+
+  <p>If you do not specify the separator character, then we will assume a
+    <strong>TAB</strong> character was used as your separator.</p>
+
+  <p>We also assume you might include comments lines in your files. In that
+    case, if you do not specify what character denotes that a line in your files
+    is a comment line, we will assume the <strong>#</strong> character.<br />
+    A comment <strong>MUST ALWAYS</strong> begin at the start of the line marked
+    with the comment character specified.</p>
+
+  <h3 class="subheading" id="docs-file-metadata">File Metadata</h3>
+  <p>We request some details about your files to help us parse and process the
+    files correctly. The details we collect are:</p>
+  <dl>
+    <dt id="docs-file-separator">File separator</dt>
+    <dd>The files you provide should be character-separated value (CSV) files.
+      We need to know what character you used to separate the values in your
+      file. Some common ones are the Tab character, the comma, etc.<br />
+      Providing that information makes it possible for the system to parse and
+      process your files correctly.<br>
+      <strong>NOTE:</strong> All the files you upload MUST use the same
+      separator.</dd>
+
+    <dt id="docs-file-comment-character">Comment character</dt>
+    <dd>We support use of comment lines in your files. We only support one type
+      of comment style, the <em>line comment</em>.<br />
+      This mean the comment begins at the start of the line, and the end of that
+      line indicates the end of that comment. If you have a really long comment,
+      then you need to break it across multiple lines, marking each line a
+      comment line.<br />
+      The "comment character" is the character at the start of the line that
+      indicates that the line is a line comment.</dd>
+
+    <dt id="docs-file-na">No-Value indicator(s)</dt>
+    <dd>Data in the real world is messy, and in some cases, entirely absent. You
+      need to indicate, in your files, that a particular field did not have a
+      value, and once you do that, you then need to let the system know how you
+      mark such fields. Common ways of indicating "empty values" are, leaving
+      the field blank, using a character such as '-', or using strings like
+      "NA", "N/A", "NULL", etc.<br />
+      Providing this information will help with parsing and processing such
+      no-value fields the correct way.</dd>
+  </dl>
+
+  <h3 class="subheading" id="docs-file-phenotype-description">
+    file: Phenotypes Descriptions</h3>
+  <p>The data in this file is a matrix of <em>phenotypes × metadata-fields</em>.
+    Please note we use the term "metadata-fields" above loosely, due to lack of
+    a good word for this.</p>
+  <p>The file <strong>MUST</strong> have columns in this order:
+    <dl>
+      <dt>Phenotype Identifiers</dt>
+      <dd>These are the names/identifiers for your phenotypes. These
+        names/identifiers are the same ones you will have in all the other files you are
+        uploading.</dd>
+
+      <dt>Descriptions</dt>
+      <dd>Each phenotype will need a description. Good description are necessary
+        to inform other people of what the data is about. Good description are
+        hard to construct, so we provide
+        <a href="https://info.genenetwork.org/faq.php#q-22"
+           title="How to write phenotype descriptions">
+          advice on describing your phenotypes.</a></dd>
+
+      <dt>Units</dt>
+      <dd>Each phenotype will need units for the measurements taken. If there are
+        none, then indicate the field is a no-value field.</dd>
+  </dl></p>
+  <p>You can add more columns after those three if you want to, but these 3
+    <strong>MUST</strong> be present.</p>
+  <p>The file would, for example, look like the following:</p>
+  <code>id,description,units,…<br />
+    pheno10001|Central nervous system, behavior, cognition; …|mg|…<br />
+    pheno10002|Aging, metabolism, central nervous system: …|mg|…<br />
+    ⋮<br /></code>
+
+  <p><strong>Note 01</strong>: The first usable row is the heading row.</p>
+  <p><strong>Note 02: </strong>This example demonstrates a subtle issue that
+    could make your CSV file invalid &mdash; the choice of your field separator
+    character.<br >
+    In the example above, we use the pipe character (<code>|</code>) as our
+    field separator. This is because, if we follow the advice on how to write
+    good descriptions, then we cannot use the comma as our separator &ndash; if
+    we did, then our CSV file would be invalid because the system would have no
+    way to tell the difference between the comma as a field separator, and the
+    comma as a way to separate the "general category and ontology terms".</p>
+
+  <h3 class="subheading">file: Phenotype Data, Standard Errors and/or Sample Counts</h3>
+  <span id="docs-file-phenotype-data"></span>
+  <span id="docs-file-phenotype-se"></span>
+  <span id="docs-file-phenotype-n"></span>
+  <p>The data is a matrix of <em>samples(or individuals) × phenotypes</em>, e.g.</p>
+  <code>
+    # num-cases: 2549
+    # num-phenos: 13
+    id,pheno10001,pheno10002,pheno10003,pheno10004,53.099998,…<br />
+    IND001,61.400002,49,62.5,55.099998,…<br />
+    IND002,54.099998,50.099998,53.299999,55.099998,…<br />
+    IND003,483,403,501,403,…<br />
+    IND004,49.799999,45.5,62.900002,NA,…<br />
+    ⋮<br /></code>
+
+  <p>where <code>IND001,IND002,IND003,IND004,…</code> are the
+    samples/individuals/cases in your study, and
+    <code>pheno10001,pheno10002,pheno10004,pheno10004,…</code> are the
+    identifiers for your phenotypes.</p>
+  <p>The lines beginning with the "<em>#</em>" symbol (i.e.
+    <code># num-cases: 2549</code> and <code># num-phenos: 13</code> are comment
+    lines and will be ignored</p>
+  <p>In this example, the comma (,) is used as the file separator.</p>
+</div>
+
+{%endblock%}
+
+
+{%block more_javascript%}
+<script src="{{url_for('base.node_modules',
+             filename='resumablejs/resumable.js')}}"></script>
+<script type="text/javascript" src="/static/js/files.js"></script>
+
+<script type="text/javascript">
+  $("#btn-reset-file-separator").on("click", (event) => {
+      event.preventDefault();
+      $("#txt-file-separator").val("\t");
+      $("#txt-file-separator").trigger("change");
+  });
+  $("#btn-reset-file-comment-character").on("click", (event) => {
+      event.preventDefault();
+      $("#txt-file-comment-character").val("#");
+      $("#txt-file-comment-character").trigger("change");
+  });
+  $("#btn-reset-file-na").on("click", (event) => {
+      event.preventDefault();
+      $("#txt-file-na").val("- NA N/A");
+      $("#txt-file-na").trigger("change");
+  });
+
+  var update_preview = (table, filedata, formdata, numrows) => {
+      table.find("thead tr").remove()
+      table.find(".data-row").remove();
+      var linenum = 0;
+      var tableheader = table.find("thead");
+      var tablebody = table.find("tbody");
+      var numheadings = 0;
+      var navalues = formdata
+          .na_strings
+          .split(" ")
+          .map((v) => {return v.trim();})
+          .filter((v) => {return Boolean(v);});
+      filedata.forEach((line) => {
+          if(line.startsWith(formdata.comment_char) || linenum >= numrows) {
+              return false;
+          }
+          var row = $("<tr></tr>");
+          line.split(formdata.separator)
+              .map((field) => {
+                  var value = field.trim();
+                  if(navalues.includes(value)) {
+                      return "⋘NUL⋙";
+                  }
+                  return value;
+              })
+              .filter((field) => {
+                  return (field !== "" && field != undefined && field != null);
+              })
+              .forEach((field) => {
+                  if(linenum == 0) {
+                      numheadings += 1;
+                      var tablefield = $("<th></th>");
+                      tablefield.text(field);
+                      row.append(tablefield);
+                  } else {
+                      add_class(row, "data-row");
+                      var tablefield = $("<td></td>");
+                      tablefield.text(field);
+                      row.append(tablefield);
+                  }
+              });
+
+          if(linenum == 0) {
+              tableheader.append(row);
+          } else {
+              tablebody.append(row);
+          }
+          linenum += 1;
+      });
+
+      if(table.find("tbody tr.data-row").length > 0) {
+          add_class(table.find(".data-row-template"), "visually-hidden");
+      } else {
+          remove_class(table.find(".data-row-template"), "visually-hidden");
+      }
+  };
+
+  var makePreviewUpdater = (preview_table) => {
+      return (data) => {
+          update_preview(
+              preview_table,
+              data,
+              filesMetadata(),
+              PREVIEW_ROWS);
+      };
+  };
+
+  var preview_tables_to_elements_map = {
+      "#tbl-preview-pheno-desc": "#finput-phenotype-descriptions",
+      "#tbl-preview-pheno-data": "#finput-phenotype-data",
+      "#tbl-preview-pheno-se": "#finput-phenotype-se",
+      "#tbl-preview-pheno-n": "#finput-phenotype-n"
+  };
+
+  var filesMetadata = () => {
+      return {
+          "separator": $("#txt-file-separator").val(),
+          "comment_char": $(
+              "#txt-file-comment-character").val(),
+          "na_strings": $("#txt-file-na").val()
+      }
+  };
+
+  var PREVIEW_ROWS = 5;
+
+  var handler_update_previews = (event) => {
+      Object.entries(preview_tables_to_elements_map).forEach((mapentry) => {
+          var preview_table = $(mapentry[0]);
+          var file_input = $(mapentry[1]);
+          if(file_input[0].files.length > 0) {
+              readFirstNLines(
+                  file_input[0].files[0],
+                  10,
+                  [makePreviewUpdater(preview_table)]);
+          }
+      });
+
+      if(typeof(resumables) !== "undefined") {
+          resumables.forEach((resumable) => {
+              if(resumable.files.length > 0) {
+                  readFirstNLines(
+                      resumable.files[0].file,
+                      10,
+                      [makePreviewUpdater(resumable.preview_table)]);
+              }
+          });
+      }
+  };
+
+  [
+      "#txt-file-separator",
+      "#txt-file-comment-character",
+      "#txt-file-na"
+  ].forEach((elementid) => {
+      $(elementid).on("change", handler_update_previews);
+  });
+
+  [
+      "#finput-phenotype-descriptions",
+      "#finput-phenotype-data",
+      "#finput-phenotype-se",
+      "#finput-phenotype-n"
+  ].forEach((elementid) => {
+      $(elementid).on("change", (event) => {
+          readFirstNLines(
+              event.target.files[0],
+              10,
+              [makePreviewUpdater(
+                  $("#" + event.target.getAttribute("data-preview-table")))]);
+      });
+  });
+
+
+  var resumableDisplayFiles = (display_area, files) => {
+      files.forEach((file) => {
+          display_area.find(".file-display").remove();
+          var display_element = display_area
+              .find(".file-display-template")
+              .clone();
+          remove_class(display_element, "visually-hidden");
+          remove_class(display_element, "file-display-template");
+          add_class(display_element, "file-display");
+          display_element.find(".filename").text(file.name
+                                                || file.fileName
+                                                || file.relativePath
+                                                || file.webkitRelativePath);
+          display_element.find(".filesize").text(
+              (file.size / (1024*1024)).toFixed(2) + "MB");
+          display_element.find(".fileuniqueid").text(file.uniqueIdentifier);
+          display_element.find(".filemimetype").text(file.file.type);
+          display_area.append(display_element);
+      });
+  };
+
+
+  var indicateProgress = (resumable, progress_bar) => {
+      return () => {/*Has no event!*/
+          var progress = (resumable.progress() * 100).toFixed(2);
+          var pbar = progress_bar.find(".progress-bar");
+          remove_class(progress_bar, "visually-hidden");
+          pbar.css("width", progress+"%");
+          pbar.attr("aria-valuenow", progress);
+          pbar.text("Uploading: " + progress + "%");
+      };
+  };
+
+  var retryUpload = (retry_button, cancel_button) => {
+      retry_button.on("click", (event) => {
+          resumable.files.forEach((file) => {file.retry();});
+          add_class(retry_button, "visually-hidden");
+          remove_class(cancel_button, "visually-hidden");
+          add_class(browse_button, "visually-hidden");
+      });
+  };
+
+  var cancelUpload = (cancel_button, retry_button) => {
+      cancel_button.on("click", (event) => {
+          resumable.files.forEach((file) => {
+              if(file.isUploading()) {
+                  file.abort();
+              }
+          });
+          add_class(cancel_button, "visually-hidden");
+          remove_class(retry_button, "visually-hidden");
+          remove_class(browse_button, "visually-hidden");
+      });
+  };
+
+
+  var startUpload = (browse_button, retry_button, cancel_button) => {
+      return (event) => {
+          remove_class(cancel_button, "visually-hidden");
+          add_class(retry_button, "visually-hidden");
+          add_class(browse_button, "visually-hidden");
+      };
+  };
+
+  var processForm = (form) => {
+      var formdata = new FormData(form);
+      uploaded_files.forEach((msg) => {
+          formdata.delete(msg["file-input-name"]);
+          formdata.append(msg["file-input-name"], JSON.stringify({
+              "uploaded-file": msg["uploaded-file"],
+              "original-name": msg["original-name"]
+          }));
+      });
+      formdata.append("resumable-upload", "true");
+      formdata.append("publication-id", $("#txt-publication-id").val());
+      return formdata;
+  }
+
+  var uploaded_files = new Set();
+  var submitForm = (new_file) => {
+      uploaded_files.add(new_file);
+      if(uploaded_files.size === resumables.length) {
+          var form = $("#frm-add-phenotypes");
+          if(form.length !== 1) {
+              // TODO: Handle error somehow?
+              alert("Could not find form!!!");
+              return false;
+          }
+
+          $.ajax({
+              "url": form.attr("action"),
+              "type": "POST",
+              "data": processForm(form[0]),
+              "processData": false,
+              "contentType": false,
+              "success": (data, textstatus, jqxhr) => {
+                  // TODO: Redirect to endpoint that should come as part of the
+                  //       success/error message.
+                  console.log("SUCCESS DATA: ", data);
+                  console.log("SUCCESS STATUS: ", textstatus);
+                  console.log("SUCCESS jqXHR: ", jqxhr);
+                  window.location.assign(window.location.origin + data["redirect-to"]);
+              },
+          });
+          return false;
+      }
+      return false;
+  };
+
+  var uploadSuccess = (file_input_name) => {
+      return (file, message) => {
+          submitForm({...JSON.parse(message), "file-input-name": file_input_name});
+      };
+  };
+
+
+  var uploadError = () => {
+      return (message, file) => {
+          $("#frm-add-phenotypes input[type=submit]").removeAttr("disabled");
+          console.log("THE FILE:", file);
+          console.log("THE ERROR MESSAGE:", message);
+      };
+  };
+
+
+
+  var makeResumableObject = (form_id, file_input_id, resumable_element_id, preview_table_id) => {
+      var the_form = $("#" + form_id);
+      var file_input = $("#" + file_input_id);
+      var submit_button = the_form.find("input[type=submit]");
+      if(file_input.length != 1) {
+          return false;
+      }
+      var r = errorHandler(
+          fileSuccessHandler(
+              uploadStartHandler(
+                  filesAddedHandler(
+                      markResumableDragAndDropElement(
+                          makeResumableElement(
+                              the_form.attr("data-resumable-target"),
+                              file_input.parent(),
+                              $("#" + resumable_element_id),
+                              submit_button,
+                              ["csv", "tsv", "txt"]),
+                          file_input.parent(),
+                          $("#" + resumable_element_id),
+                          $("#" + resumable_element_id + "-browse-button")),
+                      (files) => {
+                          // TODO: Also trigger preview!
+                          resumableDisplayFiles(
+                              $("#" + resumable_element_id + "-selected-files"), files);
+                          files.forEach((file) => {
+                              readFirstNLines(
+                                  file.file,
+                                  10,
+                                  [makePreviewUpdater(
+                                      $("#" + preview_table_id))])
+                          });
+                      }),
+                  startUpload($("#" + resumable_element_id + "-browse-button"),
+                              $("#" + resumable_element_id + "-retry-button"),
+                              $("#" + resumable_element_id + "-cancel-button"))),
+              uploadSuccess(file_input.attr("name"))),
+          uploadError());
+
+      /** Setup progress indicator **/
+      progressHandler(
+          r,
+          indicateProgress(r, $("#" + resumable_element_id + "-progress-bar")));
+
+      return r;
+  };
+
+  var resumables = [
+      ["frm-add-phenotypes", "finput-phenotype-descriptions", "resumable-phenotype-descriptions", "tbl-preview-pheno-desc"],
+      ["frm-add-phenotypes", "finput-phenotype-data", "resumable-phenotype-data", "tbl-preview-pheno-data"],
+      ["frm-add-phenotypes", "finput-phenotype-se", "resumable-phenotype-se", "tbl-preview-pheno-se"],
+      ["frm-add-phenotypes", "finput-phenotype-n", "resumable-phenotype-n", "tbl-preview-pheno-n"],
+  ].map((row) => {
+      r = makeResumableObject(row[0], row[1], row[2], row[3]);
+      r.preview_table = $("#" + row[3]);
+      return r;
+  }).filter((val) => {
+      return Boolean(val);
+  });
+
+  $("#frm-add-phenotypes input[type=submit]").on("click", (event) => {
+      event.preventDefault();
+      console.debug();
+      if ($("#txt-publication-id").val() == "") {
+          alert("You MUST provide a publication for the phenotypes.");
+          return false;
+      }
+      // TODO: Check all the relevant files exist
+      // TODO: Verify that files are not duplicated
+      var filenames = [];
+      var nondupfiles = [];
+      resumables.forEach((r) => {
+          var fname = r.files[0].file.name;
+          filenames.push(fname);
+          if(!nondupfiles.includes(fname)) {
+              nondupfiles.push(fname);
+          }
+      });
+
+      // Check that all files were provided
+      if(resumables.length !== filenames.length) {
+          window.alert("You MUST provide all the files requested.");
+          event.target.removeAttribute("disabled");
+          return false;
+      }
+
+      // Check that there are no duplicate files
+      var duplicates = Object.entries(filenames.reduce(
+          (acc, curr, idx, arr) => {
+              acc[curr] = (acc[curr] || 0) + 1;
+              return acc;
+          },
+          {})).filter((entry) => {return entry[1] !== 1;});
+      if(duplicates.length > 0) {
+          var msg = "The file(s):\r\n";
+          msg = msg + duplicates.reduce(
+              (msgstr, afile) => {
+                  return msgstr + "  • " + afile[0] + "\r\n";
+              },
+              "");
+          msg = msg + "is(are) duplicated. Please fix and try again.";
+          window.alert(msg);
+          event.target.removeAttribute("disabled");
+          return false;
+      }
+      // TODO: Check all fields
+      // Start the uploads.
+      event.target.setAttribute("disabled", "disabled");
+      resumables.forEach((r) => {r.upload();});
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html b/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html
new file mode 100644
index 0000000..29a8dea
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html
@@ -0,0 +1,189 @@
+{%extends "phenotypes/sui-add-phenotypes-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block frm_add_phenotypes_documentation%}
+<p>Select the zip file bundle containing information on the phenotypes you
+  wish to upload, then click the "Upload Phenotypes" button below to
+  upload the data.</p>
+<p>If you wish to upload the files individually instead,
+  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id)}}"
+     title="">click here</a>.</p>
+<p>See the <a href="#section-file-formats">File Formats</a> section below
+  to get an understanding of what is expected of the bundle files you
+  upload.</p>
+{%endblock%}
+
+{%block frm_add_phenotypes_elements%}
+<div class="form-group">
+  <label for="finput-phenotypes-bundle" class="form-label">
+    Phenotypes Bundle</label>
+  <input type="file"
+         id="finput-phenotypes-bundle"
+         name="phenotypes-bundle"
+         accept="application/zip, .zip"
+	 required="required"
+         class="form-control" />
+</div>
+{%endblock%}
+
+{%block page_documentation%}
+<div class="row">
+  <h2 class="heading" id="section-file-formats">File Formats</h2>
+  <p>We accept an extended form of the
+    <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html#format-of-the-data-files"
+       title="R/qtl2 software input file format documentation">
+      input files' format used with the R/qtl2 software</a> as a single ZIP
+    file</p>
+  <p>The files that are used for this feature are:
+    <ul>
+      <li>the <em>control</em> file</li>
+      <li><em>pheno</em> file(s)</li>
+      <li><em>phenocovar</em> file(s)</li>
+      <li><em>phenose</em> files(s)</li>
+    </ul>
+  </p>
+  <p>Other files within the bundle will be ignored, for this feature.</p>
+  <p>The following section will detail the expectations for each of the
+    different file types within the uploaded ZIP file bundle for phenotypes:</p>
+
+  <h3 class="subheading">Control File</h3>
+  <p>There <strong>MUST be <em>one, and only one</em></strong> file that acts
+    as the control file. This file can be:
+    <ul>
+      <li>a <em>JSON</em> file, or</li>
+      <li>a <em>YAML</em> file.</li>
+    </ul>
+  </p>
+
+  <p>The control file is useful for defining things about the bundle such as:</p>
+  <ul>
+    <li>The field separator value (default: <code>sep: ','</code>). There can
+      only ever be one field separator and it <strong>MUST</strong> be the same
+      one for <strong>ALL</strong> files in the bundle.</li>
+    <li>The comment character (default: <code>comment.char: '#'</code>). Any
+      line that starts with this character will be considered a comment line and
+      be ignored in its entirety.</li>
+    <li>Code for missing values (default: <code>na.strings: 'NA'</code>). You
+      can specify more than one code to indicate missing values, e.g.
+      <code>{…, "na.strings": ["NA", "N/A", "-"], …}</code></li>
+  </ul>
+
+  <h3 class="subheading"><em>pheno</em> File(s)</h3>
+  <p>These files are the main data files. You must have at least one of these
+    files in your bundle for it to be valid for this step.</p>
+  <p>The data is a matrix of <em>individuals × phenotypes</em> by default, as
+    below:<br />
+    <code>
+      id,10001,10002,10003,10004,…<br />
+      BXD1,61.400002,54.099998,483,49.799999,…<br />
+      BXD2,49,50.099998,403,45.5,…<br />
+      BXD5,62.5,53.299999,501,62.900002,…<br />
+      BXD6,53.099998,55.099998,403,NA,…<br />
+      ⋮<br /></code>
+  </p>
+  <p>If the <code>pheno_transposed</code> value is set to <code>True</code>,
+    then the data will be a <em>phenotypes × individuals</em> matrix as in the
+    example below:<br />
+    <code>
+      id,BXD1,BXD2,BXD5,BXD6,…<br />
+      10001,61.400002,49,62.5,53.099998,…<br />
+      10002,54.099998,50.099998,53.299999,55.099998,…<br />
+      10003,483,403,501,403,…<br />
+      10004,49.799999,45.5,62.900002,NA,…<br />
+      ⋮
+    </code>
+  </p>
+
+
+  <h3 class="subheading"><em>phenocovar</em> File(s)</h3>
+  <p>At least one phenotypes metadata file with the metadata values such as
+    descriptions, PubMed Identifier, publication titles (if present), etc.</p>
+  <p>The data in this/these file(s) is a matrix of
+    <em>phenotypes × phenotypes-covariates</em>. The first column is always the
+    phenotype names/identifiers — same as in the R/qtl2 format.</p>
+  <p><em>phenocovar</em> files <strong>should never be transposed</strong>!</p>
+  <p>This file <strong>MUST</strong> be present in the bundle, and have data for
+    the bundle to be considered valid by our system for this step.<br />
+    In addition to that, the following are the fields that <strong>must be
+      present</strong>, and
+    have values, in the file before the file is considered valid:
+    <ul>
+      <li><em>description</em>: A description for each phenotype. Useful
+        for users to know what the phenotype is about.</li>
+      <li><em>units</em>: The units of measurement for the phenotype,
+        e.g. milligrams for brain weight, centimetres/millimetres for
+        tail-length, etc.</li>
+  </ul></p>
+
+  <p>The following <em>optional</em> fields can also be provided:
+    <ul>
+      <li><em>pubmedid</em>: A PubMed Identifier for the publication where
+        the phenotype is published. If this field is not provided, the system will
+        assume your phenotype is not published.</li>
+    </ul>
+  </p>
+  <p>These files will be marked up in the control file with the
+    <code>phenocovar</code> key, as in the examples below:
+    <ol>
+      <li>JSON: single file<br />
+        <code>{<br />
+          &nbsp;&nbsp;⋮,<br />
+          &nbsp;&nbsp;"phenocovar": "your_covariates_file.csv",<br />
+          &nbsp;&nbsp;⋮<br />
+          }
+        </code>
+      </li>
+      <li>JSON: multiple files<br />
+        <code>{<br />
+          &nbsp;&nbsp;⋮,<br />
+          &nbsp;&nbsp;"phenocovar": [<br />
+          &nbsp;&nbsp;&nbsp;&nbsp;"covariates_file_01.csv",<br />
+          &nbsp;&nbsp;&nbsp;&nbsp;"covariates_file_01.csv",<br />
+          &nbsp;&nbsp;&nbsp;&nbsp;⋮<br />
+          &nbsp;&nbsp;],<br />
+          &nbsp;&nbsp;⋮<br />
+          }
+        </code>
+      </li>
+      <li>YAML: single file or<br />
+        <code>
+          ⋮<br />
+          phenocovar: your_covariates_file.csv<br />
+          ⋮
+        </code>
+      </li>
+      <li>YAML: multiple files<br />
+        <code>
+          ⋮<br />
+          phenocovar:<br />
+          - covariates_file_01.csv<br />
+          - covariates_file_02.csv<br />
+          - covariates_file_03.csv<br />
+          …<br />
+          ⋮
+        </code>
+      </li>
+    </ol>
+  </p>
+
+  <h3 class="subheading"><em>phenose</em> and <em>phenonum</em> File(s)</h3>
+  <p>These are extensions to the R/qtl2 standard, i.e. these types ofs file are
+    not supported by the original R/qtl2 file format</p>
+  <p>We use these files to upload the standard errors (<em>phenose</em>) when
+    the data file (<em>pheno</em>) is average data. In that case, the
+    <em>phenonum</em> file(s) contains the number of individuals that were
+    involved when computing the averages.</p>
+  <p>Both types of files are matrices of <em>individuals × phenotypes</em> by
+    default. Like the related <em>pheno</em> files, if
+    <code>pheno_transposed: True</code>, then the file will be a matrix of
+    <em>phenotypes × individuals</em>.</p>
+</div>
+{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-base.html b/uploader/templates/phenotypes/sui-base.html
new file mode 100644
index 0000000..d7d980f
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-base.html
@@ -0,0 +1,25 @@
+{%extends "populations/sui-base.html"%}
+{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_sui_pheno_dataset_card%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.populations.phenotypes.view_dataset',
+           species_id=species['SpeciesId'],
+           population_id=population['Id'],
+           dataset_id=dataset['Id'])}}">
+    {{dataset["Name"]}}
+  </a>
+</li>
+{%endblock%}
+
+{%block contents%}
+<div class="row">
+  <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2>
+</div>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+{{display_sui_pheno_dataset_card(species, population, dataset)}}
+{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-job-status.html b/uploader/templates/phenotypes/sui-job-status.html
new file mode 100644
index 0000000..bca87d5
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-job-status.html
@@ -0,0 +1,140 @@
+{%extends "phenotypes/sui-base.html"%}
+{%from "cli-output.html" import cli_output%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+
+{%block extrameta%}
+{%if job and job.status not in ("success", "completed:success", "error", "completed:error")%}
+<meta http-equiv="refresh" content="5" />
+{%endif%}
+{%endblock%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block contents%}
+
+{%if job%}
+<div class="row">
+  <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2>
+  <h3 class="subheading">upload progress</h3>
+</div>
+<div class="row" style="overflow:scroll;">
+  <p><strong>Process Status:</strong> {{job.status}}</p>
+  {%if metadata%}
+  <table class="table table-responsive">
+    <thead>
+      <tr>
+        <th>File</th>
+        <th>Status</th>
+        <th>Lines Processed</th>
+        <th>Total Errors</th>
+      </tr>
+    </thead>
+
+    <tbody>
+      {%for file,meta in metadata.items()%}
+      <tr>
+        <td>{{file}}</td>
+        <td>{{meta.status}}</td>
+        <td>{{meta.linecount}}</td>
+        <td>{{meta["total-errors"]}}</td>
+      </tr>
+      {%endfor%}
+    </tbody>
+  </table>
+  {%endif%}
+</div>
+
+<div class="row">
+  {%if  job.status in ("completed:success", "success")%}
+  <p>
+    {%if errors | length == 0%}
+    <a href="{{url_for('species.populations.phenotypes.review_job_data',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id,
+           job_id=job_id)}}"
+       class="btn btn-primary"
+       title="Continue to process data">Continue</a>
+    {%else%}
+    <span class="text-muted"
+          disabled="disabled"
+          style="border: solid 2px;border-radius: 5px;padding: 0.3em;">
+      Cannot continue due to errors. Please fix the errors first.
+    </span>
+    {%endif%}
+  </p>
+  {%endif%}
+</div>
+
+<h3 class="subheading">upload errors</h3>
+<div class="row" style="max-height: 20em; overflow: scroll;">
+  {%if errors | length == 0 %}
+  <p class="text-info">
+    <span class="glyphicon glyphicon-info-sign"></span>
+    No errors found so far
+  </p>
+  {%else%}
+  <table class="table table-responsive">
+    <thead style="position: sticky; top: 0; background: white;">
+      <tr>
+        <th>File</th>
+        <th>Row</th>
+        <th>Column</th>
+        <th>Value</th>
+        <th>Message</th>
+      </tr>
+    </thead>
+
+    <tbody style="font-size: 0.9em;">
+      {%for error in errors%}
+      <tr>
+        <td>{{error.filename}}</td>
+        <td>{{error.rowtitle}}</td>
+        <td>{{error.coltitle}}</td>
+        <td>{%if error.cellvalue is not none and error.cellvalue | length > 25%}
+          {{error.cellvalue[0:24]}}&hellip;
+          {%else%}
+          {{error.cellvalue}}
+          {%endif%}
+        </td>
+        <td>
+          {%if error.message | length > 250 %}
+          {{error.message[0:249]}}&hellip;
+          {%else%}
+          {{error.message}}
+          {%endif%}
+        </td>
+      </tr>
+      {%endfor%}
+    </tbody>
+  </table>
+  {%endif%}
+</div>
+
+<div class="row">
+  {{cli_output(job, "stdout")}}
+</div>
+
+<div class="row">
+  {{cli_output(job, "stderr")}}
+</div>
+
+{%else%}
+<div class="row">
+  <h3 class="text-danger">No Such Job</h3>
+  <p>Could not find a job with the ID: {{job_id}}</p>
+  <p>
+    Please go back to
+    <a href="{{url_for('species.populations.phenotypes.view_dataset',
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             dataset_id=dataset.Id)}}"
+       title="'{{dataset.Name}}' dataset page">
+      the '{{dataset.Name}}' dataset page</a>
+    to upload new phenotypes or edit existing ones.</p>
+</div>
+{%endif%}
+{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-load-phenotypes-success.html b/uploader/templates/phenotypes/sui-load-phenotypes-success.html
new file mode 100644
index 0000000..dff0682
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-load-phenotypes-success.html
@@ -0,0 +1,26 @@
+{%extends "phenotypes/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block contents%}
+{{super()}}
+
+<div class="row">
+  <p>You have successfully loaded
+    <!-- maybe indicate the number of phenotypes here? -->your
+    new phenotypes into the database.</p>
+  <!-- TODO: Maybe notify user that they have sole access. -->
+  <!-- TODO: Maybe provide a link to go to GeneNetwork to view the data. -->
+  <p>View your data
+    <a href="{{search_page_uri}}"
+       target="_blank">on GeneNetwork2</a>.
+    You might need to login to GeneNetwork2 to view specific traits.</p>
+</div>
+{%endblock%}
+
+
+{%block more_javascript%}{%endblock%}
diff --git a/uploader/templates/phenotypes/sui-review-job-data.html b/uploader/templates/phenotypes/sui-review-job-data.html
new file mode 100644
index 0000000..ea4183d
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-review-job-data.html
@@ -0,0 +1,121 @@
+{%extends "phenotypes/sui-base.html"%}
+{%from "cli-output.html" import cli_output%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
+
+{%block extrameta%}
+{%if not job%}
+<meta http-equiv="refresh"
+      content="20; url={{url_for('species.populations.phenotypes.view_dataset', species_id=species.SpeciesId,
+               population_id=population.Id,
+               dataset_id=dataset.Id)}}" />
+{%endif%}
+{%endblock%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block lvl4_breadcrumbs%}
+<li {%if activelink=="add-phenotypes"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id)}}">View Datasets</a>
+</li>
+{%endblock%}
+
+{%block contents%}
+
+{%if job%}
+<div class="row">
+  <h3 class="heading">Data Review</h3>
+  <p class="text-info"><strong>
+      The data has <em>NOT</em> been added/saved yet. Review the details below
+      and click "Continue" to save the data.</strong></p>
+  <p>The &#x201C;<strong>{{dataset.FullName}}</strong>&#x201D; dataset from the
+    &#x201C;<strong>{{population.FullName}}</strong>&#x201D; population of the
+    species &#x201C;<strong>{{species.SpeciesName}} ({{species.FullName}})</strong>&#x201D;
+    will be updated as follows:</p>
+
+  <ul>
+    {%if publication%}
+    <li>All {{summary.get("pheno", {}).get("total-data-rows", "0")}} phenotypes
+      are linked to the following publication:
+      <ul>
+        <li><strong>Publication Title:</strong>
+          {{publication.Title or "—"}}</li>
+        <li><strong>Author(s):</strong>
+          {{publication.Authors or "—"}}</li>
+      </ul>
+    </li>
+    {%endif%}
+  {%for ftype in ("phenocovar", "pheno", "phenose", "phenonum")%}
+  {%if summary.get(ftype, False)%}
+    <li>A total of {{summary[ftype]["number-of-files"]}} files will be processed
+      adding {%if ftype == "phenocovar"%}(possibly){%endif%}
+      {{summary[ftype]["total-data-rows"]}} new
+      {%if ftype == "phenocovar"%}
+      phenotypes
+      {%else%}
+      {{summary[ftype]["description"]}} rows
+      {%endif%}
+      to the database.
+    </li>
+  {%endif%}
+  {%endfor%}
+  </ul>
+
+  <form id="frm-review-phenotype-data"
+        method="POST"
+        action="{{url_for('species.populations.phenotypes.load_data_to_database',
+                species_id=species.SpeciesId,
+                population_id=population.Id,
+                dataset_id=dataset.Id)}}">
+    <input type="hidden" name="data-qc-job-id" value="{{job.jobid}}" />
+    <input type="submit"
+           value="continue"
+           class="btn btn-primary" />
+  </form>
+</div>
+{%else%}
+<div class="row">
+  <h4 class="subheading">Invalid Job</h3>
+  <p class="text-danger">
+    Could not find a job with the ID: <strong>{{job_id}}.</p>
+  <p>You will be redirected in
+    <span id="countdown-element" class="text-info">20</span> second(s)</p>
+  <p class="text-muted">
+    <small>
+      If you are not redirected, please
+      <a href="{{url_for(
+               'species.populations.phenotypes.view_dataset',
+               species_id=species.SpeciesId,
+               population_id=population.Id,
+               dataset_id=dataset.Id)}}">click here</a> to continue
+    </small>
+  </p>
+</div>
+{%endif%}
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript">
+  $(document).ready(function() {
+      var countdown = 20;
+      var countdown_element = $("#countdown-element");
+      if(countdown_element.length === 1) {
+          intv = window.setInterval(function() {
+              countdown = countdown - 1;
+              countdown_element.html(countdown);
+          }, 1000);
+      }
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/populations/sui-base.html b/uploader/templates/populations/sui-base.html
index cc01c9e..0ca5c59 100644
--- a/uploader/templates/populations/sui-base.html
+++ b/uploader/templates/populations/sui-base.html
@@ -6,7 +6,7 @@
   <a href="{{url_for('species.populations.view_population',
            species_id=species['SpeciesId'],
            population_id=population['Id'])}}">
-    {{population["FullName"]}}
+    {{population["Name"]}}
   </a>
 </li>
 {%endblock%}
diff --git a/uploader/templates/populations/sui-view-population.html b/uploader/templates/populations/sui-view-population.html
index 3bf8d0d..6244f4d 100644
--- a/uploader/templates/populations/sui-view-population.html
+++ b/uploader/templates/populations/sui-view-population.html
@@ -57,55 +57,95 @@
          role="tabpanel"
          aria-labelledby="samples-content-tab">
       <p>Think of a <strong>"sample"</strong> as say a single case or individual
-        in the experiment. It could even be a single strain (where applicable)
-        under consideration.</p>
+        in the experiment. It could even be a single strain (where applicable).
+      </p>
       <p>This is a convenience feature for when you want to upload phenotypes to
         the system, but do not have the genotypes data ready yet.</p>
-      <p>Please click the button below to provide the samples that will be used
-        in your data.</p>
       <a href="{{url_for('species.populations.samples.list_samples',
                species_id=species.SpeciesId,
                population_id=population.Id)}}"
-         title="Upload samples for population '{{population['Name']}}'"
-         class="btn btn-primary">Upload Samples</a>
+         title="View and upload samples for population '{{population['Name']}}'"
+         class="btn btn-primary">Manage Samples</a>
     </div>
 
     <div class="tab-pane fade show active"
          id="phenotypes-content"
          role="tabpanel"
          aria-labelledby="phenotypes-content-tab">
-      <p>Upload and manage phenotypes and publications for population
-        "<em>{{population.FullName}} ({{population.Name}})</em>" of species
-        "<em>{{species.FullName}} ({{species.Name}})</em>".</p>
 
-      <p class="text-danger">Tabs will not work nicely here. Maybe present
-        options e.g.:
-      </p>
-      <div class="row">
+      <div class="row" style="margin-top: 0.3em;">
         <div class="col">
-          <a href="{{url_for('species.populations.phenotypes.view_dataset',
+          <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
                    species_id=species.SpeciesId,
                    population_id=population.Id,
                    dataset_id=dataset.Id)}}"
              title="Upload phenotype data for population '{{population['Name']}}'"
              class="btn btn-primary">Upload new Phenotypes</a>
-          <!-- Go straight to upload form(s). -->
-        </div>
-        <div class="col">
-          <a href="#"
-             title="List all existing phenotypes for this population."
-             class="btn btn-info not-implemented">list existing phenotypes</a>
-          <!-- Means and QTLReaper will be computed in this page. -->
         </div>
         <div class="col">
           <a href="#"
              title="List all existing publications for this population."
-             class="btn btn-info not-implemented">list existing publications</a>
+             class="btn btn-primary not-implemented">view publications</a>
           <!-- Maybe, actually filter publications by population? -->
           <!-- Provide other features for publications on loaded page. -->
         </div>
       </div>
+
+      <div class="row" style="margin-top: 1em;">
+        <h3> Phenotypes in  Population "{{population.FullName}} ({{population.Name}})"</h3>
+
+        <p>The table below lists the phenotypes that already exist for
+          population "<em>{{population.FullName}} ({{population.Name}})</em>" of
+          species "<em>{{species.FullName}} ({{species.Name}})</em>".</p>
+
+        <div class="row phenotypes-list-actions">
+          <div class="col">
+            <form id="frm-recompute-phenotype-means"
+                  method="POST"
+                  action="{{url_for(
+                          'species.populations.phenotypes.recompute_means',
+                          species_id=species['SpeciesId'],
+                          population_id=population['Id'],
+                          dataset_id=dataset['Id'])}}">
+              <input id="submit-frm-recompute-phenotype-means"
+                     class="btn btn-info"
+                     type="submit"
+                     title="Compute/Recompute the means for selected phenotypes (or all phenotypes if none selected)."
+                     value="(Rec/C)ompute means" />
+            </form>
+          </div>
+          <div class="col">
+            <form id="frm-rerun-qtlreaper"
+                  method="POST"
+                  action="{{url_for(
+                          'species.populations.phenotypes.rerun_qtlreaper',
+                          species_id=species['SpeciesId'],
+                          population_id=population['Id'],
+                          dataset_id=dataset['Id'])}}">
+              <input id="submit-frm-rerun-qtlreaper"
+                     class="btn btn-info"
+                     type="submit"
+                     title="Run/Rerun QTLReaper for selected phenotypes (or all phenotypes if none selected)."
+                     value="(rer/r)un QTLReaper" />
+            </form>
+          </div>
+        </div>
+
+        <table id="tbl-phenotypes-list" class="table compact stripe cell-border">
+          <thead>
+            <tr>
+              <th></th>
+              <th>Index</th>
+              <th>Record</th>
+              <th>Description</th>
+            </tr>
+          </thead>
+
+          <tbody></tbody>
+        </table>
+      </div>
     </div>
+
     <div class="tab-pane fade"
          id="genotypes-content"
          role="tabpanel"
@@ -136,3 +176,92 @@
 {%endblock%}
 
 
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/urls.js"></script>
+
+<script type="text/javascript">
+  $(function() {
+      /** JS to build list of phenotypes table. **/
+      var species_id = {{species.SpeciesId}};
+      var population_id = {{population.Id}};
+      var dataset_id = {{dataset.Id}};
+      var dataset_name = "{{dataset.Name}}";
+      var data = {{phenotypes | tojson}};
+
+      var dtPhenotypesList = buildDataTable(
+          "#tbl-phenotypes-list",
+          data,
+          [
+              {
+                  data: function(pheno) {
+                      return `<input type="checkbox" name="selected-phenotypes" `
+                          + `id="chk-selected-phenotypes-${pheno.InbredSetCode}_${pheno.xref_id}" `
+                          + `value="${pheno.InbredSetCode}_${pheno.xref_id}" `
+                          + `class="chk-row-select" />`
+                  }
+              },
+              {data: "sequence_number"},
+              {
+                  data: function(pheno, type, set, meta) {
+                      var spcs_id = {{species.SpeciesId}};
+                      var pop_id = {{population.Id}};
+                      var dtst_id = {{dataset.Id}};
+                      var url = buildURLFromCurrentURL(
+                          (`/species/${spcs_id}` +
+                          `/populations/${pop_id}` +
+                          `/phenotypes/datasets/${dtst_id}` +
+                           `/phenotype/${pheno.xref_id}`));
+                      return `<a href="${url.toString()}" target="_blank">` +
+                          `${pheno.InbredSetCode}_${pheno.xref_id}` +
+                          `</a>`;
+                  }
+              },
+              {
+                  data: function(pheno) {
+                      return (pheno.Post_publication_description ||
+                              pheno.Original_description ||
+                              pheno.Pre_publication_description);
+                  }
+              }
+          ],
+          {
+              select: "multi+shift",
+              layout: {
+                  top1Start: {
+                      pageLength: {
+                          text: "Show _MENU_ of _TOTAL_"
+                      }
+                  },
+                  topStart: "info",
+                  top1End: null
+              },
+              rowId: function(pheno) {
+                  return `${pheno.InbredSetCode}_${pheno.xref_id}`;
+              }
+          });
+
+
+      $("#submit-frm-rerun-qtlreaper").on(
+          "click",
+          function(event) {
+              // (Re)run the QTLReaper script for selected phenotypes.
+              event.preventDefault();
+              var form = $("#frm-rerun-qtlreaper");
+              form.find(".dynamically-added-element").remove();
+              dtPhenotypesList.rows({selected: true}).nodes().each((node, index) => {
+                  _cloned = $(node).find(".chk-row-select").clone();
+                  _cloned.removeAttr("id");
+                  _cloned.removeAttr("class");
+                  _cloned.attr("style", "display: none;");
+                  _cloned.attr("data-type", "dynamically-added-element");
+                  _cloned.attr("class", "dynamically-added-element checkbox");
+                  _cloned.prop("checked", true);
+                  form.append(_cloned);
+              });
+              form.submit();
+          });
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/edit-publication.html b/uploader/templates/publications/edit-publication.html
index 540ecf1..97fa134 100644
--- a/uploader/templates/publications/edit-publication.html
+++ b/uploader/templates/publications/edit-publication.html
@@ -12,7 +12,9 @@
 <div class="row">
   <form id="frm-create-publication"
         method="POST"
-        action="{{url_for('publications.edit_publication', publication_id=publication_id, **request.args)}}"
+        action="{{url_for('publications.edit_publication',
+                publication_id=publication_id,
+                next=request.args.get('next', ''))}}"
         class="form-horizontal">
 
     <div class="row mb-3">
diff --git a/uploader/templates/samples/sui-base.html b/uploader/templates/samples/sui-base.html
new file mode 100644
index 0000000..ee08e2e
--- /dev/null
+++ b/uploader/templates/samples/sui-base.html
@@ -0,0 +1,19 @@
+{%extends "populations/sui-base.html"%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">Manage Samples</li>
+{%endblock%}
+
+{%block contents%}
+<div class="row">
+  <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2>
+</div>
+{%endblock%}
+
+
+
+{%block sidebarcontents%}
+{{display_sui_population_card(species, population)}}
+{%endblock%}
diff --git a/uploader/templates/samples/sui-list-samples.html b/uploader/templates/samples/sui-list-samples.html
new file mode 100644
index 0000000..e9ed71a
--- /dev/null
+++ b/uploader/templates/samples/sui-list-samples.html
@@ -0,0 +1,98 @@
+{%extends "samples/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "populations/macro-select-population.html" import select_population_form%}
+
+{%block title%}Samples &mdash; List Samples{%endblock%}
+
+{%block contents%}
+{{super()}}
+
+<div class="row">
+  <h3 class="subheading">manage samples</h3>
+  {{flash_all_messages()}}
+</div>
+
+<div class="row">
+  <div class="col">
+    <a href="{{url_for('species.populations.samples.upload_samples',
+             species_id=species.SpeciesId,
+             population_id=population.Id)}}"
+       title="Add samples for population '{{population.FullName}}' from species
+              '{{species.FullName}}'."
+       class="btn btn-primary">add new samples</a>
+  </div>
+</div>
+
+{%if samples | length > 0%}
+<div class="row">
+  <p>
+    Population "{{population.FullName}} ({{population.Name}})" already has
+    <strong>{{total_samples}}</strong> samples/individuals entered. You can
+    explore the list of samples in the table below.
+  </p>
+</div>
+
+<div class="row">
+  <div class="col-md-2">
+    {%if offset > 0:%}
+    <a href="{{url_for('species.populations.samples.list_samples',
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             from=offset-count,
+             count=count)}}">
+      <span class="glyphicon glyphicon-backward"></span>
+      Previous
+    </a>
+    {%endif%}
+  </div>
+
+  <div class="col-md-8" style="text-align: center;">
+    Samples {{offset}} &mdash; {{offset+(count if offset + count < total_samples else total_samples - offset)}} / {{total_samples}}
+                                                                   </div>
+
+  <div class="col-md-2">
+    {%if offset + count < total_samples:%}
+    <a href="{{url_for('species.populations.samples.list_samples',
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             from=offset+count,
+             count=count)}}">
+      Next
+      <span class="glyphicon glyphicon-forward"></span>
+    </a>
+    {%endif%}
+  </div>
+</div>
+<div class="row">
+  <table class="table">
+    <thead>
+      <tr>
+        <th></th>
+        <th>Name</th>
+        <th>Auxilliary Name</th>
+        <th>Symbol</th>
+        <th>Alias</th>
+      </tr>
+    </thead>
+
+    <tbody>
+      {%for sample in samples%}
+      <tr>
+        <td>{{sample.sequence_number}}</td>
+        <td>{{sample.Name}}</td>
+        <td>{{sample.Name2}}</td>
+        <td>{{sample.Symbol or "-"}}</td>
+        <td>{{sample.Alias or "-"}}</td>
+      </tr>
+      {%endfor%}
+    </tbody>
+  </table>
+</div>
+{%else%}
+<div class="row">
+  <p>There are no samples entered for this population. Click the "Add Samples"
+    button above, to add some new samples.</p>
+</div>
+{%endif%}
+
+{%endblock%}