about summary refs log tree commit diff
path: root/uploader
diff options
context:
space:
mode:
Diffstat (limited to 'uploader')
-rw-r--r--uploader/__init__.py28
-rw-r--r--uploader/background_jobs.py20
-rw-r--r--uploader/base_routes.py34
-rw-r--r--uploader/datautils.py12
-rw-r--r--uploader/db/datasets.py4
-rw-r--r--uploader/default_settings.py7
-rw-r--r--uploader/expression_data/views.py2
-rw-r--r--uploader/flask_extensions.py33
-rw-r--r--uploader/genotypes/views.py2
-rw-r--r--uploader/oauth2/client.py3
-rw-r--r--uploader/oauth2/tokens.py47
-rw-r--r--uploader/oauth2/views.py56
-rw-r--r--uploader/phenotypes/models.py213
-rw-r--r--uploader/phenotypes/views.py458
-rw-r--r--uploader/platforms/views.py8
-rw-r--r--uploader/population/models.py22
-rw-r--r--uploader/population/rqtl2.py2
-rw-r--r--uploader/population/views.py49
-rw-r--r--uploader/publications/models.py27
-rw-r--r--uploader/publications/pubmed.py7
-rw-r--r--uploader/publications/views.py119
-rw-r--r--uploader/route_utils.py52
-rw-r--r--uploader/samples/models.py3
-rw-r--r--uploader/samples/views.py22
-rw-r--r--uploader/species/models.py2
-rw-r--r--uploader/species/views.py25
-rw-r--r--uploader/static/css/layout-common.css3
-rw-r--r--uploader/static/css/layout-large.css62
-rw-r--r--uploader/static/css/layout-medium.css64
-rw-r--r--uploader/static/css/layout-small.css60
-rw-r--r--uploader/static/css/theme.css90
-rw-r--r--uploader/static/js/populations.js17
-rw-r--r--uploader/static/js/pubmed.js2
-rw-r--r--uploader/static/js/species.js16
-rw-r--r--uploader/static/js/upload_samples.js24
-rw-r--r--uploader/static/js/urls.js26
-rw-r--r--uploader/sui.py8
-rw-r--r--uploader/templates/background-jobs/default-success-page.html17
-rw-r--r--uploader/templates/background-jobs/sui-default-success-page.html17
-rw-r--r--uploader/templates/base.html2
-rw-r--r--uploader/templates/cli-output.html2
-rw-r--r--uploader/templates/flash_messages.html12
-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/macro-forms.html9
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-base.html1
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-raw-files.html320
-rw-r--r--uploader/templates/phenotypes/edit-phenotype.html124
-rw-r--r--uploader/templates/phenotypes/job-status.html2
-rw-r--r--uploader/templates/phenotypes/load-phenotypes-success.html2
-rw-r--r--uploader/templates/phenotypes/macro-display-pheno-dataset-card.html28
-rw-r--r--uploader/templates/phenotypes/review-job-data.html3
-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/phenotypes/sui-view-dataset.html207
-rw-r--r--uploader/templates/phenotypes/view-dataset.html165
-rw-r--r--uploader/templates/phenotypes/view-phenotype.html54
-rw-r--r--uploader/templates/platforms/sui-base.html17
-rw-r--r--uploader/templates/platforms/sui-create-platform.html116
-rw-r--r--uploader/templates/platforms/sui-list-platforms.html88
-rw-r--r--uploader/templates/populations/create-population.html39
-rw-r--r--uploader/templates/populations/list-populations.html7
-rw-r--r--uploader/templates/populations/macro-display-population-card.html39
-rw-r--r--uploader/templates/populations/sui-base.html12
-rw-r--r--uploader/templates/populations/sui-view-population.html127
-rw-r--r--uploader/templates/populations/view-population.html6
-rw-r--r--uploader/templates/publications/delete-publication-success.html18
-rw-r--r--uploader/templates/publications/delete-publication.html88
-rw-r--r--uploader/templates/publications/edit-publication.html196
-rw-r--r--uploader/templates/publications/index.html12
-rw-r--r--uploader/templates/publications/sui-base.html9
-rw-r--r--uploader/templates/publications/sui-create-publication.html200
-rw-r--r--uploader/templates/publications/sui-delete-publication.html95
-rw-r--r--uploader/templates/publications/sui-edit-publication.html203
-rw-r--r--uploader/templates/publications/sui-index.html109
-rw-r--r--uploader/templates/publications/sui-view-publication.html80
-rw-r--r--uploader/templates/publications/view-publication.html22
-rw-r--r--uploader/templates/samples/list-samples.html36
-rw-r--r--uploader/templates/samples/sui-base.html25
-rw-r--r--uploader/templates/samples/sui-list-samples.html98
-rw-r--r--uploader/templates/samples/sui-upload-failure.html32
-rw-r--r--uploader/templates/samples/sui-upload-progress.html26
-rw-r--r--uploader/templates/samples/sui-upload-samples.html153
-rw-r--r--uploader/templates/samples/sui-upload-success.html31
-rw-r--r--uploader/templates/samples/upload-samples.html18
-rw-r--r--uploader/templates/species/macro-display-species-card.html29
-rw-r--r--uploader/templates/species/sui-base.html10
-rw-r--r--uploader/templates/species/sui-view-species.html127
-rw-r--r--uploader/templates/sui-base.html103
-rw-r--r--uploader/templates/sui-index.html161
l---------uploader/templates/sui-login.html1
97 files changed, 5648 insertions, 816 deletions
diff --git a/uploader/__init__.py b/uploader/__init__.py
index 8b49ad5..7425b38 100644
--- a/uploader/__init__.py
+++ b/uploader/__init__.py
@@ -22,6 +22,7 @@ from .files.views import files
 from .species import speciesbp
 from .publications import pubbp
 from .oauth2.views import oauth2
+from .flask_extensions import url_for
 from .expression_data import exprdatabp
 from .errors import register_error_handlers
 from .background_jobs import background_jobs_bp
@@ -64,17 +65,13 @@ def setup_logging(app: Flask) -> Flask:
         "SERVER_SOFTWARE", "").split('/')
     return __log_gunicorn__(app) if bool(software) else __log_dev__(app)
 
-def setup_modules_logging(app_logger):
+def setup_modules_logging(app_logger, modules):
     """Setup module-level loggers to the same log-level as the application."""
     loglevel = logging.getLevelName(app_logger.getEffectiveLevel())
-
-    def __setup__(logger_name):
-        _logger = logging.getLogger(logger_name)
+    for module in modules:
+        _logger = logging.getLogger(module)
         _logger.setLevel(loglevel)
 
-    __setup__("uploader.publications.models")
-    __setup__("uploader.publications.datatables")
-
 
 def create_app(config: Optional[dict] = None):
     """The application factory.
@@ -111,15 +108,24 @@ def create_app(config: Optional[dict] = None):
         default_timeout=int(app.config["SESSION_FILESYSTEM_CACHE_TIMEOUT"]))
 
     setup_logging(app)
-    setup_modules_logging(app.logger)
+    setup_modules_logging(app.logger, (
+        "uploader.base_routes",
+        "uploader.flask_extensions",
+        "uploader.publications.models",
+        "uploader.publications.datatables",
+        "uploader.phenotypes.models"))
 
     # setup jinja2 symbols
-    app.add_template_global(lambda : request.url, name="request_url")
+    app.add_template_global(user_logged_in)
+    app.add_template_global(url_for, name="url_for")
     app.add_template_global(authserver_authorise_uri)
+    app.add_template_global(lambda : request.url, name="request_url")
     app.add_template_global(lambda: app.config["GN2_SERVER_URL"],
                             name="gn2server_uri")
-    app.add_template_global(user_logged_in)
-    app.add_template_global(lambda : session.user_details()["email"], name="user_email")
+    app.add_template_global(lambda : session.user_details()["email"],
+                            name="user_email")
+    app.add_template_global(lambda: app.config["FEATURE_FLAGS_HTTP"],
+                            name="http_feature_flags")
 
     Session(app)
 
diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py
index dc9f837..4aded1d 100644
--- a/uploader/background_jobs.py
+++ b/uploader/background_jobs.py
@@ -5,7 +5,6 @@ from typing import Callable
 from functools import partial
 
 from flask import (
-    url_for,
     redirect,
     Response,
     Blueprint,
@@ -16,6 +15,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__)
@@ -56,7 +59,7 @@ def register_job_handlers(job: str):
         return getattr(module, _parts[-1])
 
     metadata = job["metadata"]
-    if metadata["success_handler"]:
+    if metadata.get("success_handler"):
         _success_handler = __load_handler__(metadata["success_handler"])
         try:
             _error_handler = __load_handler__(metadata["error_handler"])
@@ -76,8 +79,8 @@ def handler(job: dict, handler_type: str) -> HandlerType:
     ).get(handler_type)
     if bool(_handler):
         return _handler(job)
-    raise Exception(# pylint: disable=[broad-exception-raised]
-        f"No '{handler_type}' handler registered for job type: {_job_type}")
+    return render_template(sui_template("background-jobs/default-success-page.html"),
+                           job=job)
 
 
 error_handler = partial(handler, handler_type="error")
@@ -100,10 +103,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)
 
 
@@ -114,6 +117,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 74a3b90..e9f1165 100644
--- a/uploader/base_routes.py
+++ b/uploader/base_routes.py
@@ -1,15 +1,26 @@
 """Basic routes required for all pages"""
 import os
+import logging
 from urllib.parse import urljoin
 
-from flask import (Blueprint,
+from gn_libs.mysqldb import database_connection
+from flask import (flash,
+                   request,
+                   redirect,
+                   Blueprint,
                    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
+from uploader.species.models import all_species, species_by_id
 
 base = Blueprint("base", __name__)
+logger = logging.getLogger(__name__)
 render_template = make_template_renderer("home")
 
 
@@ -24,9 +35,24 @@ def favicon():
 @base.route("/", methods=["GET"])
 def index():
     """Load the landing page"""
-    return render_template("index.html" if user_logged_in() else "login.html",
-                           gn2server_intro=urljoin(app.config["GN2_SERVER_URL"],
-                                                   "/intro"))
+    streamlined_ui = request.args.get("streamlined_ui")
+    with database_connection(app.config["SQL_URI"]) as conn:
+        print("We found a species ID. Processing...")
+        if not bool(request.args.get("species_id")):
+            return render_template(
+                sui_template("index.html" if user_logged_in() else "login.html"),
+                gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], "/intro"),
+                species=all_species(conn),
+                streamlined_ui=streamlined_ui)
+
+        species = species_by_id(conn, request.args.get("species_id"))
+        if not bool(species):
+            flash("Selected species was not found!", "alert alert-danger")
+            return redirect(url_for("base.index", streamlined_ui=streamlined_ui))
+
+        return redirect(url_for("species.view_species",
+                                species_id=species["SpeciesId"]))
+
 
 def appenv():
     """Get app's guix environment path."""
diff --git a/uploader/datautils.py b/uploader/datautils.py
index 46a55c4..d132c42 100644
--- a/uploader/datautils.py
+++ b/uploader/datautils.py
@@ -1,5 +1,7 @@
 """Generic data utilities: Rename module."""
 import math
+import json
+import base64
 from functools import reduce
 from typing import Union, Sequence
 
@@ -36,3 +38,13 @@ def safe_int(val: Union[str, int, float]) -> int:
         return int(val)
     except ValueError:
         return 0
+
+
+def base64_encode_dict(dct: dict, **kwargs) -> bytes:
+    """Base64 encode a dictionary. Takes the same keywords as `json.dumps` function."""
+    return base64.urlsafe_b64encode(json.dumps(dct, **kwargs).encode("utf-8"))
+
+
+def base64_decode_to_dict(value: str, **kwargs) -> dict:
+    """Base64 encode a dictionary. Takes the same keywords as `json.loads` function."""
+    return json.loads(base64.urlsafe_b64decode(value), **kwargs)
diff --git a/uploader/db/datasets.py b/uploader/db/datasets.py
index 767ec41..4b263f5 100644
--- a/uploader/db/datasets.py
+++ b/uploader/db/datasets.py
@@ -53,7 +53,7 @@ def probeset_study_by_id(conn: mdb.Connection, studyid) -> Optional[dict]:
         _study = cursor.fetchone()
         return dict(_study) if bool(_study) else None
 
-def probeset_create_study(conn: mdb.Connection,#pylint: disable=[too-many-arguments]
+def probeset_create_study(conn: mdb.Connection,#pylint: disable=[too-many-arguments, too-many-positional-arguments]
                           populationid: int,
                           platformid: int,
                           tissueid: int,
@@ -87,7 +87,7 @@ def probeset_create_study(conn: mdb.Connection,#pylint: disable=[too-many-argume
                        (studyid, studyid))
         return {**studydata, "studyid": studyid}
 
-def probeset_create_dataset(conn: mdb.Connection,#pylint: disable=[too-many-arguments]
+def probeset_create_dataset(conn: mdb.Connection,#pylint: disable=[too-many-arguments, too-many-positional-arguments]
                             studyid: int,
                             averageid: int,
                             datasetname: str,
diff --git a/uploader/default_settings.py b/uploader/default_settings.py
index 1136ff8..bb3a967 100644
--- a/uploader/default_settings.py
+++ b/uploader/default_settings.py
@@ -2,7 +2,6 @@
 The default configuration file. The values here should be overridden in the
 actual configuration file used for the production and staging systems.
 """
-import hashlib
 
 LOG_LEVEL = "WARNING"
 SECRET_KEY = b"<Please! Please! Please! Change This!>"
@@ -25,8 +24,12 @@ SESSION_FILESYSTEM_CACHE_PATH = "./flask_session"
 SESSION_FILESYSTEM_CACHE_THRESHOLD = 500
 SESSION_FILESYSTEM_CACHE_TIMEOUT = 300
 SESSION_FILESYSTEM_CACHE_MODE = 0o600
-SESSION_FILESYSTEM_CACHE_HASH_METHOD = hashlib.md5
+SESSION_FILESYSTEM_CACHE_HASH_METHOD = None # default: hashlib.md5
 ## --- END: Settings for CacheLib session type --- ##
 
 JWKS_ROTATION_AGE_DAYS = 7 # Days (from creation) to keep a JWK in use.
 JWKS_DELETION_AGE_DAYS = 14 # Days (from creation) to keep a JWK around before deleting it.
+
+
+## --- Feature flags ---
+FEATURE_FLAGS_HTTP = []
diff --git a/uploader/expression_data/views.py b/uploader/expression_data/views.py
index 7629f3e..0b318b7 100644
--- a/uploader/expression_data/views.py
+++ b/uploader/expression_data/views.py
@@ -11,7 +11,6 @@ from werkzeug.utils import secure_filename
 from gn_libs.mysqldb import database_connection
 from flask import (flash,
                    request,
-                   url_for,
                    redirect,
                    Blueprint,
                    current_app as app)
@@ -19,6 +18,7 @@ from flask import (flash,
 from quality_control.errors import InvalidValue, DuplicateHeading
 
 from uploader import jobs
+from uploader.flask_extensions import url_for
 from uploader.datautils import order_by_family
 from uploader.ui import make_template_renderer
 from uploader.authorisation import require_login
diff --git a/uploader/flask_extensions.py b/uploader/flask_extensions.py
new file mode 100644
index 0000000..30fbad7
--- /dev/null
+++ b/uploader/flask_extensions.py
@@ -0,0 +1,33 @@
+"""Custom extensions to the default flask functions/classes."""
+import logging
+from typing import Any, Optional
+
+from flask import (request, current_app as app, url_for as flask_url_for)
+
+logger = logging.getLogger(__name__)
+
+
+def url_for(
+        endpoint: str,
+        _anchor: Optional[str] = None,
+        _method: Optional[str] = None,
+        _scheme: Optional[str] = None,
+        _external: Optional[bool] = None,
+        **values: Any) -> str:
+    """Extension to flask's `url_for` function."""
+    flags = {}
+    for flag in app.config["FEATURE_FLAGS_HTTP"]:
+        flag_value = (request.args.get(flag) or request.form.get(flag) or "").strip()
+        if bool(flag_value):
+            flags[flag] = flag_value
+            continue
+        continue
+
+    logger.debug("HTTP FEATURE FLAGS: %s, other variables: %s", flags, values)
+    return flask_url_for(endpoint=endpoint,
+                         _anchor=_anchor,
+                         _method=_method,
+                         _scheme=_scheme,
+                         _external=_external,
+                         **values,
+                         **flags)
diff --git a/uploader/genotypes/views.py b/uploader/genotypes/views.py
index 54c2444..d991614 100644
--- a/uploader/genotypes/views.py
+++ b/uploader/genotypes/views.py
@@ -3,12 +3,12 @@ from MySQLdb.cursors import DictCursor
 from gn_libs.mysqldb import database_connection
 from flask import (flash,
                    request,
-                   url_for,
                    redirect,
                    Blueprint,
                    render_template,
                    current_app as app)
 
+from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
 from uploader.oauth2.client import oauth2_post
 from uploader.authorisation import require_login
diff --git a/uploader/oauth2/client.py b/uploader/oauth2/client.py
index 12fbf80..b94a044 100644
--- a/uploader/oauth2/client.py
+++ b/uploader/oauth2/client.py
@@ -43,7 +43,8 @@ def __fetch_auth_server_jwks__() -> KeySet:
     return KeySet([
         JsonWebKey.import_key(key)
         for key in requests.get(
-                urljoin(authserver_uri(), "auth/public-jwks")
+                urljoin(authserver_uri(), "auth/public-jwks"),
+                timeout=(9.13, 20)
         ).json()["jwks"]])
 
 
diff --git a/uploader/oauth2/tokens.py b/uploader/oauth2/tokens.py
new file mode 100644
index 0000000..eb650f6
--- /dev/null
+++ b/uploader/oauth2/tokens.py
@@ -0,0 +1,47 @@
+"""Utilities for dealing with tokens."""
+import uuid
+from typing import Union
+from urllib.parse import urljoin
+from datetime import datetime, timedelta
+
+from authlib.jose import jwt
+from flask import current_app as app
+
+from uploader import monadic_requests as mrequests
+
+from . import jwks
+from .client import (SCOPE, authserver_uri, oauth2_clientid)
+
+
+def request_token(token_uri: str, user_id: Union[uuid.UUID, str], **kwargs):
+    """Request token from the auth server."""
+    issued = datetime.now()
+    jwtkey = jwks.newest_jwk_with_rotation(
+        jwks.jwks_directory(app, "UPLOADER_SECRETS"),
+        int(app.config["JWKS_ROTATION_AGE_DAYS"]))
+    _mins2expiry = kwargs.get("minutes_to_expiry", 5)
+    return mrequests.post(
+        token_uri,
+        json={
+            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
+            "scope": kwargs.get("scope", SCOPE),
+            "assertion": jwt.encode(
+                header={
+                    "alg": "RS256",
+                    "typ": "JWT",
+                    "kid": jwtkey.as_dict()["kid"]
+                },
+                payload={
+                    "iss": str(oauth2_clientid()),
+                    "sub": str(user_id),
+                    "aud": urljoin(authserver_uri(), "auth/token"),
+                    "exp": (issued + timedelta(minutes=_mins2expiry)).timestamp(),
+                    "nbf": int(issued.timestamp()),
+                    "iat": int(issued.timestamp()),
+                    "jti": str(uuid.uuid4())
+                },
+                key=jwtkey).decode("utf8"),
+            "client_id": oauth2_clientid(),
+            **kwargs.get("extra_params", {})
+        }
+    )
diff --git a/uploader/oauth2/views.py b/uploader/oauth2/views.py
index db4ef61..b1b740f 100644
--- a/uploader/oauth2/views.py
+++ b/uploader/oauth2/views.py
@@ -1,26 +1,22 @@
 """Views for OAuth2 related functionality."""
-import uuid
-from datetime import datetime, timedelta
 from urllib.parse import urljoin, urlparse, urlunparse
 
-from authlib.jose import jwt
 from flask import (
     flash,
     jsonify,
-    url_for,
     request,
     redirect,
     Blueprint,
     current_app as app)
 
 from uploader import session
+from uploader.flask_extensions import url_for
 from uploader import monadic_requests as mrequests
 from uploader.monadic_requests import make_error_handler
 
 from . import jwks
+from .tokens import request_token
 from .client import (
-    SCOPE,
-    oauth2_get,
     user_logged_in,
     authserver_uri,
     oauth2_clientid,
@@ -33,20 +29,20 @@ oauth2 = Blueprint("oauth2", __name__)
 @oauth2.route("/code")
 def authorisation_code():
     """Receive authorisation code from auth server and use it to get token."""
-    def __process_error__(resp_or_exception):
-        app.logger.debug("ERROR: (%s)", resp_or_exception)
+    def __process_error__(error_response):
+        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)
@@ -57,39 +53,17 @@ 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)
-    issued = datetime.now()
-    jwtkey = jwks.newest_jwk_with_rotation(
-        jwks.jwks_directory(app, "UPLOADER_SECRETS"),
-        int(app.config["JWKS_ROTATION_AGE_DAYS"]))
-    return mrequests.post(
-        urljoin(authserver_uri(), "auth/token"),
-        json={
-            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
+    return request_token(
+        token_uri=urljoin(authserver_uri(), "auth/token"),
+        user_id=request.args["user_id"],
+        extra_params={
             "code": code,
-            "scope": SCOPE,
             "redirect_uri": urljoin(
                 urlunparse(baseurl),
                 url_for("oauth2.authorisation_code")),
-            "assertion": jwt.encode(
-                header={
-                    "alg": "RS256",
-                    "typ": "JWT",
-                    "kid": jwtkey.as_dict()["kid"]
-                },
-                payload={
-                    "iss": str(oauth2_clientid()),
-                    "sub": request.args["user_id"],
-                    "aud": urljoin(authserver_uri(),"auth/token"),
-                    "exp": (issued + timedelta(minutes=5)).timestamp(),
-                    "nbf": int(issued.timestamp()),
-                    "iat": int(issued.timestamp()),
-                    "jti": str(uuid.uuid4())
-                },
-                key=jwtkey).decode("utf8"),
-            "client_id": oauth2_clientid()
         }).either(__process_error__, __success__)
 
 @oauth2.route("/public-jwks")
@@ -113,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(
@@ -126,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/models.py b/uploader/phenotypes/models.py
index a6ee694..e962e62 100644
--- a/uploader/phenotypes/models.py
+++ b/uploader/phenotypes/models.py
@@ -4,14 +4,15 @@ import tempfile
 from pathlib import Path
 from functools import reduce
 from datetime import datetime
-from typing import Optional, Iterable
+from typing import Union, Optional, Iterable
 
 import MySQLdb as mdb
 from MySQLdb.cursors import Cursor, DictCursor
 
-from functional_tools import take
 from gn_libs.mysqldb import debug_query
 
+from functional_tools import take
+
 logger = logging.getLogger(__name__)
 
 
@@ -91,7 +92,8 @@ def dataset_phenotypes(conn: mdb.Connection,
                        limit: Optional[int] = None) -> tuple[dict, ...]:
     """Fetch the actual phenotypes."""
     _query = (
-        "SELECT pheno.*, pxr.Id AS xref_id, pxr.InbredSetId, ist.InbredSetCode FROM Phenotype AS pheno "
+        "SELECT pheno.*, pxr.Id AS xref_id, pxr.InbredSetId, ist.InbredSetCode "
+        "FROM Phenotype AS pheno "
         "INNER JOIN PublishXRef AS pxr ON pheno.Id=pxr.PhenotypeId "
         "INNER JOIN PublishFreeze AS pf ON pxr.InbredSetId=pf.InbredSetId "
         "INNER JOIN InbredSet AS ist ON pf.InbredSetId=ist.Id "
@@ -217,7 +219,7 @@ def phenotype_by_id(
                 ).values())
             }
         if bool(_pheno) and len(_pheno.keys()) > 1:
-            raise Exception(
+            raise Exception(# pylint: disable=[broad-exception-raised]
                 "We found more than one phenotype with the same identifier!")
 
     return None
@@ -246,6 +248,59 @@ def phenotypes_data(conn: mdb.Connection,
         return tuple(dict(row) for row in cursor.fetchall())
 
 
+def phenotypes_vector_data(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        conn: mdb.Connection,
+        species_id: int,
+        population_id: int,
+        xref_ids: tuple[int, ...] = tuple(),
+        offset: int = 0,
+        limit: Optional[int] = None
+) -> dict[tuple[int, int, int]: dict[str, Union[int,float]]]:
+    """Retrieve the vector data values for traits in the database."""
+    _params = (species_id, population_id)
+    _query = ("SELECT "
+              "Species.Id AS SpeciesId, iset.Id AS InbredSetId, "
+              "pxr.Id AS xref_id, pdata.*, Strain.Id AS StrainId, "
+              "Strain.Name AS StrainName "
+              "FROM "
+              "Species INNER JOIN InbredSet AS iset "
+              "ON Species.Id=iset.SpeciesId "
+              "INNER JOIN PublishXRef AS pxr "
+              "ON iset.Id=pxr.InbredSetId "
+              "INNER JOIN PublishData AS pdata "
+              "ON pxr.DataId=pdata.Id "
+              "INNER JOIN Strain "
+              "ON pdata.StrainId=Strain.Id "
+              "WHERE Species.Id=%s AND iset.Id=%s")
+    if len(xref_ids) > 0:
+        _paramstr = ", ".join(["%s"] * len(xref_ids))
+        _query = _query + f" AND pxr.Id IN ({_paramstr})"
+        _params = _params + xref_ids
+
+    def __organise__(acc, row):
+        _rowid = (species_id, population_id, row["xref_id"])
+        _phenodata = {
+            **acc.get(
+                _rowid, {
+                    "species_id": species_id,
+                    "population_id": population_id,
+                    "xref_id": row["xref_id"]
+                }),
+            row["StrainName"]: row["value"]
+        }
+        return {
+            **acc,
+            _rowid: _phenodata
+        }
+
+    with conn.cursor(cursorclass=DictCursor) as cursor:
+        cursor.execute(
+            _query + (f" LIMIT {limit} OFFSET {offset}" if bool(limit) else ""),
+            _params)
+        debug_query(cursor, logger)
+        return reduce(__organise__, cursor.fetchall(), {})
+
+
 def save_new_dataset(cursor: Cursor,
                      population_id: int,
                      dataset_name: str,
@@ -302,32 +357,146 @@ def phenotypes_data_by_ids(
             reduce(__organise_by_phenotype__, cursor.fetchall(), {}).values())
 
 
-def create_new_phenotypes(conn: mdb.Connection,
-                          phenotypes: Iterable[dict]) -> tuple[dict, ...]:
-    """Add entirely new phenotypes to the database."""
+def __pre_process_phenotype_data__(row):
+    _desc = row.get("description", "")
+    _pre_pub_desc = row.get("pre_publication_description", _desc)
+    _orig_desc = row.get("original_description", _desc)
+    _post_pub_desc = row.get("post_publication_description", _orig_desc)
+    _pre_pub_abbr = row.get("pre_publication_abbreviation", row["id"])
+    _post_pub_abbr = row.get("post_publication_abbreviation", _pre_pub_abbr)
+    return {
+        "pre_publication_description": _pre_pub_desc,
+        "post_publication_description": _post_pub_desc,
+        "original_description": _orig_desc,
+        "units": row["units"],
+        "pre_publication_abbreviation": _pre_pub_abbr,
+        "post_publication_abbreviation": _post_pub_abbr
+    }
+
+
+def create_new_phenotypes(# pylint: disable=[too-many-locals]
+        conn: mdb.Connection,
+        population_id: int,
+        publication_id: int,
+        phenotypes: Iterable[dict]
+) -> tuple[dict, ...]:
+    """Add entirely new phenotypes to the database. WARNING: Not thread-safe."""
     _phenos = tuple()
     with conn.cursor(cursorclass=DictCursor) as cursor:
+        def make_next_id(idcol, table):
+            cursor.execute(f"SELECT MAX({idcol}) AS last_id FROM {table}")
+            _last_id = int(cursor.fetchone()["last_id"])
+            def __next_id__():
+                _next_id = _last_id + 1
+                while True:
+                    yield _next_id
+                    _next_id = _next_id + 1
+
+            return __next_id__
+
+        ### Bottleneck: Everything below makes this function not         ###
+        ###   thread-safe because we have to retrieve the last IDs from  ###
+        ###   the database and increment those to compute the next IDs.  ###
+        ###   This is an unfortunate result from the current schema that ###
+        ###   has a cross-reference table that requires that a phenotype ###
+        ###   be linked to an existing publication, and have data IDs to ###
+        ###   link to that phenotype's data.                             ###
+        ###   The fact that the IDs are sequential also compounds the    ###
+        ###   bottleneck.                                                ###
+        ###
+        ###   For extra safety, ensure the following tables are locked   ###
+        ###   for `WRITE`:                                               ###
+        ###   - PublishXRef                                              ###
+        ###   - Phenotype                                                ###
+        ###   - PublishXRef                                              ###
+        __next_xref_id = make_next_id("Id", "PublishXRef")()
+        __next_pheno_id__ = make_next_id("Id", "Phenotype")()
+        __next_data_id__ = make_next_id("DataId", "PublishXRef")()
+
+        def __build_params_and_prepubabbrevs__(acc, row):
+            processed = __pre_process_phenotype_data__(row)
+            return (
+                acc[0] + ({
+                    **processed,
+                    "population_id": population_id,
+                    "publication_id": publication_id,
+                    "phenotype_id": next(__next_pheno_id__),
+                    "xref_id": next(__next_xref_id),
+                    "data_id": next(__next_data_id__)
+                },),
+                acc[1] + (processed["pre_publication_abbreviation"],))
         while True:
             batch = take(phenotypes, 1000)
             if len(batch) == 0:
                 break
 
+            params, abbrevs = reduce(__build_params_and_prepubabbrevs__,
+                                     batch,
+                                     (tuple(), tuple()))
+            # Check for uniqueness for all "Pre_publication_description" values
+            abbrevs_paramsstr = ", ".join(["%s"] * len(abbrevs))
+            _query = ("SELECT PublishXRef.PhenotypeId, Phenotype.* "
+                      "FROM PublishXRef "
+                      "INNER JOIN Phenotype "
+                      "ON PublishXRef.PhenotypeId=Phenotype.Id "
+                      "WHERE PublishXRef.InbredSetId=%s "
+                      "AND Phenotype.Pre_publication_abbreviation IN "
+                      f"({abbrevs_paramsstr})")
+            cursor.execute(_query,
+                           ((population_id,) + abbrevs))
+            existing = tuple(row["Pre_publication_abbreviation"]
+                             for row in cursor.fetchall())
+            if len(existing) > 0:
+                # Narrow this exception, perhaps?
+                raise Exception(# pylint: disable=[broad-exception-raised]
+                    "Found already existing phenotypes with the following "
+                    "'Pre-publication abbreviations':\n\t"
+                    "\n\t".join(f"* {item}" for item in existing))
+
             cursor.executemany(
-                ("INSERT INTO "
-                 "Phenotype(Pre_publication_description, Original_description, Units, Authorized_Users) "
-                 "VALUES (%(id)s, %(description)s, %(units)s, 'robwilliams')"),
-                tuple(batch))
-            paramstr = ", ".join(["%s"] * len(batch))
-            cursor.execute(
-                "SELECT * FROM Phenotype WHERE Pre_publication_description IN "
-                f"({paramstr})",
-                tuple(item["id"] for item in batch))
-            _phenos = _phenos + tuple({
-                "phenotype_id": row["Id"],
-                "id": row["Pre_publication_description"],
-                "description": row["Original_description"],
-                "units": row["Units"]
-            } for row in cursor.fetchall())
+                (
+                    "INSERT INTO "
+                    "Phenotype("
+                    "Id, "
+                    "Pre_publication_description, "
+                    "Post_publication_description, "
+                    "Original_description, "
+                    "Units, "
+                    "Pre_publication_abbreviation, "
+                    "Post_publication_abbreviation, "
+                    "Authorized_Users"
+                    ")"
+                    "VALUES ("
+                    "%(phenotype_id)s, "
+                    "%(pre_publication_description)s, "
+                    "%(post_publication_description)s, "
+                    "%(original_description)s, "
+                    "%(units)s, "
+                    "%(pre_publication_abbreviation)s, "
+                    "%(post_publication_abbreviation)s, "
+                    "'robwilliams'"
+                    ")"),
+                params)
+            _comments = f"Created at {datetime.now().isoformat()}"
+            cursor.executemany(
+                ("INSERT INTO PublishXRef("
+                 "Id, "
+                 "InbredSetId, "
+                 "PhenotypeId, "
+                 "PublicationId, "
+                 "DataId, "
+                 "comments"
+                 ")"
+                 "VALUES("
+                 "%(xref_id)s, "
+                 "%(population_id)s, "
+                 "%(phenotype_id)s, "
+                 "%(publication_id)s, "
+                 "%(data_id)s, "
+                 f"'{_comments}'"
+                 ")"),
+                params)
+            _phenos = _phenos + params
 
     return _phenos
 
diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py
index 6bc7471..9df7d81 100644
--- a/uploader/phenotypes/views.py
+++ b/uploader/phenotypes/views.py
@@ -1,61 +1,55 @@
-"""Views handling ('classical') phenotypes."""
+"""Views handling ('classical') phenotypes."""# pylint: disable=[too-many-lines]
 import sys
-import csv
 import uuid
 import json
 import logging
-import tempfile
 from typing import Any
 from pathlib import Path
 from zipfile import ZipFile
-from urllib.parse import urljoin
 from functools import wraps, reduce
-from logging import INFO, ERROR, DEBUG, FATAL, CRITICAL, WARNING
+from urllib.parse import urljoin, urlparse, ParseResult, urlunparse, urlencode
 
 import datetime
-from datetime import timedelta
 
 from redis import Redis
 from pymonad.either import Left
 from requests.models import Response
 from MySQLdb.cursors import DictCursor
-from werkzeug.utils import secure_filename
 
 from gn_libs import sqlite3
 from gn_libs import jobs as gnlibs_jobs
 from gn_libs.jobs.jobs import JobNotFound
 from gn_libs.mysqldb import database_connection
-from gn_libs import monadic_requests as mrequests
 
-from authlib.jose import jwt
 from flask import (flash,
                    request,
-                   url_for,
                    jsonify,
                    redirect,
                    Blueprint,
-                   send_file,
                    current_app as app)
 
-# from r_qtl import r_qtl2 as rqtl2
 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#, fullpath
+from uploader.files import save_file
+from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
 from uploader.oauth2.client import oauth2_post
+from uploader.oauth2.tokens import request_token
 from uploader.authorisation import require_login
-from uploader.oauth2 import jwks, client as oauth2client
+from uploader.oauth2 import client as oauth2client
+from uploader.route_utils import build_next_argument
 from uploader.route_utils import generic_select_population
 from uploader.datautils import safe_int, enumerate_sequence
 from uploader.species.models import all_species, species_by_id
 from uploader.monadic_requests import make_either_error_handler
 from uploader.publications.models import fetch_publication_by_id
 from uploader.request_checks import with_species, with_population
-from uploader.samples.models import samples_by_species_and_population
 from uploader.input_validation import (encode_errors,
                                        decode_errors,
                                        is_valid_representative_name)
@@ -66,9 +60,9 @@ from .models import (dataset_by_id,
                      save_new_dataset,
                      dataset_phenotypes,
                      datasets_by_population,
-                     phenotypes_data_by_ids,
                      phenotype_publication_data)
 
+logger = logging.getLogger(__name__)
 phenotypesbp = Blueprint("phenotypes", __name__)
 render_template = make_template_renderer("phenotypes")
 
@@ -203,7 +197,7 @@ def view_dataset(# pylint: disable=[unused-argument]
 
         start_at = max(safe_int(request.args.get("start_at") or 0), 0)
         count = int(request.args.get("count") or 20)
-        return render_template("phenotypes/view-dataset.html",
+        return render_template(sui_template("phenotypes/view-dataset.html"),
                                species=species,
                                population=population,
                                dataset=dataset,
@@ -242,11 +236,6 @@ def view_phenotype(# pylint: disable=[unused-argument]
                                     population["Id"],
                                     dataset["Id"],
                                     xref_id)
-        def __non_empty__(value) -> bool:
-            if isinstance(value, str):
-                return value.strip() != ""
-            return bool(value)
-
         return render_template(
             "phenotypes/view-phenotype.html",
             species=species,
@@ -255,19 +244,14 @@ def view_phenotype(# pylint: disable=[unused-argument]
             xref_id=xref_id,
             phenotype=phenotype,
             has_se=any(bool(item.get("error")) for item in phenotype["data"]),
-            publish_data={
-                key.replace("_", " "): val
-                for key,val in
-                (phenotype_publication_data(conn, phenotype["Id"]) or {}).items()
-                if (key in ("PubMed_ID", "Authors", "Title", "Journal")
-                    and __non_empty__(val))
-            },
-            privileges=(privileges
-                        ### For demo! Do not commit this part
-                            + ("group:resource:edit-resource",
-                               "group:resource:delete-resource",)
-                        ### END: For demo! Do not commit this part
-                            ),
+            publication=(phenotype_publication_data(conn, phenotype["Id"]) or {}),
+            privileges=privileges,
+            next=build_next_argument(
+                uri="species.populations.phenotypes.view_phenotype",
+                species_id=species["SpeciesId"],
+                population_id=population["Id"],
+                dataset_id=dataset["Id"],
+                xref_id=xref_id),
             activelink="view-phenotype")
 
     def __fail__(error):
@@ -374,10 +358,17 @@ def process_phenotypes_individual_files(error_uri):
     bundlepath = Path(app.config["UPLOAD_FOLDER"],
                       f"{str(uuid.uuid4()).replace('-', '')}.zip")
     with ZipFile(bundlepath,mode="w") as zfile:
-        for rqtlkey, formkey in (("phenocovar", "phenotype-descriptions"),
-                                 ("pheno", "phenotype-data"),
-                                 ("phenose", "phenotype-se"),
-                                 ("phenonum", "phenotype-n")):
+        for rqtlkey, formkey, _type in (
+                ("phenocovar", "phenotype-descriptions", "mandatory"),
+                ("pheno", "phenotype-data", "mandatory"),
+                ("phenose", "phenotype-se", "optional"),
+                ("phenonum", "phenotype-n", "optional")):
+            if _type == "optional" and not bool(form.get(formkey)):
+                continue # skip if an optional key does not exist.
+
+            cdata[f"{rqtlkey}_transposed"] = (
+                (form.get(f"{formkey}-transposed") or "off") == "on")
+
             if form.get("resumable-upload", False):
                 # Chunked upload of large files was used
                 filedata = json.loads(form[formkey])
@@ -386,7 +377,7 @@ def process_phenotypes_individual_files(error_uri):
                     arcname=filedata["original-name"])
                 cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filedata["original-name"]]
             else:
-                # TODO: Check this path: fix any bugs.
+                # T0DO: Check this path: fix any bugs.
                 _sentfile = request.files[formkey]
                 if not bool(_sentfile):
                     flash(f"Expected file ('{formkey}') was not provided.",
@@ -400,6 +391,7 @@ def process_phenotypes_individual_files(error_uri):
                     arcname=filepath.name)
                 cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filepath.name]
 
+
         zfile.writestr("control_data.json", data=json.dumps(cdata, indent=2))
 
     return bundlepath
@@ -424,15 +416,13 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
         dataset_id=dataset["Id"]))
     _redisuri = app.config["REDIS_URL"]
     _sqluri = app.config["SQL_URI"]
-    with (Redis.from_url(_redisuri, decode_responses=True) as rconn,
-          # database_connection(_sqluri) as conn,
-          # conn.cursor(cursorclass=DictCursor) as cursor
-          ):
+    with Redis.from_url(_redisuri, decode_responses=True) as rconn:
         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 "phenotypes/add-phenotypes-raw-files.html"),
                 species=species,
                 population=population,
                 dataset=dataset,
@@ -462,7 +452,6 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
                 [sys.executable, "-m", "scripts.rqtl2.phenotypes_qc", _sqluri,
                  _redisuri, _namespace, str(_jobid), str(species["SpeciesId"]),
                  str(population["Id"]),
-                 # str(dataset["Id"]),
              str(phenobundle),
                  "--loglevel",
                  logging.getLevelName(
@@ -519,7 +508,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,
@@ -599,7 +589,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,
@@ -640,12 +630,16 @@ def load_data_to_database(
         **kwargs
 ):# pylint: disable=[unused-argument]
     """Load the data from the given QC job into the database."""
-    jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
+    _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
     with (Redis.from_url(app.config["REDIS_URL"], decode_responses=True) as rconn,
-          sqlite3.connection(jobs_db) as conn):
+          sqlite3.connection(_jobs_db) as conn):
+        # T0DO: Maybe break the connection between the jobs here, pass:
+        # - the bundle name (rebuild the full path here.)
+        # - publication details, where separate
+        # - details about the files: e.g. total lines, etc
         qc_job = jobs.job(rconn, jobs.jobsnamespace(), request.form["data-qc-job-id"])
         _meta = json.loads(qc_job["job-metadata"])
-        load_job_id = uuid.uuid4()
+        _load_job_id = uuid.uuid4()
         _loglevel = logging.getLevelName(app.logger.getEffectiveLevel()).lower()
         command = [
             sys.executable,
@@ -653,8 +647,8 @@ def load_data_to_database(
             "-m",
             "scripts.load_phenotypes_to_db",
             app.config["SQL_URI"],
-            jobs_db,
-            str(load_job_id),
+            _jobs_db,
+            str(_load_job_id),
             "--log-level",
             _loglevel
         ]
@@ -667,41 +661,14 @@ def load_data_to_database(
             return redirect(url_for(
                 "background-jobs.job_status", job_id=load_job["job_id"]))
 
-        issued = datetime.datetime.now()
-        jwtkey = jwks.newest_jwk_with_rotation(
-            jwks.jwks_directory(app, "UPLOADER_SECRETS"),
-            int(app.config["JWKS_ROTATION_AGE_DAYS"]))
 
-        return mrequests.post(
-            urljoin(oauth2client.authserver_uri(), "auth/token"),
-            json={
-                "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
-                "scope": oauth2client.SCOPE,
-                "assertion": jwt.encode(
-                    header={
-                        "alg": "RS256",
-                        "typ": "JWT",
-                        "kid": jwtkey.as_dict()["kid"]
-                    },
-                    payload={
-                        "iss": str(oauth2client.oauth2_clientid()),
-                        "sub": str(session.user_details()["user_id"]),
-                        "aud": urljoin(oauth2client.authserver_uri(),
-                                       "auth/token"),
-                        # TODO: Update expiry time once fix is implemented in
-                        #       auth server.
-                        "exp": (issued + timedelta(minutes=5)).timestamp(),
-                        "nbf": int(issued.timestamp()),
-                        "iat": int(issued.timestamp()),
-                        "jti": str(uuid.uuid4())
-                    },
-                    key=jwtkey).decode("utf8"),
-                "client_id": oauth2client.oauth2_clientid()
-            }
+        return request_token(
+            token_uri=urljoin(oauth2client.authserver_uri(), "auth/token"),
+            user_id=session.user_details()["user_id"]
         ).then(
             lambda token: gnlibs_jobs.initialise_job(
                 conn,
-                load_job_id,
+                _load_job_id,
                 command,
                 "load-new-phenotypes-data",
                 extra_meta={
@@ -719,8 +686,8 @@ def load_data_to_database(
         ).then(
             lambda job: gnlibs_jobs.launch_job(
                 job,
-                jobs_db,
-                f"{app.config['UPLOAD_FOLDER']}/job_errors",
+                _jobs_db,
+                Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"),
                 worker_manager="gn_libs.jobs.launcher",
                 loglevel=_loglevel)
         ).either(__handle_error__, __handle_success__)
@@ -878,12 +845,7 @@ def edit_phenotype_data(# pylint: disable=[unused-argument]
     def __render__(**kwargs):
         processed_kwargs = {
             **kwargs,
-            "privileges": (kwargs.get("privileges", tuple())
-                           ### For demo! Do not commit this part
-                            + ("group:resource:edit-resource",
-                               "group:resource:delete-resource",)
-                           ### END: For demo! Do not commit this part
-                            )
+            "privileges": kwargs.get("privileges", tuple())
         }
         return render_template(
             "phenotypes/edit-phenotype.html",
@@ -983,181 +945,213 @@ def edit_phenotype_data(# pylint: disable=[unused-argument]
             xref_id=xref_id))
 
 
-def process_phenotype_data_for_download(pheno: dict) -> dict:
-    """Sanitise data for download."""
-    return {
-        "UniqueIdentifier": f"phId:{pheno['Id']}::xrId:{pheno['xref_id']}",
-        **{
-            key: val for key, val in pheno.items()
-            if key not in ("Id", "xref_id", "data", "Units")
-        },
-        **{
-            data_item["StrainName"]: data_item["value"]
-            for data_item in pheno.get("data", {}).values()
-        }
-    }
-
-
-BULK_EDIT_COMMON_FIELDNAMES = [
-    "UniqueIdentifier",
-    "Post_publication_description",
-    "Pre_publication_abbreviation",
-    "Pre_publication_description",
-    "Original_description",
-    "Post_publication_abbreviation",
-    "PubMed_ID"
-]
-
-
 @phenotypesbp.route(
     "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
-    "/<int:dataset_id>/edit-download",
-    methods=["POST"])
+    "/<int:dataset_id>/load-data-success/<uuid:job_id>",
+    methods=["GET"])
 @require_login
 @with_dataset(
     species_redirect_uri="species.populations.phenotypes.index",
     population_redirect_uri="species.populations.phenotypes.select_population",
     redirect_uri="species.populations.phenotypes.list_datasets")
-def edit_download_phenotype_data(# pylint: disable=[unused-argument]
+def load_data_success(
         species: dict,
         population: dict,
         dataset: dict,
+        job_id: uuid.UUID,
         **kwargs
-):
-    formdata = request.json
-    with database_connection(app.config["SQL_URI"]) as conn:
-        samples_list = [
-            sample["Name"] for sample in samples_by_species_and_population(
-                conn, species["SpeciesId"], population["Id"])]
-        data = (
-            process_phenotype_data_for_download(pheno)
-            for pheno in phenotypes_data_by_ids(conn, tuple({
-                    "population_id": population["Id"],
-                    "phenoid": row["phenotype_id"],
-                    "xref_id": row["xref_id"]
-            } for row in formdata)))
-
-        with (tempfile.TemporaryDirectory(
-                prefix=app.config["TEMPORARY_DIRECTORY"]) as tmpdir):
-            filename = Path(tmpdir).joinpath("tempfile.tsv")
-            with open(filename, mode="w") as outfile:
-                outfile.write(
-                    "# **DO NOT** delete the 'UniqueIdentifier' row. It is used "
-                    "by the system to identify and edit the correct rows and "
-                    "columns in the database.\n")
-                outfile.write(
-                    "# The '…_description' fields are useful for you to figure out "
-                    "what row you are working on. Changing any of this fields will "
-                    "also update the database, so do be careful.\n")
-                outfile.write(
-                    "# Leave a field empty to delete the value in the database.\n")
-                outfile.write(
-                    "# Any line beginning with a '#' character is considered a "
-                    "comment line. This line, and all the lines above it, are "
-                    "all comment lines. Comment lines will be ignored.\n")
-                writer = csv.DictWriter(outfile,
-                                        fieldnames= (
-                                            BULK_EDIT_COMMON_FIELDNAMES +
-                                            samples_list),
-                                        dialect="excel-tab")
-                writer.writeheader()
-                writer.writerows(data)
-                outfile.flush()
-
-            return send_file(
-                filename,
-                mimetype="text/csv",
-                as_attachment=True,
-                download_name=secure_filename(f"{dataset['Name']}_data"))
+):# pylint: disable=[unused-argument]
+    """Display success page if loading data to database was successful."""
+    with (database_connection(app.config["SQL_URI"]) as conn,
+          sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"])
+          as jobsconn):
+        try:
+            gn2_uri = urlparse(app.config["GN2_SERVER_URL"])
+            job = gnlibs_jobs.job(jobsconn, job_id, fulldetails=True)
+            app.logger.debug("THE JOB: %s", job)
+            _xref_ids = tuple(
+                str(item) for item
+                in json.loads(job["metadata"].get("xref_ids", "[]")))
+            _publication = fetch_publication_by_id(
+                conn, int(job["metadata"].get("publication_id", "0")))
+            _search_terms = (item for item in
+                             (str(_publication["PubMed_ID"] or ""),
+                              _publication["Authors"],
+                              (_publication["Title"] or ""))
+                             if item != "")
+            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="")))
+        except JobNotFound as _jnf:
+            return render_template("jobs/job-not-found.html", job_id=job_id)
 
 
 @phenotypesbp.route(
     "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
-    "/<int:dataset_id>/edit-upload",
-    methods=["GET", "POST"])
+    "/<int:dataset_id>/recompute-means",
+    methods=["POST"])
 @require_login
 @with_dataset(
     species_redirect_uri="species.populations.phenotypes.index",
     population_redirect_uri="species.populations.phenotypes.select_population",
     redirect_uri="species.populations.phenotypes.list_datasets")
-def edit_upload_phenotype_data(# pylint: disable=[unused-argument]
+def recompute_means(# pylint: disable=[unused-argument]
         species: dict,
         population: dict,
         dataset: dict,
         **kwargs
 ):
-    if request.method == "GET":
-        return render_template(
-            "phenotypes/bulk-edit-upload.html",
-            species=species,
-            population=population,
-            dataset=dataset,
-            activelink="edit-phenotype")
-
-    edit_file = save_file(request.files["file-upload-bulk-edit-upload"],
-                          Path(app.config["UPLOAD_FOLDER"]))
-
-    jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
-    with sqlite3.connection(jobs_db) as conn:
-        job_id = uuid.uuid4()
-        job_cmd = [
-            sys.executable, "-u",
-            "-m", "scripts.phenotypes_bulk_edit",
-            app.config["SQL_URI"],
-            jobs_db,
-            str(job_id),
-            "--log-level",
-            logging.getLevelName(
-                app.logger.getEffectiveLevel()
-            ).lower()
-        ]
-        app.logger.debug("Phenotype-edit, bulk-upload command: %s", job_cmd)
+    """Compute/Recompute the means for phenotypes in a particular population."""
+    _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
+    _job_id = uuid.uuid4()
+    _xref_ids = tuple(int(item.split("_")[-1])
+                      for item in request.form.getlist("selected-phenotypes"))
+
+    _loglevel = logging.getLevelName(app.logger.getEffectiveLevel()).lower()
+    command = [
+        sys.executable,
+        "-u",
+        "-m",
+        "scripts.compute_phenotype_means",
+        app.config["SQL_URI"],
+        _jobs_db,
+        str(population["Id"]),
+        "--log-level",
+        _loglevel] + (
+            ["--cross-ref-ids", ",".join(str(_id) for _id in _xref_ids)]
+            if len(_xref_ids) > 0 else
+            [])
+    logger.debug("%s.recompute_means: command (%s)", __name__, command)
+
+    with sqlite3.connection(_jobs_db) as conn:
         _job = gnlibs_jobs.launch_job(
-            gnlibs_jobs.initialise_job(conn,
-                                       job_id,
-                                       job_cmd,
-                                       "phenotype-bulk-edit",
-                                       extra_meta = {
-                                           "edit-file": str(edit_file),
-                                           "species-id": species["SpeciesId"],
-                                           "population-id": population["Id"],
-                                           "dataset-id": dataset["Id"]
-                                       }),
-            jobs_db,
-            f"{app.config['UPLOAD_FOLDER']}/job_errors",
-            worker_manager="gn_libs.jobs.launcher")
-
-
-    return redirect(url_for("background-jobs.job_status",
-                            job_id=job_id,
-                            job_type="phenotype-bulk-edit"))
+            gnlibs_jobs.initialise_job(
+                conn,
+                _job_id,
+                command,
+                "(re)compute-phenotype-means",
+                extra_meta={
+                    "species_id": species["SpeciesId"],
+                    "population_id": population["Id"],
+                    "dataset_id": dataset["Id"],
+                    "success_handler": (
+                        "uploader.phenotypes.views."
+                        "recompute_phenotype_means_success_handler")
+            }),
+            _jobs_db,
+            Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"),
+            worker_manager="gn_libs.jobs.launcher",
+            loglevel=_loglevel)
+        return redirect(url_for("background-jobs.job_status",
+                                job_id=_job["job_id"]))
+
+
+def return_to_dataset_view_handler(job, msg: str):
+    """Handler for background jobs: Returns to `View Dataset` page."""
+    flash(msg, "alert alert-success")
+    return redirect(url_for(
+        "species.populations.phenotypes.view_dataset",
+        species_id=job["metadata"]["species_id"],
+        population_id=job["metadata"]["population_id"],
+        dataset_id=job["metadata"]["dataset_id"],
+        job_id=job["job_id"]))
+
+def recompute_phenotype_means_success_handler(job):
+    """Handle loading new phenotypes into the database successfully."""
+    return return_to_dataset_view_handler(job, "Means computed successfully!")
 
 
 @phenotypesbp.route(
     "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
-    "/<int:dataset_id>/load-data-success/<uuid:job_id>",
-    methods=["GET"])
+    "/<int:dataset_id>/rerun-qtlreaper",
+    methods=["POST"])
 @require_login
 @with_dataset(
     species_redirect_uri="species.populations.phenotypes.index",
     population_redirect_uri="species.populations.phenotypes.select_population",
     redirect_uri="species.populations.phenotypes.list_datasets")
-def load_data_success(
+def rerun_qtlreaper(# pylint: disable=[unused-argument]
         species: dict,
         population: dict,
         dataset: dict,
-        job_id: uuid.UUID,
         **kwargs
-):# pylint: disable=[unused-argument]
-    with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn:
-        try:
-            job = gnlibs_jobs.job(conn, job_id, fulldetails=True)
-            app.logger.debug("THE JOB: %s", job)
-            return render_template("phenotypes/load-phenotypes-success.html",
-                                   species=species,
-                                   population=population,
-                                   dataset=dataset,
-                                   job=job,
-                                   gn2_server_url=app.config["GN2_SERVER_URL"])
-        except JobNotFound as jnf:
-            return render_template("jobs/job-not-found.html", job_id=job_id)
+):
+    """(Re)run QTLReaper for phenotypes in a particular population."""
+    _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]
+    _job_id = uuid.uuid4()
+    _loglevel = logging.getLevelName(app.logger.getEffectiveLevel()).lower()
+
+    _workingdir = Path(app.config["TEMPORARY_DIRECTORY"]).joinpath("qtlreaper")
+    _workingdir.mkdir(exist_ok=True)
+    command = [
+        sys.executable,
+        "-u",
+        "-m",
+        "scripts.run_qtlreaper",
+        "--log-level", _loglevel,
+        app.config["SQL_URI"],
+        str(species["SpeciesId"]),
+        str(population["Id"]),
+        str(Path(app.config["GENOTYPE_FILES_DIRECTORY"]).joinpath(
+            "genotype")),
+        str(_workingdir)
+    ] + [
+        str(_xref_id) for _xref_id in (
+            int(item.split("_")[-1])
+            for item in request.form.getlist("selected-phenotypes"))
+    ]
+    logger.debug("(Re)run QTLReaper: %s", command)
+    with sqlite3.connection(_jobs_db) as conn:
+        _job_id = uuid.uuid4()
+        _job = gnlibs_jobs.launch_job(
+            gnlibs_jobs.initialise_job(
+                conn,
+                _job_id,
+                command,
+                "(re)run-qtlreaper",
+                extra_meta={
+                    "species_id": species["SpeciesId"],
+                    "population_id": population["Id"],
+                    "dataset_id": dataset["Id"],
+                    "success_handler": (
+                        "uploader.phenotypes.views."
+                        "rerun_qtlreaper_success_handler")
+            }),
+            _jobs_db,
+            Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"),
+            worker_manager="gn_libs.jobs.launcher",
+            loglevel=_loglevel)
+        return redirect(url_for("background-jobs.job_status",
+                                job_id=_job["job_id"]))
+    return redirect(url_for(
+        "background-jobs.job_status", job_id=_job["job_id"]))
+
+
+def rerun_qtlreaper_success_handler(job):
+    """Handle success (re)running QTLReaper script."""
+    return return_to_dataset_view_handler(job, "QTLReaper ran successfully!")
diff --git a/uploader/platforms/views.py b/uploader/platforms/views.py
index d12a9ef..0092760 100644
--- a/uploader/platforms/views.py
+++ b/uploader/platforms/views.py
@@ -4,11 +4,13 @@ from gn_libs.mysqldb import database_connection
 from flask import (
     flash,
     request,
-    url_for,
     redirect,
     Blueprint,
     current_app as app)
 
+from uploader.sui import sui_template
+
+from uploader.flask_extensions import url_for
 from uploader.ui import make_template_renderer
 from uploader.authorisation import require_login
 from uploader.species.models import all_species, species_by_id
@@ -60,7 +62,7 @@ def list_platforms(species_id: int):
         start_from = max(safe_int(request.args.get("start_from") or 0), 0)
         count = safe_int(request.args.get("count") or 20)
         return render_template(
-            "platforms/list-platforms.html",
+            sui_template("platforms/list-platforms.html"),
             species=species,
             platforms=enumerate_sequence(
                 platforms_by_species(conn,
@@ -87,7 +89,7 @@ def create_platform(species_id: int):
 
         if request.method == "GET":
             return render_template(
-                "platforms/create-platform.html",
+                sui_template("platforms/create-platform.html"),
                 species=species,
                 activelink="create-platform")
 
diff --git a/uploader/population/models.py b/uploader/population/models.py
index d78a821..4d95065 100644
--- a/uploader/population/models.py
+++ b/uploader/population/models.py
@@ -26,13 +26,23 @@ def populations_by_species(conn: mdb.Connection, speciesid) -> tuple:
 
     return tuple()
 
+__GENERIC_POPULATION_FAMILIES__ = (
+    "Reference Populations (replicate average, SE, N)",
+    "Crosses and Heterogeneous Stock (individuals)",
+    "Groups Without Genotypes")
 
-def population_families(conn) -> tuple:
+def population_families(conn, species_id: int) -> tuple[str]:
     """Fetch the families under which populations are grouped."""
     with conn.cursor(cursorclass=DictCursor) as cursor:
+        paramstr = ", ".join(["%s"] * len(__GENERIC_POPULATION_FAMILIES__))
         cursor.execute(
-            "SELECT DISTINCT(Family) FROM InbredSet WHERE Family IS NOT NULL")
-        return tuple(row["Family"] for row in cursor.fetchall())
+            "SELECT DISTINCT(Family) FROM InbredSet "
+            "WHERE SpeciesId=%s "
+            "AND Family IS NOT NULL "
+            f"AND Family NOT IN ({paramstr})",
+            (species_id, *__GENERIC_POPULATION_FAMILIES__))
+        return __GENERIC_POPULATION_FAMILIES__ + tuple(
+            row["Family"] for row in cursor.fetchall())
 
 
 def population_genetic_types(conn) -> tuple:
@@ -47,9 +57,11 @@ def population_genetic_types(conn) -> tuple:
 def save_population(cursor: mdb.cursors.Cursor, population_details: dict) -> dict:
     """Save the population details to the db."""
     cursor.execute("SELECT DISTINCT(Family), FamilyOrder FROM InbredSet "
-                   "WHERE Family IS NOT NULL AND Family != '' "
+                   "WHERE SpeciesId=%s "
+                   "AND Family IS NOT NULL AND Family != '' "
                    "AND FamilyOrder IS NOT NULL "
-                   "ORDER BY FamilyOrder ASC")
+                   "ORDER BY FamilyOrder ASC",
+                   (population_details["SpeciesId"],))
     _families = {
         row["Family"]: int(row["FamilyOrder"])
         for row in cursor.fetchall()
diff --git a/uploader/population/rqtl2.py b/uploader/population/rqtl2.py
index 044cdd4..97d4854 100644
--- a/uploader/population/rqtl2.py
+++ b/uploader/population/rqtl2.py
@@ -12,9 +12,9 @@ import MySQLdb as mdb
 from redis import Redis
 from MySQLdb.cursors import DictCursor
 from gn_libs.mysqldb import database_connection
+from markupsafe import escape
 from flask import (
     flash,
-    escape,
     request,
     url_for,
     redirect,
diff --git a/uploader/population/views.py b/uploader/population/views.py
index 270dd5f..a6e2358 100644
--- a/uploader/population/views.py
+++ b/uploader/population/views.py
@@ -7,12 +7,14 @@ from MySQLdb.cursors import DictCursor
 from gn_libs.mysqldb import database_connection
 from flask import (flash,
                    request,
-                   url_for,
                    redirect,
                    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
 from uploader.ui import make_template_renderer
 from uploader.authorisation import require_login
@@ -23,6 +25,8 @@ 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,
@@ -100,7 +104,7 @@ def create_population(species_id: int):
             return render_template(
                 "populations/create-population.html",
                 species=species,
-                families = population_families(conn),
+                families = population_families(conn, species["SpeciesId"]),
                 genetic_types = population_genetic_types(conn),
                 mapping_methods=(
                     {"id": "0", "value": "No mapping support"},
@@ -153,7 +157,7 @@ def create_population(species_id: int):
             "FullName": population_fullname,
             "InbredSetCode": request.form.get("population_code") or None,
             "Description": request.form.get("population_description") or None,
-            "Family": request.form.get("population_family") or None,
+            "Family": request.form.get("population_family").strip() or None,
             "MappingMethodId": request.form.get("population_mapping_method_id"),
             "GeneticType": request.form.get("population_genetic_type") or None
         })
@@ -193,10 +197,15 @@ def create_population(species_id: int):
 @require_login
 def view_population(species_id: int, population_id: int):
     """View the details of a population."""
+    streamlined_ui =  request.args.get("streamlined_ui")
     with database_connection(app.config["SQL_URI"]) as conn:
         species = species_by_id(conn, species_id)
         population = population_by_species_and_id(conn, species_id, population_id)
+        datasets = datasets_by_population(conn, species_id, population_id)
         error = False
+        if len(datasets) > 1:
+            error = True
+            flash("Got more than one dataset for the population.", "alert alert-danger")
 
         if not bool(species):
             flash("You must select a species.", "alert-danger")
@@ -207,9 +216,33 @@ def view_population(species_id: int, population_id: int):
             error = True
 
         if error:
-            return redirect(url_for("species.populations.index"))
+            return redirect(url_for(("species.view_species"
+                                     if bool(streamlined_ui)
+                                     else "species.populations.index"),
+                                    species_id=species["SpeciesId"],
+                                    streamlined_ui=streamlined_ui))
+
+        _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("populations/view-population.html",
-                               species=species,
-                               population=population,
-                               activelink="view-population")
+        return render_template(sui_template("populations/view-population.html"),
+                               **_kwargs)
diff --git a/uploader/publications/models.py b/uploader/publications/models.py
index b199991..dcfa02b 100644
--- a/uploader/publications/models.py
+++ b/uploader/publications/models.py
@@ -1,6 +1,6 @@
 """Module to handle persistence and retrieval of publication to/from MariaDB"""
 import logging
-from typing import Iterable, Optional
+from typing import Iterable
 
 from MySQLdb.cursors import DictCursor
 
@@ -30,6 +30,7 @@ def create_new_publications(
         conn: Connection,
         publications: tuple[dict, ...]
 ) -> tuple[dict, ...]:
+    """Create new publications in the database."""
     if len(publications) > 0:
         with conn.cursor(cursorclass=DictCursor) as cursor:
             cursor.executemany(
@@ -47,7 +48,8 @@ def create_new_publications(
             return tuple({
                 **row, "publication_id": row["Id"]
             } for row in cursor.fetchall())
-        return tuple()
+
+    return tuple()
 
 
 def update_publications(conn: Connection , publications: tuple[dict, ...]) -> tuple[dict, ...]:
@@ -69,6 +71,27 @@ def update_publications(conn: Connection , publications: tuple[dict, ...]) -> tu
     return tuple()
 
 
+def delete_publications(conn: Connection , publications: tuple[dict, ...]):
+    """Delete multiple publications"""
+    publications = tuple(pub for pub in publications if bool(pub))
+    if len(publications) > 0:
+        _pub_ids = tuple(pub["Id"] for pub in publications)
+        _paramstr = ", ".join(["%s"] * len(_pub_ids))
+        _phenos_query = (
+            "SELECT PublicationId, COUNT(PhenotypeId) FROM PublishXRef "
+            f"WHERE PublicationId IN ({_paramstr}) GROUP BY PublicationId;")
+
+        with conn.cursor(cursorclass=DictCursor) as cursor:
+            cursor.execute(_phenos_query, _pub_ids)
+            _linked_phenos = cursor.fetchall()
+            if len(_linked_phenos) > 0:
+                raise Exception(# pylint: disable=[broad-exception-raised]
+                    "Cannot delete publications with linked phenotypes.")
+
+            cursor.execute(
+                f"DELETE FROM Publication WHERE Id IN ({_paramstr})", _pub_ids)
+
+
 def fetch_publication_by_id(conn: Connection, publication_id: int) -> dict:
     """Fetch a specific publication from the database."""
     with conn.cursor(cursorclass=DictCursor) as cursor:
diff --git a/uploader/publications/pubmed.py b/uploader/publications/pubmed.py
index ed9b652..2531c4a 100644
--- a/uploader/publications/pubmed.py
+++ b/uploader/publications/pubmed.py
@@ -29,9 +29,7 @@ def __journal__(journal: etree.Element) -> dict:
     }
 
 def __author__(author: etree.Element) -> str:
-    return "%s %s" % (
-        author.find("LastName").text,
-        author.find("Initials").text)
+    return f'{author.find("LastName").text} {author.find("Initials").text}'
 
 
 def __pages__(pagination: etree.Element) -> str:
@@ -88,7 +86,8 @@ def fetch_publications(pubmed_ids: tuple[int, ...]) -> tuple[dict, ...]:
                 "db": "pubmed",
                 "retmode": "xml",
                 "id": ",".join(str(item) for item in pubmed_ids)
-            })
+            },
+            timeout=(9.13, 20))
 
         if response.status_code == 200:
             return __process_pubmed_publication_data__(response.text)
diff --git a/uploader/publications/views.py b/uploader/publications/views.py
index 137052b..f0ec95a 100644
--- a/uploader/publications/views.py
+++ b/uploader/publications/views.py
@@ -1,28 +1,30 @@
 """Endpoints for publications"""
 import json
 
-from MySQLdb.cursors import DictCursor
 from gn_libs.mysqldb import database_connection
 from flask import (
     flash,
     request,
-    url_for,
     redirect,
     Blueprint,
     render_template,
     current_app as app)
 
+from uploader.sui import sui_template
+
+from uploader.flask_extensions import url_for
 from uploader.authorisation import require_login
+from uploader.route_utils import redirect_to_next
 
 from .models import (
+    delete_publications,
+    update_publications,
     fetch_publication_by_id,
     create_new_publications,
     fetch_publication_phenotypes)
 
 from .datatables import fetch_publications
 
-from gn_libs.debug import __pk__
-
 pubbp = Blueprint("publications", __name__)
 
 
@@ -30,21 +32,21 @@ pubbp = Blueprint("publications", __name__)
 @require_login
 def index():
     """Index page for publications."""
-    with database_connection(app.config["SQL_URI"]) as conn:
-        return render_template("publications/index.html")
+    return render_template(sui_template("publications/index.html"))
 
 
 @pubbp.route("/list", methods=["GET"])
 @require_login
 def list_publications():
+    """Fetch publications that fulfill a specific search, or all of them, if
+    there is no search term."""
     # request breakdown:
     # https://datatables.net/manual/server-side
     _page = int(request.args.get("draw"))
     _length = int(request.args.get("length") or '-1')
     _start = int(request.args.get("start") or '0')
     _search = request.args["search[value]"]
-    with (database_connection(app.config["SQL_URI"]) as conn,
-          conn.cursor(cursorclass=DictCursor) as cursor):
+    with database_connection(app.config["SQL_URI"]) as conn:
         _publications, _current_rows, _totalfiltered, _totalrows = fetch_publications(
             conn,
             _search,
@@ -65,9 +67,15 @@ def list_publications():
 def view_publication(publication_id: int):
     """View more details on a particular publication."""
     with database_connection(app.config["SQL_URI"]) as conn:
+        publication = fetch_publication_by_id(conn, publication_id)
+
+        if not bool(publication):
+            flash("Requested publication was not found!", "alert-warning")
+            return redirect(url_for('publications.index'))
+
         return render_template(
-            "publications/view-publication.html",
-            publication=fetch_publication_by_id(conn, publication_id),
+            sui_template("publications/view-publication.html"),
+            publication=publication,
             linked_phenotypes=tuple(fetch_publication_phenotypes(
                 conn, publication_id)))
 
@@ -76,17 +84,25 @@ def view_publication(publication_id: int):
 @require_login
 def create_publication():
     """Create a new publication."""
-    if(request.method == "GET"):
-        return render_template("publications/create-publication.html")
+    _get_args = {
+        key: request.args[key]
+        for key in ("species_id", "population_id", "dataset_id", "return_to")
+        if bool(request.args.get(key))
+    }
+
+    if request.method == "GET":
+        return render_template(
+            sui_template("publications/create-publication.html"),
+            get_args=_get_args)
     form = request.form
     authors = form.get("publication-authors").encode("utf8")
     if authors is None or authors == "":
         flash("The publication's author(s) MUST be provided!", "alert alert-danger")
-        return redirect(url_for("publications.create", **request.args))
+        return redirect(url_for("publications.create"))
 
     with database_connection(app.config["SQL_URI"]) as conn:
         publications = create_new_publications(conn, ({
-            "pubmed_id": form.get("pubmed-id"),
+            "pubmed_id": form.get("pubmed-id") or None,
             "abstract": form.get("publication-abstract").encode("utf8") or None,
             "authors": authors,
             "title":  form.get("publication-title").encode("utf8") or None,
@@ -100,8 +116,81 @@ def create_publication():
         return redirect(url_for(
             request.args.get("return_to") or "publications.view_publication",
             publication_id=publications[0]["publication_id"],
-            **request.args))
+            **_get_args))
 
     flash("Publication creation failed!", "alert alert-danger")
     app.logger.debug("Failed to create the new publication.", exc_info=True)
     return redirect(url_for("publications.create_publication"))
+
+
+@pubbp.route("/edit/<int:publication_id>", methods=["GET", "POST"])
+@require_login
+def edit_publication(publication_id: int):
+    """Edit a publication's details."""
+    with database_connection(app.config["SQL_URI"]) as conn:
+        if request.method == "GET":
+            return render_template(
+                sui_template("publications/edit-publication.html"),
+                publication=fetch_publication_by_id(conn, publication_id),
+                linked_phenotypes=tuple(fetch_publication_phenotypes(
+                    conn, publication_id)),
+                publication_id=publication_id)
+
+        form = request.form
+        _pub = update_publications(conn, ({
+            "publication_id": publication_id,
+            "pubmed_id": form.get("pubmed-id") or None,
+            "abstract": form.get("publication-abstract").encode("utf8") or None,
+            "authors": form.get("publication-authors").encode("utf8"),
+            "title":  form.get("publication-title").encode("utf8") or None,
+            "journal": form.get("publication-journal").encode("utf8") or None,
+            "volume": form.get("publication-volume").encode("utf8") or None,
+            "pages": form.get("publication-pages").encode("utf8") or None,
+            "month": (form.get("publication-month") or "").encode("utf8").capitalize() or None,
+            "year": form.get("publication-year").encode("utf8") or None
+        },))
+
+        if not _pub:
+            flash("There was an error updating the publication details.",
+                  "alert-danger")
+            return redirect(url_for(
+                "publications.edit_publication", publication_id=publication_id))
+
+        flash("Successfully updated the publication details.",
+              "alert-success")
+        return redirect_to_next({
+            "uri": "publications.view_publication",
+            "publication_id": publication_id
+        })
+
+
+@pubbp.route("/delete/<int:publication_id>", methods=["GET", "POST"])
+@require_login
+def delete_publication(publication_id: int):
+    """Delete a particular publication."""
+    with database_connection(app.config["SQL_URI"]) as conn:
+        publication = fetch_publication_by_id(conn, publication_id)
+        linked_phenotypes=tuple(fetch_publication_phenotypes(
+            conn, publication_id))
+
+        if not bool(publication):
+            flash("Requested publication was not found!", "alert-warning")
+            return redirect(url_for('publications.index'))
+
+        if len(linked_phenotypes) > 0:
+            flash("Cannot delete publication with linked phenotypes!",
+                  "alert-warning")
+            return redirect(url_for(
+                sui_template("publications.view_publication"),
+                publication_id=publication_id))
+
+        if request.method == "GET":
+            return render_template(
+                sui_template("publications/delete-publication.html"),
+                publication=publication,
+                linked_phenotypes=linked_phenotypes,
+                publication_id=publication_id)
+
+        delete_publications(conn, (publication,))
+        flash("Deleted the publication successfully.", "alert-success")
+        return redirect(url_for("publications.index"))
diff --git a/uploader/route_utils.py b/uploader/route_utils.py
index ce718fb..fa63233 100644
--- a/uploader/route_utils.py
+++ b/uploader/route_utils.py
@@ -1,11 +1,22 @@
 """Generic routing utilities."""
-from flask import flash, url_for, redirect, render_template, current_app as app
+import logging
+from json.decoder import JSONDecodeError
+
+from flask import (flash,
+                   request,
+                   redirect,
+                   render_template,
+                   current_app as app)
 
 from gn_libs.mysqldb import database_connection
 
+from uploader.flask_extensions import url_for
+from uploader.datautils import base64_encode_dict, base64_decode_to_dict
 from uploader.population.models import (populations_by_species,
                                         population_by_species_and_id)
 
+logger = logging.getLogger(__name__)
+
 def generic_select_population(
         # pylint: disable=[too-many-arguments, too-many-positional-arguments]
         species: dict,
@@ -40,3 +51,42 @@ def generic_select_population(
         return redirect(url_for(forward_to,
                                 species_id=species["SpeciesId"],
                                 population_id=population["Id"]))
+
+
+def redirect_to_next(default: dict):
+    """Redirect to the next uri if specified, else redirect to default."""
+    assert "uri" in default, "You must provide at least the 'uri' value."
+    _next = request.args.get("next")
+    if bool(_next):
+        try:
+            next_page = base64_decode_to_dict(_next)
+            _uri = next_page["uri"]
+            next_page.pop("uri")
+            return redirect(url_for(_uri, **next_page))
+        except (TypeError, JSONDecodeError) as _err:
+            logger.debug("We could not decode the next value '%s'",
+                         next_page,
+                         exc_info=True)
+
+    return redirect(url_for(
+        default["uri"],
+        **{key:value for key,value in default.items() if key != "uri"}))
+
+
+def build_next_argument(uri: str, **kwargs) -> str:
+    """Build the `next` URI argument from provided details."""
+    dumps_keywords = (
+        "skipkeys", "ensure_ascii", "check_circular", "allow_nan", "cls",
+        "indent", "separators", "default", "sort_keys")
+    return base64_encode_dict(
+        {
+            "uri": uri,
+            **{
+                key: val for key,val in kwargs.items()
+                if key not in dumps_keywords
+            }
+        },
+        **{
+            key: val for key,val in kwargs.items()
+            if key in dumps_keywords
+        })
diff --git a/uploader/samples/models.py b/uploader/samples/models.py
index b419d61..1e9293f 100644
--- a/uploader/samples/models.py
+++ b/uploader/samples/models.py
@@ -34,8 +34,7 @@ def read_samples_file(filepath, separator: str, firstlineheading: bool, **kwargs
                 else ("Name", "Name2", "Symbol", "Alias")),
             delimiter=separator,
             quotechar=kwargs.get("quotechar", '"'))
-        for row in reader:
-            yield row
+        yield from reader
 
 
 def save_samples_data(conn: mdb.Connection,
diff --git a/uploader/samples/views.py b/uploader/samples/views.py
index c0adb88..93f0c29 100644
--- a/uploader/samples/views.py
+++ b/uploader/samples/views.py
@@ -7,13 +7,15 @@ from pathlib import Path
 from redis import Redis
 from flask import (flash,
                    request,
-                   url_for,
                    redirect,
                    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
 from uploader.ui import make_template_renderer
 from uploader.authorisation import require_login
 from uploader.input_validation import is_integer_input
@@ -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],
@@ -96,7 +98,7 @@ def list_samples(species: dict, population: dict, **kwargs):# pylint: disable=[u
                                activelink="list-samples")
 
 
-def build_sample_upload_job(# pylint: disable=[too-many-arguments]
+def build_sample_upload_job(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
         speciesid: int,
         populationid: int,
         samplesfile: Path,
@@ -147,7 +149,7 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma
                         code=307)
 
     if request.method == "GET" or request.files.get("samples_file") is None:
-        return render_template("samples/upload-samples.html",
+        return render_template(sui_template("samples/upload-samples.html"),
                                species=species,
                                population=population)
 
@@ -159,7 +161,7 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma
               "alert-error")
         return samples_uploads_page
 
-    firstlineheading = (request.form.get("first_line_heading") == "on")
+    firstlineheading = request.form.get("first_line_heading") == "on"
 
     separator = request.form.get("separator", ",")
     if separator == "other":
@@ -172,7 +174,7 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma
 
     redisuri = app.config["REDIS_URL"]
     with Redis.from_url(redisuri, decode_responses=True) as rconn:
-        #TODO: Add a QC step here — what do we check?
+        #T0DO: Add a QC step here — what do we check?
         # 1. Does any sample in the uploaded file exist within the database?
         #    If yes, what is/are its/their species and population?
         # 2. If yes 1. above, provide error with notes on which species and
@@ -214,7 +216,7 @@ def upload_status(species: dict, population: dict, job_id: uuid.UUID, **kwargs):
     if job:
         status = job["status"]
         if status == "success":
-            return render_template("samples/upload-success.html",
+            return render_template(sui_template("samples/upload-success.html"),
                                    job=job,
                                    species=species,
                                    population=population,)
@@ -234,7 +236,7 @@ def upload_status(species: dict, population: dict, job_id: uuid.UUID, **kwargs):
                 return redirect(url_for(
                     "samples.upload_failure", job_id=job_id))
 
-        return render_template("samples/upload-progress.html",
+        return render_template(sui_template("samples/upload-progress.html"),
                                species=species,
                                population=population,
                                job=job) # maybe also handle this?
@@ -251,7 +253,7 @@ def upload_status(species: dict, population: dict, job_id: uuid.UUID, **kwargs):
 @require_login
 @with_population(species_redirect_uri="species.populations.samples.index",
                  redirect_uri="species.populations.samples.select_population")
-def upload_failure(species: dict, population: dict, job_id: uuid.UUID, **kwargs):
+def upload_failure(species: dict, population: dict, job_id: uuid.UUID, **kwargs):# pylint: disable=[unused-argument]
     """Display the errors of the samples upload failure."""
     job = with_redis_connection(lambda rconn: jobs.job(
         rconn, jobs.jobsnamespace(), job_id))
@@ -265,7 +267,7 @@ def upload_failure(species: dict, population: dict, job_id: uuid.UUID, **kwargs)
         if stat.st_size > 0:
             return render_template("worker_failure.html", job_id=job_id)
 
-    return render_template("samples/upload-failure.html",
+    return render_template(sui_template("samples/upload-failure.html"),
                            species=species,
                            population=population,
                            job=job)
diff --git a/uploader/species/models.py b/uploader/species/models.py
index db53d48..acfa51e 100644
--- a/uploader/species/models.py
+++ b/uploader/species/models.py
@@ -92,7 +92,7 @@ def save_species(conn: mdb.Connection,
         }
 
 
-def update_species(# pylint: disable=[too-many-arguments]
+def update_species(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
         conn: mdb.Connection,
         species_id: int,
         common_name: str,
diff --git a/uploader/species/views.py b/uploader/species/views.py
index cea2f68..9b14d01 100644
--- a/uploader/species/views.py
+++ b/uploader/species/views.py
@@ -4,17 +4,21 @@ from pymonad.either import Left, Right, Either
 from gn_libs.mysqldb import database_connection
 from flask import (flash,
                    request,
-                   url_for,
                    redirect,
                    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
 from uploader.ui import make_template_renderer
 from uploader.oauth2.client import oauth2_get, oauth2_post
 from uploader.authorisation import require_login, require_token
 from uploader.datautils import order_by_family, enumerate_sequence
+from uploader.population.models import (populations_by_species,
+                                        population_by_species_and_id)
 
 from .models import (all_species,
                      save_species,
@@ -41,15 +45,26 @@ def list_species():
 @require_login
 def view_species(species_id: int):
     """View details of a particular species and menus to act upon it."""
+    streamlined_ui = request.args.get("streamlined_ui")
     with database_connection(app.config["SQL_URI"]) as conn:
         species = species_by_id(conn, species_id)
         if bool(species):
-            return render_template("species/view-species.html",
-                                   species=species,
-                                   activelink="view-species")
+            population = population_by_species_and_id(
+                conn, species_id, request.args.get("population_id"))
+            if bool(population):
+                return redirect(url_for("species.populations.view_population",
+                                        species_id=species_id,
+                                        population_id=population["Id"]))
+            return render_template(
+                sui_template("species/view-species.html"),
+                species=species,
+                activelink="view-species",
+                populations=populations_by_species(conn, species["SpeciesId"]))
         flash("Could not find a species with the given identifier.",
               "alert-danger")
-        return redirect(url_for("species.view_species"))
+        return redirect(url_for("base.index"
+                                if streamlined_ui
+                                else "species.view_species"))
 
 @speciesbp.route("/create", methods=["GET", "POST"])
 @require_login
diff --git a/uploader/static/css/layout-common.css b/uploader/static/css/layout-common.css
new file mode 100644
index 0000000..36a5735
--- /dev/null
+++ b/uploader/static/css/layout-common.css
@@ -0,0 +1,3 @@
+* {
+    box-sizing: border-box;
+}
diff --git a/uploader/static/css/layout-large.css b/uploader/static/css/layout-large.css
new file mode 100644
index 0000000..8abd2dd
--- /dev/null
+++ b/uploader/static/css/layout-large.css
@@ -0,0 +1,62 @@
+@media screen and (min-width: 20.1in) {
+    body {
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 1em;
+    }
+
+    #header {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 8fr 2fr;
+    }
+
+    #header #header-text {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+
+        /* Content styling */
+        padding-left: 1em;
+    }
+
+    #header #header-nav {
+        /* Place it in the parent element */
+        grid-column-start: 2;
+        grid-column-end: 3;
+    }
+
+    #main {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 1.5em;
+    }
+
+    #main #breadcrumbs {
+        grid-column-start: 1;
+        grid-column-end: 3;
+        padding: 0 3px;
+    }
+
+    #main #main-content {
+        max-width: 950px;
+
+        grid-column-start: 1;
+        grid-column-end: 2;
+    }
+
+    #main #sidebar-content {
+        grid-column-start: 2;
+        grid-column-end: 3;
+        padding: 1em 0 0 0;
+    }
+}
diff --git a/uploader/static/css/layout-medium.css b/uploader/static/css/layout-medium.css
new file mode 100644
index 0000000..2cca711
--- /dev/null
+++ b/uploader/static/css/layout-medium.css
@@ -0,0 +1,64 @@
+@media screen and (width > 8in) and (max-width: 20in) {
+    body {
+        display: grid;
+        grid-template-columns: 65fr 35fr;
+        grid-gap: 1em;
+    }
+
+    #header {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 8fr 2fr;
+    }
+
+    #header #header-text {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+
+        /* Content styling */
+        padding-left: 1em;
+    }
+
+    #header #header-nav {
+        /* Place it in the parent element */
+        grid-column-start: 2;
+        grid-column-end: 3;
+    }
+
+    #main {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 7fr 3fr;
+        grid-gap: 5px;
+    }
+
+    #main #breadcrumbs {
+        grid-column-start: 1;
+        grid-column-end: 3;
+        padding: 0 3px;
+    }
+
+    #main #main-content {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+        grid-gap: 5px;
+
+        /* Define layout for the children elements */
+        max-width: 100%;
+    }
+
+    #main #sidebar-content {
+        grid-column-start: 2;
+        grid-column-end: 3;
+    }
+}
diff --git a/uploader/static/css/layout-small.css b/uploader/static/css/layout-small.css
new file mode 100644
index 0000000..80a3759
--- /dev/null
+++ b/uploader/static/css/layout-small.css
@@ -0,0 +1,60 @@
+@media screen and (max-width: 8in) {
+    body {
+        display: grid;
+        grid-template-columns: 1fr;
+        grid-template-rows: 1fr 2fr 7fr;
+        grid-gap: 1em;
+    }
+
+    #header {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 3;
+
+        /* Define layout for the children elements */
+        display: grid;
+        grid-template-columns: 1fr;
+    }
+
+    #header #header-text {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+
+        /* Content styling */
+        padding-left: 1em;
+    }
+
+    #header #header-nav {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+    }
+
+    #main {
+        /* Place it in the parent element */
+        grid-column-start: 1;
+        grid-column-end: 2;
+        display: grid;
+
+        /* Define layout for the children elements */
+        grid-template-rows: 1.5em 80% 20%;
+        grid-template-columns: 1fr;
+    }
+
+    #main #breadcrumbs {
+        grid-row-start: 1;
+        grid-row-end: 2;
+        
+    }
+
+    #main #main-content {
+        grid-row-start: 2;
+        grid-row-end: 3;
+    }
+
+    #main #sidebar-content {
+        grid-row-start: 3;
+        grid-row-end: 4;
+    }
+}
diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css
new file mode 100644
index 0000000..bdac745
--- /dev/null
+++ b/uploader/static/css/theme.css
@@ -0,0 +1,90 @@
+body {
+    margin: 0.7em;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    font-style: normal;
+    font-size: 20px;
+}
+
+#header {
+    background-color: #336699;
+    color: #FFFFFF;
+    border-radius: 3px;
+    min-height: 30px;
+}
+
+#header #header-nav .nav li a {
+    /* Content styling */
+    color: #FFFFFF;
+    background: #4477AA;
+    border: solid 5px #336699;
+    border-radius: 5px;
+    font-size: 0.7em;
+    text-align: center;
+    padding: 1px 7px;
+}
+
+#main #breadcrumbs {
+    border-radius:3px;
+    text-align: center;
+}
+
+#main #main-content {
+    border-radius: 5px;
+    padding: 0 5px;
+}
+
+#main #sidebar-content {
+    background: #EEEEEE;
+
+    border-radius: 5px;
+    padding: 10px 5px;
+}
+
+#main .row {
+    margin: 0 2px;
+}
+
+
+.heading {
+    border-bottom: solid #EEBB88;
+    text-transform: capitalize;
+}
+
+.subheading {
+    padding: 1em 0 0.1em 0.5em;
+    border-bottom: solid #88BBEE;
+    text-transform: capitalize;
+}
+
+label {
+    text-transform: Capitalize;
+}
+
+input[type="search"] {
+    border-radius: 5px;
+}
+
+.btn {
+    text-transform: Capitalize;
+}
+
+table.dataTable thead th, table.dataTable tfoot th{
+    border-right: 1px solid white;
+    color: white;
+    background-color: #369 !important;
+}
+
+table.dataTable tbody tr.selected td {
+    background-color: #ffee99 !important;
+}
+
+.form-group {
+    margin-bottom: 2em;
+    padding-bottom: 0.2em;
+    border-bottom: solid gray 1px;
+}
+
+
+.breadcrumb-item {
+    text-transform: Capitalize;
+}
diff --git a/uploader/static/js/populations.js b/uploader/static/js/populations.js
index be1231f..111ebb7 100644
--- a/uploader/static/js/populations.js
+++ b/uploader/static/js/populations.js
@@ -13,9 +13,24 @@ $(() => {
                 }
             },
             {
+                searchable: true,
                 data: (apopulation) => {
                     return `${apopulation.FullName} (${apopulation.InbredSetName})`;
                 }
             }
-        ]);
+        ],
+        {
+            select: "single",
+            paging: true,
+            scrollY: 500,
+            deferRender: true,
+            scroller: true,
+            scrollCollapse: true,
+            layout: {
+                topStart: "info",
+                topEnd: "search",
+                bottomStart: "pageLength",
+                bottomEnd: false
+            }
+        });
 });
diff --git a/uploader/static/js/pubmed.js b/uploader/static/js/pubmed.js
index 9afd4c3..f425f49 100644
--- a/uploader/static/js/pubmed.js
+++ b/uploader/static/js/pubmed.js
@@ -22,7 +22,7 @@ var extract_details = (pubmed_id, details) => {
         "journal": details[pubmed_id].fulljournalname,
         "volume": details[pubmed_id].volume,
         "pages": details[pubmed_id].pages,
-        "month": _date.length > 1 ? months[_date[1].toLowerCase()] : "jan",
+        "month": _date.length > 1 ? (months[_date[1].toLowerCase()] || "January") : "January",
         "year": _date[0],
     };
 };
diff --git a/uploader/static/js/species.js b/uploader/static/js/species.js
index 9ea3017..fb0d2d2 100644
--- a/uploader/static/js/species.js
+++ b/uploader/static/js/species.js
@@ -16,5 +16,19 @@ $(() => {
                     return `${aspecies.FullName} (${aspecies.SpeciesName})`;
                 }
             }
-        ]);
+        ],
+        {
+            select: "single",
+            paging: true,
+            scrollY: 500,
+            deferRender: true,
+            scroller: true,
+            scrollCollapse: true,
+            layout: {
+                topStart: "info",
+                topEnd: "search",
+                bottomStart: "pageLength",
+                bottomEnd: false
+            }
+        });
 });
diff --git a/uploader/static/js/upload_samples.js b/uploader/static/js/upload_samples.js
index aed536f..1c25a1d 100644
--- a/uploader/static/js/upload_samples.js
+++ b/uploader/static/js/upload_samples.js
@@ -87,20 +87,20 @@ function display_preview(event) {
     var data_preview_table = document.getElementById("tbl:samples-preview");
     remove_rows(data_preview_table);
 
-    var separator = document.getElementById("select:separator").value;
+    var separator = document.getElementById("select-separator").value;
     if(separator === "other") {
-	separator = document.getElementById("txt:separator").value;
+	separator = document.getElementById("txt-separator").value;
     }
     if(separator == "") {
 	display_error_row(data_preview_table, "Please provide a separator.");
 	return false;
     }
 
-    var delimiter = document.getElementById("txt:delimiter").value;
+    var delimiter = document.getElementById("txt-delimiter").value;
 
-    var firstlineheading = document.getElementById("chk:heading").checked;
+    var firstlineheading = document.getElementById("chk-heading").checked;
 
-    var fileelement = document.getElementById("file:samples");
+    var fileelement = document.getElementById("file-samples");
     var preview_data = JSON.parse(
 	fileelement.getAttribute("data-preview-content") || "[]");
     if(preview_data.length == 0) {
@@ -115,18 +115,18 @@ function display_preview(event) {
 	delimiter));
 }
 
-document.getElementById("chk:heading").addEventListener(
+document.getElementById("chk-heading").addEventListener(
     "change", display_preview);
-document.getElementById("select:separator").addEventListener(
+document.getElementById("select-separator").addEventListener(
     "change", display_preview);
-document.getElementById("txt:separator").addEventListener(
+document.getElementById("txt-separator").addEventListener(
     "keyup", display_preview);
-document.getElementById("txt:delimiter").addEventListener(
+document.getElementById("txt-delimiter").addEventListener(
     "keyup", display_preview);
-document.getElementById("file:samples").addEventListener(
+document.getElementById("file-samples").addEventListener(
     "change", (event) => {
 	read_first_n_lines(event,
-			   document.getElementById("file:samples"),
+			   document.getElementById("file-samples"),
 			   30,
-			   document.getElementById("chk:heading").checked);
+			   document.getElementById("chk-heading").checked);
     });
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/default-success-page.html b/uploader/templates/background-jobs/default-success-page.html
new file mode 100644
index 0000000..5732456
--- /dev/null
+++ b/uploader/templates/background-jobs/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/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/base.html b/uploader/templates/base.html
index 3c0d0d4..d521ccb 100644
--- a/uploader/templates/base.html
+++ b/uploader/templates/base.html
@@ -45,7 +45,7 @@
     <aside id="nav-sidebar">
       <ul class="nav flex-column">
         <li {%if activemenu=="home"%}class="activemenu"{%endif%}>
-          <a href="/" >Home</a></li>
+          <a href="{{url_for('base.index')}}" >Home</a></li>
         <li {%if activemenu=="publications"%}class="activemenu"{%endif%}>
           <a href="{{url_for('publications.index')}}"
              title="View and manage publications.">Publications</a></li>
diff --git a/uploader/templates/cli-output.html b/uploader/templates/cli-output.html
index 64b1a9a..9cff09d 100644
--- a/uploader/templates/cli-output.html
+++ b/uploader/templates/cli-output.html
@@ -1,7 +1,7 @@
 {%macro cli_output(job, stream)%}
 
 <h4 class="subheading">{{stream | upper}} Output</h4>
-<div class="cli-output" style="max-height: 10em; overflow: auto;">
+<div class="cli-output" style="overflow: auto;">
   <pre>{{job.get(stream, "")}}</pre>
 </div>
 
diff --git a/uploader/templates/flash_messages.html b/uploader/templates/flash_messages.html
index b7af178..b42e64e 100644
--- a/uploader/templates/flash_messages.html
+++ b/uploader/templates/flash_messages.html
@@ -1,11 +1,11 @@
 {%macro flash_all_messages()%}
 {%with messages = get_flashed_messages(with_categories=true)%}
 {%if messages:%}
-<ul>
+<div>
   {%for category, message in messages:%}
-  <li class="{{category}}">{{message}}</li>
+  <div class="alert {{category}}">{{message}}</div>
   {%endfor%}
-</ul>
+</div>
 {%endif%}
 {%endwith%}
 {%endmacro%}
@@ -13,13 +13,13 @@
 {%macro flash_messages(filter_class)%}
 {%with messages = get_flashed_messages(with_categories=true)%}
 {%if messages:%}
-<ul>
+<div>
   {%for category, message in messages:%}
   {%if filter_class in category%}
-  <li class="{{category}}">{{message}}</li>
+  <div class="alert {{category}}">{{message}}</div>
   {%endif%}
   {%endfor%}
-</ul>
+</div>
 {%endif%}
 {%endwith%}
 {%endmacro%}
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/macro-forms.html b/uploader/templates/macro-forms.html
new file mode 100644
index 0000000..0ccab32
--- /dev/null
+++ b/uploader/templates/macro-forms.html
@@ -0,0 +1,9 @@
+{%macro add_http_feature_flags()%}
+{%for flag in http_feature_flags():%}
+{%if (request.args.get(flag) or request.form.get(flag) or ""):%}
+<input type="hidden"
+       name="{{flag}}"
+       value="{{(request.args.get(flag) or request.form.get(flag))}}" />
+{%endif%}
+{%endfor%}
+{%endmacro%}
diff --git a/uploader/templates/phenotypes/add-phenotypes-base.html b/uploader/templates/phenotypes/add-phenotypes-base.html
index 01cd0fe..9909c20 100644
--- a/uploader/templates/phenotypes/add-phenotypes-base.html
+++ b/uploader/templates/phenotypes/add-phenotypes-base.html
@@ -139,6 +139,7 @@
               scrollY: 700,
               deferRender: true,
               scroller: true,
+              scrollCollapse: true,
               layout: {
                   topStart: "info",
                   topEnd: "search"
diff --git a/uploader/templates/phenotypes/add-phenotypes-raw-files.html b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
index 1f7ec66..67b56e3 100644
--- a/uploader/templates/phenotypes/add-phenotypes-raw-files.html
+++ b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
@@ -105,115 +105,213 @@
   </div>
 </fieldset>
 
-<fieldset id="fldset-data-files">
+<fieldset id="fldset-files">
   <legend>Data File(s)</legend>
 
-  <div class="form-group 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>You can drop a CSV file that contains the phenotype descriptions here,
-    or you can click the "Browse" button (below and to the right) to select it
-    from your computer.</p>
-  <p>The CSV file must conform to some standards, as documented in the
-    <a href="#docs-file-phenotype-description"
-       title="Documentation of the phenotype data file format.">
-      "Phenotypes Descriptions" documentation</a> section below.</p>')}}
-  {{display_preview_table("tbl-preview-pheno-desc", "phenotype descriptions")}}
-
-
-  <div class="form-group 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>You can drop a CSV file that contains the phenotype data here,
-    or you can click the "Browse" button (below and to the right) to select it
-    from your computer.</p>
-  <p>The CSV file must conform to some standards, as documented in the
-    <a href="#docs-file-phenotype-data"
-       title="Documentation of the phenotype data file format.">
-      "Phenotypes Data" documentation</a> section below.</p>')}}
-  {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}}
-
-  {%if population.Family in families_with_se_and_n%}
-  <div class="form-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>You can drop a CSV file that contains the computed standard-errors data
-    here, or you can click the "Browse" button (below and to the right) to
-    select it from your computer.</p>
-  <p>The CSV file must conform to some standards, as documented in the
-    <a href="#docs-file-phenotype-se"
-       title="Documentation of the phenotype data file format.">
-      "Phenotypes Data" documentation</a> section below.</p>')}}
-  {{display_preview_table("tbl-preview-pheno-se", "standard errors")}}
+  <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>
 
   
-  <div class="form-group 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>You can drop a CSV file that contains the number of samples/individuals
-    used in computation of the standard-errors here, or you can click the
-    "Browse" button (below and to the right) to select it from your computer.
-  </p>
-  <p>The CSV file must conform to some standards, as documented in the
-    <a href="#docs-file-phenotype-n"
-       title="Documentation of the phenotype data file format.">
-      "Phenotypes Data" documentation</a> section below.</p>')}}
-  {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}}
+  {%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%}
@@ -326,15 +424,15 @@
   <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>phenotypes × individuals</em>, e.g.</p>
+  <p>The data is a matrix of <em>samples(or individuals) × phenotypes</em>, e.g.</p>
   <code>
     # num-cases: 2549
     # num-phenos: 13
-    id,IND001,IND002,IND003,IND004,…<br />
-    pheno10001,61.400002,54.099998,483,49.799999,…<br />
-    pheno10002,49,50.099998,403,45.5,…<br />
-    pheno10003,62.5,53.299999,501,62.900002,…<br />
-    pheno10004,53.099998,55.099998,403,NA,…<br />
+    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
diff --git a/uploader/templates/phenotypes/edit-phenotype.html b/uploader/templates/phenotypes/edit-phenotype.html
index 32c903f..115d6af 100644
--- a/uploader/templates/phenotypes/edit-phenotype.html
+++ b/uploader/templates/phenotypes/edit-phenotype.html
@@ -201,130 +201,6 @@
   </form>
 </div>
 
-
-<div class="row">
-  <h3 class="subheading">publication information</h3>
-  <p>Use the form below to update the publication information for this
-    phenotype.</p>
-  <form id="frm-edit-phenotype-pub-data"
-        class="form-horizontal"
-        method="POST"
-        action="#">
-    <div class="form-group">
-      <label for="txt-pubmed-id" class="control-label col-sm-2">Pubmed ID</label>
-      <div class="col-sm-10">
-        <input id="txt-pubmed-id" name="pubmed-id" type="text"
-               class="form-control" />
-        <span class="form-text text-muted">
-          Enter your publication's PubMed ID.</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-authors" class="control-label col-sm-2">Authors</label>
-      <div class="col-sm-10">
-        <input id="txt-publication-authors" name="publication-authors"
-               type="text" class="form-control" />
-        <span class="form-text text-muted">
-          Enter the authors.</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-title" class="control-label col-sm-2">
-        Publication Title</label>
-      <div class="col-sm-10">
-        <input id="txt-publication-title" name="publication-title" type="text"
-               class="form-control" />
-        <span class="form-text text-muted">
-          Enter your publication's title.</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-abstract" class="control-label col-sm-2">
-        Publication Abstract</label>
-      <div class="col-sm-10">
-        <textarea id="txt-publication-abstract" name="publication-abstract"
-                  class="form-control" rows="10"></textarea>
-        <span class="form-text text-muted">
-          Enter the abstract for your publication.</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-journal" class="control-label col-sm-2">Journal</label>
-      <div class="col-sm-10">
-        <input id="txt-publication-journal" name="journal" type="text"
-               class="form-control" />
-        <span class="form-text text-muted">
-          Enter the name of the journal where your work was published.</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-volume" class="control-label col-sm-2">Volume</label>
-      <div class="col-sm-10">
-        <input id="txt-publication-volume" name="publication-volume" type="text"
-               class="form-control" />
-        <span class="form-text text-muted">
-          Enter the volume in the following format &hellip;</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-pages" class="control-label col-sm-2">Pages</label>
-      <div class="col-sm-10">
-        <input id="txt-publication-pages" name="publication-pages" type="text"
-               class="form-control" />
-        <span class="form-text text-muted">
-          Enter the journal volume where your work was published.</span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="select-publication-month" class="control-label col-sm-2">
-        Publication Month</label>
-      <div class="col-sm-10">
-        <select id="select-publication-month" name="publication-month"
-                class="form-control">
-          {%for month in monthnames%}
-          <option value="{{month | lower}}"
-                  {%if current_month | lower == month | lower%}
-                  selected="selected"
-                  {%endif%}>{{month | capitalize}}</option>
-          {%endfor%}
-        </select>
-        <span class="form-text text-muted">
-          Select the month when the work was published.
-          <span class="text-danger">
-            This cannot be before, say 1600 and cannot be in the future!</span></span>
-      </div>
-    </div>
-
-    <div class="form-group">
-      <label for="txt-publication-year" class="control-label col-sm-2">Publication Year</label>
-      <div class="col-sm-10">
-        <input id="txt-publication-year" name="publication-year" type="text"
-               class="form-control" value="{{current_year}}" />
-        <span class="form-text text-muted">
-          Enter the year your work was published.
-          <span class="text-danger">
-            This cannot be before, say 1600 and cannot be in the future!</span>
-        </span>
-      </div>
-    </div>
-    <div class="form-group">
-      <div class="col-sm-offset-2 col-sm-10">
-        <input type="submit"
-               name="submit"
-               class="btn btn-primary not-implemented"
-               value="update publication" />
-      </div>
-    </div>
-  </form>
-</div>
-
 {%endblock%}
 
 {%block sidebarcontents%}
diff --git a/uploader/templates/phenotypes/job-status.html b/uploader/templates/phenotypes/job-status.html
index 12963c1..257f726 100644
--- a/uploader/templates/phenotypes/job-status.html
+++ b/uploader/templates/phenotypes/job-status.html
@@ -105,7 +105,7 @@
         <td>{{error.filename}}</td>
         <td>{{error.rowtitle}}</td>
         <td>{{error.coltitle}}</td>
-        <td>{%if error.cellvalue | length > 25%}
+        <td>{%if error.cellvalue is not none and error.cellvalue | length > 25%}
           {{error.cellvalue[0:24]}}&hellip;
           {%else%}
           {{error.cellvalue}}
diff --git a/uploader/templates/phenotypes/load-phenotypes-success.html b/uploader/templates/phenotypes/load-phenotypes-success.html
index 3baca5b..645be16 100644
--- a/uploader/templates/phenotypes/load-phenotypes-success.html
+++ b/uploader/templates/phenotypes/load-phenotypes-success.html
@@ -28,7 +28,7 @@
   <!-- 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="{{gn2_server_url}}search?species={{species.Name}}&group={{population.Name}}&type=Phenotypes&dataset={{dataset.Name}}&search_terms_or=*%0D%0A&search_terms_and=*%0D%0A&accession_id=None&FormID=searchResult"
+    <a href="{{search_page_uri}}"
        target="_blank">on GeneNetwork2</a>.
     You might need to login to GeneNetwork2 to view specific traits.</p>
 </div>
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/review-job-data.html b/uploader/templates/phenotypes/review-job-data.html
index 1343c19..859df74 100644
--- a/uploader/templates/phenotypes/review-job-data.html
+++ b/uploader/templates/phenotypes/review-job-data.html
@@ -35,6 +35,9 @@
 {%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;
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/phenotypes/sui-view-dataset.html b/uploader/templates/phenotypes/sui-view-dataset.html
new file mode 100644
index 0000000..f858c4e
--- /dev/null
+++ b/uploader/templates/phenotypes/sui-view-dataset.html
@@ -0,0 +1,207 @@
+{%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 lvl4_breadcrumbs%}
+<li {%if activelink=="view-dataset"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.populations.phenotypes.view_dataset',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id)}}">View</a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <p>The basic dataset details are:</p>
+
+  <table class="table">
+    <thead>
+      <tr>
+        <th>Name</th>
+        <th>Full Name</th>
+        <th>Short Name</th>
+      </tr>
+    </thead>
+
+    <tbody>
+      <tr>
+        <td>{{dataset.Name}}</td>
+        <td>{{dataset.FullName}}</td>
+        <td>{{dataset.ShortName}}</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+
+<div class="row">
+  <div class="col">
+    <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             dataset_id=dataset.Id)}}"
+       title="Add a bunch of phenotypes"
+       class="btn btn-primary">Add phenotypes</a>
+  </div>
+
+  <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'])}}"
+          class="d-flex flex-row align-items-center flex-wrap"
+          style="display: inline;">
+      <input type="submit"
+             title="Compute/Recompute the means for all phenotypes."
+             class="btn btn-info"
+             value="(rec/c)ompute means"
+             id="submit-frm-recompute-phenotype-means" />
+    </form>
+  </div>
+
+  <div class="col">
+    <form id="frm-run-qtlreaper"
+          method="POST"
+          action="{{url_for(
+                  'species.populations.phenotypes.rerun_qtlreaper',
+                  species_id=species['SpeciesId'],
+                  population_id=population['Id'],
+                  dataset_id=dataset['Id'])}}"
+          class="d-flex flex-row align-items-center flex-wrap"
+          style="display: inline;">
+      <input type="submit"
+             title="Run/Rerun QTLReaper."
+             class="btn btn-info"
+             value="(re)run QTLReaper"
+             id="submit-frm-rerun-qtlreaper" />
+    </form>
+  </div>
+</div>
+
+<div class="row">
+  <h2>Phenotype Data</h2>
+
+  <p>Click on any of the phenotypes in the table below to view and edit that
+    phenotype's data.</p>
+  <p>Use the search to filter through all the phenotypes and find specific
+    phenotypes of interest.</p>
+</div>
+
+
+<div class="row">
+
+  <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>
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/urls.js"></script>
+
+<script type="text/javascript">
+  $(function() {
+      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-run-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/phenotypes/view-dataset.html b/uploader/templates/phenotypes/view-dataset.html
index 21563d6..c634a48 100644
--- a/uploader/templates/phenotypes/view-dataset.html
+++ b/uploader/templates/phenotypes/view-dataset.html
@@ -46,12 +46,50 @@
 </div>
 
 <div class="row">
-  <p><a href="{{url_for('species.populations.phenotypes.add_phenotypes',
-              species_id=species.SpeciesId,
-              population_id=population.Id,
-              dataset_id=dataset.Id)}}"
-        title="Add a bunch of phenotypes"
-        class="btn btn-primary">Add phenotypes</a></p>
+  <div class="col">
+    <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+             species_id=species.SpeciesId,
+             population_id=population.Id,
+             dataset_id=dataset.Id)}}"
+       title="Add a bunch of phenotypes"
+       class="btn btn-primary">Add phenotypes</a>
+  </div>
+
+  <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'])}}"
+          class="d-flex flex-row align-items-center flex-wrap"
+          style="display: inline;">
+      <input type="submit"
+             title="Compute/Recompute the means for all phenotypes."
+             class="btn btn-info"
+             value="(rec/c)ompute means"
+             id="submit-frm-recompute-phenotype-means" />
+    </form>
+  </div>
+
+  <div class="col">
+    <form id="frm-run-qtlreaper"
+          method="POST"
+          action="{{url_for(
+                  'species.populations.phenotypes.rerun_qtlreaper',
+                  species_id=species['SpeciesId'],
+                  population_id=population['Id'],
+                  dataset_id=dataset['Id'])}}"
+          class="d-flex flex-row align-items-center flex-wrap"
+          style="display: inline;">
+      <input type="submit"
+             title="Run/Rerun QTLReaper."
+             class="btn btn-info"
+             value="(re)run QTLReaper"
+             id="submit-frm-rerun-qtlreaper" />
+    </form>
+  </div>
 </div>
 
 <div class="row">
@@ -133,100 +171,6 @@
           {
               select: "multi+shift",
               layout: {
-                  top2: {
-                      buttons: [
-                          {
-                              extend: "selectAll",
-                              className: "btn btn-info",
-                              titleAttr: "Click to select ALL records in the table."
-                          },
-                          {
-                              extend: "selectNone",
-                              className: "btn btn-info",
-                              titleAttr: "Click to deselect ANY selected record(s) in the table."
-                          },
-                          {
-                              text: "Bulk Edit (Download Data)",
-                              className: "btn btn-info btn-bulk-edit",
-                              titleAttr: "Click to download data for editing.",
-                              action: (event, dt, node, config) => {
-                                  var phenoids = [];
-                                  var selected = dt.rows({selected: true, page: "all"}).data();
-                                  for(var idx = 0; idx < selected.length; idx++) {
-                                      phenoids.push({
-                                          phenotype_id: selected[idx].Id,
-                                          xref_id: selected[idx].xref_id
-                                      });
-                                  }
-                                  if(phenoids.length == 0) {
-                                      alert("No record selected. Nothing to do!");
-                                      return false;
-                                  }
-
-                                  $(".btn-bulk-edit").prop("disabled", true);
-                                  $(".btn-bulk-edit").addClass("d-none");
-                                  var spinner = $(
-                                      "<div id='bulk-edit-spinner' class='spinner-grow text-info'>");
-                                  spinner_content = $(
-                                      "<span class='visually-hidden'>");
-                                  spinner_content.html(
-                                      "Downloading data &hellip;");
-                                  spinner.append(spinner_content)
-                                  $(".btn-bulk-edit").parent().append(
-                                      spinner);
-
-                                  $.ajax(
-                                      (`/species/${species_id}/populations/` +
-                                       `${population_id}/phenotypes/datasets/` +
-                                       `${dataset_id}/edit-download`),
-                                      {
-                                          method: "POST",
-                                          data: JSON.stringify(phenoids),
-                                          xhrFields: {
-                                              responseType: "blob"
-                                          },
-                                          success: (data, textStatus, jqXHR) => {
-                                              var link = document.createElement("a");
-                                              uri = window.URL.createObjectURL(data);
-                                              link.href = uri;
-                                              link.download = `${dataset_name}_data.tsv`;
-
-                                              document.body.appendChild(link);
-                                              link.click();
-                                              window.URL.revokeObjectURL(uri);
-                                              link.remove();
-                                          },
-                                          error: (jQXHR, textStatus, errorThrown) => {
-                                              console.log("Experienced an error: ", textStatus);
-                                              console.log("The ERROR: ", errorThrown);
-                                          },
-                                          complete: (jqXHR, textStatus) => {
-                                              $("#bulk-edit-spinner").remove();
-                                              $(".btn-bulk-edit").removeClass(
-                                                  "d-none");
-                                              $(".btn-bulk-edit").prop(
-                                                  "disabled", false);
-                                          },
-                                          contentType: "application/json"
-                                      });
-                              }
-                          },
-                          {
-                              text: "Bulk Edit (Upload Data)",
-                              className: "btn btn-info btn-bulk-edit",
-                              titleAttr: "Click to upload edited data you got by clicking the `Bulk Edit (Download Data)` button.",
-                              action: (event, dt, node, config) => {
-                                  window.location.assign(
-                                      `${window.location.protocol}//` +
-                                          `${window.location.host}` +
-                                          `/species/${species_id}` +
-                                          `/populations/${population_id}` +
-                                          `/phenotypes/datasets/${dataset_id}` +
-                                          `/edit-upload`)
-                              }
-                          }
-                      ]
-                  },
                   top1Start: {
                       pageLength: {
                           text: "Show _MENU_ of _TOTAL_"
@@ -239,6 +183,27 @@
                   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-run-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/phenotypes/view-phenotype.html b/uploader/templates/phenotypes/view-phenotype.html
index 21ac501..75e3c1e 100644
--- a/uploader/templates/phenotypes/view-phenotype.html
+++ b/uploader/templates/phenotypes/view-phenotype.html
@@ -24,8 +24,10 @@
 {{flash_all_messages()}}
 
 <div class="row">
-  <div class="panel panel-default">
-    <div class="panel-heading"><strong>Basic Phenotype Details</strong></div>
+  <div class="card">
+    <div class="card-header">
+      <h5 class="card-title">Basic Phenotype Details</h5>
+    </div>
 
     <table class="table">
       <tbody>
@@ -41,24 +43,46 @@
           <td><strong>Units</strong></td>
           <td>{{phenotype.Units}}</td>
         </tr>
-        {%for key,value in publish_data.items()%}
-        <tr>
-          <td><strong>{{key}}</strong></td>
-          <td>{{value}}</td>
-        </tr>
-        {%else%}
-        <tr>
-          <td colspan="2" class="text-muted">
-            <span class="glyphicon glyphicon-exclamation-sign"></span>
-            No publication data found.
-          </td>
-        </tr>
-        {%endfor%}
       </tbody>
     </table>
   </div>
 </div>
 
+<div class="row" style="margin-top:5px;">
+  <div class="card">
+    <div class="card-header">
+      <h5 class="card-title">Publication Details</h5>
+    </div>
+
+    <div class="card-body">
+      <table class="table">
+        <tbody>
+          <tr>
+            {%for key in ("PubMed_ID", "Authors", "Title", "Journal"):%}
+          <tr>
+            <td><strong>{{key}}</strong></td>
+            <td>{{publication.get(key, "")}}</td>
+          </tr>
+          {%else%}
+          <tr>
+            <td colspan="2" class="text-muted">
+              <span class="glyphicon glyphicon-exclamation-sign"></span>
+              No publication data found.
+            </td>
+          </tr>
+          {%endfor%}
+          </tr>
+        </tbody>
+      </table>
+      <div style="text-align: right;">
+        <a href="{{url_for('publications.edit_publication', publication_id=publication.Id, next=next)}}"
+           class="btn btn-info">edit</a>
+        <a href="#" class="btn btn-danger not-implemented">change</a>
+      </div>
+    </div>
+  </div>
+</div>
+
 {%if "group:resource:edit-resource" in privileges
 or "group:resource:delete-resource" in privileges%}
 <div class="row">
diff --git a/uploader/templates/platforms/sui-base.html b/uploader/templates/platforms/sui-base.html
new file mode 100644
index 0000000..136b956
--- /dev/null
+++ b/uploader/templates/platforms/sui-base.html
@@ -0,0 +1,17 @@
+{%extends "species/sui-base.html"%}
+{%from "species/macro-display-species-card.html" import display_sui_species_card%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.platforms.list_platforms',
+           species_id=species['SpeciesId'])}}">
+    Platforms
+  </a>
+</li>
+{%endblock%}
+
+
+{%block sidebarcontents%}
+{{display_sui_species_card(species)}}
+{%endblock%}
diff --git a/uploader/templates/platforms/sui-create-platform.html b/uploader/templates/platforms/sui-create-platform.html
new file mode 100644
index 0000000..25977a7
--- /dev/null
+++ b/uploader/templates/platforms/sui-create-platform.html
@@ -0,0 +1,116 @@
+{%extends "platforms/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Platforms &mdash; Create Platforms{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.platforms.create_platform',
+           species_id=species['SpeciesId'])}}">
+    Create
+  </a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <h2>Create New Platform</h2>
+
+  <p>You can create a new genetic sequencing platform below.</p>
+</div>
+
+<div class="row">
+  <form id="frm-create-platform"
+        method="POST"
+        action="{{url_for('species.platforms.create_platform',
+                species_id=species.SpeciesId)}}">
+
+    <div class="form-group">
+      <label for="txt-geo-platform" class="form-label">GEO Platform</label>
+      <input type="text"
+             id="txt-geo-platform"
+             name="geo-platform"
+             required="required"
+             class="form-control"  />
+      <small class="form-text text-muted">
+        <p>This is the platform's
+          <a href="https://www.ncbi.nlm.nih.gov/geo/browse/?view=platforms&tax={{species.TaxonomyId}}"
+             title="Platforms for '{{species.FullName}}' on NCBI">
+            accession value on NCBI</a>. If you do not know the value, click the
+          link and search on NCBI for species '{{species.FullName}}'.</p></small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-platform-name" class="form-label">Platform Name</label>
+      <input type="text"
+             id="txt-platform-name"
+             name="platform-name"
+             required="required"
+             class="form-control" />
+      <small class="form-text text-muted">
+        <p>This is name of the genetic sequencing platform.</p></small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-platform-shortname" class="form-label">
+        Platform Short Name</label>
+      <input type="text"
+             id="txt-platform-shortname"
+             name="platform-shortname"
+             required="required"
+             class="form-control" />
+      <small class="form-text text-muted">
+        <p>Use the following conventions for this field:
+          <ol>
+            <li>Start with a 4-letter vendor code, e.g. "Affy" for "Affymetrix", "Illu" for "Illumina", etc.</li>
+            <li>Append an underscore to the 4-letter vendor code</li>
+            <li>Use the name of the array given by the vendor, e.g.  U74AV2, MOE430A, etc.</li>
+          </ol>
+        </p>
+      </small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-platform-title" class="form-label">Platform Title</label>
+      <input type="text"
+             id="txt-platform-title"
+             name="platform-title"
+             required="required"
+             class="form-control"  />
+      <small class="form-text text-muted">
+        <p>The full platform title. Sometimes, this is the same as the Platform
+          Name above.</p></small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-go-tree-value" class="form-label">GO Tree Value</label>
+      <input type="text"
+             id="txt-go-tree-value"
+             name="go-tree-value"
+             class="form-control"  />
+      <small class="form-text text-muted">
+        <p>This is a Chip identification value useful for analysis with the
+          <strong>
+            <a href="https://www.geneweaver.org/"
+               title="Go to the GeneWeaver site."
+               target="_blank">GeneWeaver</a></strong>
+          and
+          <strong>
+            <a href="https://www.webgestalt.org/"
+               title="Go to the WEB-based GEne SeT AnaLysis Toolkit site."
+               target="_blank">WebGestalt</a></strong>
+          tools.<br />
+        This can be left blank for custom platforms.</p></small>
+    </div>
+
+    <div class="form-group">
+      <input type="submit"
+             value="create new platform"
+             class="btn btn-primary"  />
+    </div>
+  </form>
+</div>
+{%endblock%}
diff --git a/uploader/templates/platforms/sui-list-platforms.html b/uploader/templates/platforms/sui-list-platforms.html
new file mode 100644
index 0000000..f4c17e8
--- /dev/null
+++ b/uploader/templates/platforms/sui-list-platforms.html
@@ -0,0 +1,88 @@
+{%extends "platforms/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Platforms &mdash; List Platforms{%endblock%}
+
+{%block pagetitle%}Platforms &mdash; List Platforms{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <p>View the list of the genetic sequencing platforms that are currently
+    supported by GeneNetwork.</p>
+  <p>If you cannot find the platform you wish to use, you can add it by clicking
+    the "New Platform" button below.</p>
+  <p><a href="{{url_for('species.platforms.create_platform',
+              species_id=species.SpeciesId)}}"
+        title="Create a new genetic sequencing platform for species {{species.FullName}}"
+        class="btn btn-primary">Create Platform</a></p>
+</div>
+
+<div class="row">
+  <h2>Supported Platforms</h2>
+  {%if platforms is defined and platforms | length > 0%}
+  <p>There are {{total_platforms}} platforms supported by GeneNetwork</p>
+
+  <div class="row">
+    <div class="col-md-2" style="text-align: start;">
+      {%if start_from > 0%}
+      <a href="{{url_for('species.platforms.list_platforms',
+               species_id=species.SpeciesId,
+               start_from=start_from-count,
+               count=count)}}">
+        <span class="glyphicon glyphicon-backward"></span>
+        Previous
+      </a>
+      {%endif%}
+    </div>
+    <div class="col-md-8" style="text-align: center;">
+      Displaying platforms {{start_from+1}} to {{start_from+count if start_from+count < total_platforms else total_platforms}} of
+      {{total_platforms}}
+    </div>
+    <div class="col-md-2" style="text-align: end;">
+      {%if start_from + count < total_platforms%}
+      <a href="{{url_for('species.platforms.list_platforms',
+               species_id=species.SpeciesId,
+               start_from=start_from+count,
+               count=count)}}">
+        Next
+        <span class="glyphicon glyphicon-forward"></span>
+      </a>
+      {%endif%}
+    </div>
+  </div>
+
+  <table class="table">
+    <thead>
+      <tr>
+        <th></th>
+        <th>Platform Name</th>
+        <th><a href="https://www.ncbi.nlm.nih.gov/geo/browse/?view=platforms&tax={{species.TaxonomyId}}"
+               title="Gene Expression Omnibus: Platforms section"
+               target="_blank">GEO Platform</a></th>
+        <th>Title</th>
+      </tr>
+    </thead>
+
+    <tbody>
+      {%for platform in platforms%}
+      <tr>
+        <td>{{platform.sequence_number}}</td>
+        <td>{{platform.GeneChipName}}</td>
+        <td><a href="https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc={{platform.GeoPlatform}}"
+               title="View platform on the Gene Expression Omnibus"
+               target="_blank">{{platform.GeoPlatform}}</a></td>
+        <td>{{platform.Title}}</td>
+      </tr>
+      {%endfor%}
+    </tbody>
+  </table>
+  {%else%}
+  <p class="text-warning">
+    <span class="glyphicon glyphicon-exclamation-sign"></span>
+    There are no platforms supported at this time!</p>
+  {%endif%}
+</div>
+{%endblock%}
diff --git a/uploader/templates/populations/create-population.html b/uploader/templates/populations/create-population.html
index c0c4f45..007b6bf 100644
--- a/uploader/templates/populations/create-population.html
+++ b/uploader/templates/populations/create-population.html
@@ -154,24 +154,35 @@
          {%else%}
          class="form-group"
          {%endif%}>
-      <label for="select-population-family" class="form-label">Family</label>
-      <select id="select-population-family"
-              name="population_family"
-              class="form-control"
-              required="required">
-        <option value="">Please select a family</option>
+      <label for="txt-population-family" class="form-label">Family</label>
+      <input type="text"
+             id="txt-population-family"
+             name="population_family"
+             class="form-control"
+             list="families-list" />
+      <datalist id="families-list">
         {%for family in families%}
-        <option value="{{family}}"
-                {%if error_values.population_family == family%}
-                selected="selected"
-                {%endif%}>{{family}}</option>
+        <option value="{{family}}">{{family}}</option>
         {%endfor%}
-      </select>
+      </datalist>
       <small class="form-text text-muted">
         <p>
-          This is a rough grouping of the populations in GeneNetwork into lists
-          of common types of populations.
-        </p>
+          This is <strong>optional</strong> metadata. It is used to group
+          populations into "families" for presentation in the menus.
+          {%if families | length > 0%}
+          Examples of currently existing families are:
+          <ul>
+            {%for family in families[0:7]%}
+            <li>{{family}}</li>
+            {%endfor%}
+            <li>etc.</li>
+          </ul>
+          {%endif%}
+
+          You can
+          {%if families|length>0%} select from existing families, or {%endif%}
+          create a new family by typing in the input box above. You can also
+          leave the family blank.</p>
       </small>
     </div>
 
diff --git a/uploader/templates/populations/list-populations.html b/uploader/templates/populations/list-populations.html
index f780e94..a092e34 100644
--- a/uploader/templates/populations/list-populations.html
+++ b/uploader/templates/populations/list-populations.html
@@ -54,7 +54,7 @@
         <th></th>
         <th>Name</th>
         <th>Full Name</th>
-        <th>Description</th>
+        <th>Information</th>
       </tr>
     </thead>
 
@@ -71,7 +71,10 @@
           </a>
         </td>
         <td>{{population.FullName}}</td>
-        <td>{{population.Description}}</td>
+        <td><a href="https://info.genenetwork.org/species/source.php?SpeciesName={{species.Name}}&InbredSetName={{population.Name}}"
+               title="Link to detailed information on this population."
+               class="btn btn-info"
+               target="_blank">info</a></td>
       </tr>
       {%else%}
       <tr>
diff --git a/uploader/templates/populations/macro-display-population-card.html b/uploader/templates/populations/macro-display-population-card.html
index 16b477f..6b5f1e0 100644
--- a/uploader/templates/populations/macro-display-population-card.html
+++ b/uploader/templates/populations/macro-display-population-card.html
@@ -1,4 +1,4 @@
-{%from "species/macro-display-species-card.html" import display_species_card%}
+{%from "species/macro-display-species-card.html" import display_species_card,display_sui_species_card%}
 
 {%macro display_population_card(species, population)%}
 {{display_species_card(species)}}
@@ -39,3 +39,40 @@
   </div>
 </div>
 {%endmacro%}
+
+
+{%macro display_sui_population_card(species, population)%}
+{{display_sui_species_card(species)}}
+
+<div class="row">
+  <table class="table">
+    <caption>Current population</caption>
+    <tbody>
+      <tr>
+        <th>Name</th>
+        <td>{{population.Name}}</td>
+      </tr>
+
+      <tr>
+        <th>Full Name</th>
+        <td>{{population.FullName}}</td>
+      </tr>
+
+      <tr>
+        <th>Code</th>
+        <td>{{population.InbredSetCode}}</td>
+      </tr>
+
+      <tr>
+        <th>Genetic Type</th>
+        <td>{{population.GeneticType}}</td>
+      </tr>
+
+      <tr>
+        <th>Family</th>
+        <td>{{population.Family}}</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+{%endmacro%}
diff --git a/uploader/templates/populations/sui-base.html b/uploader/templates/populations/sui-base.html
new file mode 100644
index 0000000..0ca5c59
--- /dev/null
+++ b/uploader/templates/populations/sui-base.html
@@ -0,0 +1,12 @@
+{%extends "species/sui-base.html"%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.populations.view_population',
+           species_id=species['SpeciesId'],
+           population_id=population['Id'])}}">
+    {{population["Name"]}}
+  </a>
+</li>
+{%endblock%}
diff --git a/uploader/templates/populations/sui-view-population.html b/uploader/templates/populations/sui-view-population.html
new file mode 100644
index 0000000..a35eac6
--- /dev/null
+++ b/uploader/templates/populations/sui-view-population.html
@@ -0,0 +1,127 @@
+{%extends "populations/sui-base.html"%}
+{%from "macro-step-indicator.html" import step_indicator%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
+
+{%block contents%}
+<div class="row">
+  <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2>
+</div>
+
+<div class="row">
+  <ul class="nav nav-tabs" id="population-actions">
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="samples-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#samples-content"
+              type="button"
+              role="tab"
+              aria-controls="samples-content"
+              aria-selected="true">Samples</button></li>
+    <li class="nav-item presentation">
+      <button class="nav-link active"
+              id="phenotypes-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#phenotypes-content"
+              type="button"
+              role="tab"
+              aria-controls="phenotypes-content"
+              aria-selected="false">Phenotypes</button></li>
+    {%if view_under_construction%}
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="genotypes-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#genotypes-content"
+              type="button"
+              role="tab"
+              aria-controls="genotypes-content"
+              aria-selected="false">Genotypes</button></li>
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="expression-data-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#expression-data-content"
+              type="button"
+              role="tab"
+              aria-controls="expression-data-content"
+              aria-selected="false">Expression-Data</button></li>
+    {%endif%}
+  </ul>
+</div>
+
+<div class="row">
+  <div class="tab-content" id="populations-tabs-content">
+    <div class="tab-pane fade"
+         id="samples-content"
+         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).
+      </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>
+      <a href="{{url_for('species.populations.samples.list_samples',
+               species_id=species.SpeciesId,
+               population_id=population.Id)}}"
+         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">
+
+      <div class="row" style="margin-top: 1em;">
+        <h3> Phenotypes in  Population "{{population.FullName}} ({{population.Name}})"</h3>
+
+        <p>To view existing phenotype traits, or upload new ones, click the button below:</p>
+
+        <div class="row">
+          <div class="col">
+            <a href="{{url_for(
+                     'species.populations.phenotypes.list_datasets',
+                     species_id=species.SpeciesId,
+                     population_id=population.Id)}}"
+               title="View and upload phenotype traits"
+               class="btn btn-primary">Phenotypes</a>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="tab-pane fade"
+         id="genotypes-content"
+         role="tabpanel"
+         aria-labelledby="genotypes-content-tab">
+      <p>This allows you to upload the data that concerns your genotypes.</p>
+      <p>Any samples/individuals/cases/strains that do not already exist in the
+        system will be added. This does not delete any existing data.</p>
+      <a href="{{url_for('species.populations.genotypes.list_genotypes',
+               species_id=species.SpeciesId,
+               population_id=population.Id)}}"
+         title="Upload genotype information for the '{{population.FullName}}' population of the '{{species.FullName}}' species."
+         class="btn btn-primary">upload genotypes</a>
+    </div>
+    <div class="tab-pane fade" id="expression-data-content" role="tabpanel" aria-labelledby="expression-data-content-tab">
+      <p>Upload expression data (mRNA data) for this population.</p>
+      <a href="#" title="" class="btn btn-primary">upload genotypes</a>
+    </div>
+  </div>
+</div>
+{%endblock%}
+
+{%block sidebarcontents%}
+<div class="row">
+  <p>Each tab presents a feature that's available at the population level.
+    Select the tab that allows you to continue with your task.</p>
+</div>
+{{display_sui_population_card(species, population)}}
+{%endblock%}
+
+
+
+
+{%block javascript%}
+{%endblock%}
diff --git a/uploader/templates/populations/view-population.html b/uploader/templates/populations/view-population.html
index b23caeb..3b9661b 100644
--- a/uploader/templates/populations/view-population.html
+++ b/uploader/templates/populations/view-population.html
@@ -42,8 +42,10 @@
     <dt>Family</dt>
     <dd>{{population.Family}}</dd>
 
-    <dt>Description</dt>
-    <dd><pre>{{population.Description or "-"}}</pre></dd>
+    <dt>Information</dt>
+    <dd><a href="https://info.genenetwork.org/species/source.php?SpeciesName={{species.Name}}&InbredSetName={{population.Name}}"
+           title="Link to detailed information on this population."
+           target="_blank">Population Information</a></dd>
   </dl>
 </div>
 
diff --git a/uploader/templates/publications/delete-publication-success.html b/uploader/templates/publications/delete-publication-success.html
new file mode 100644
index 0000000..53a44ec
--- /dev/null
+++ b/uploader/templates/publications/delete-publication-success.html
@@ -0,0 +1,18 @@
+{%extends "publications/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}View Publication{%endblock%}
+
+{%block pagetitle%}View Publication{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript">
+  $(function() {});
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/delete-publication.html b/uploader/templates/publications/delete-publication.html
new file mode 100644
index 0000000..0ac93ec
--- /dev/null
+++ b/uploader/templates/publications/delete-publication.html
@@ -0,0 +1,88 @@
+{%extends "publications/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}View Publication{%endblock%}
+
+{%block pagetitle%}View Publication{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+<div class="row">
+  <p>You are about to delete the publication with the following details:</p>
+</div>
+
+<div class="row">
+  <table class="table">
+    <tr>
+      <th>Linked Phenotypes</th>
+      <td>{{linked_phenotypes | count}}</td>
+    </tr>
+    <tr>
+      <th>PubMed</th>
+      <td>
+        {%if publication.PubMed_ID%}
+        <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/"
+           target="_blank">{{publication.PubMed_ID}}</a>
+        {%else%}
+        —
+        {%endif%}
+      </td>
+    </tr>
+    <tr>
+      <th>Title</th>
+      <td>{{publication.Title or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Authors</th>
+      <td>{{publication.Authors or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Journal</th>
+      <td>{{publication.Journal or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Published</th>
+      <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Volume</th>
+      <td>{{publication.Volume or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Pages</th>
+      <td>{{publication.Pages or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Abstract</th>
+      <td>
+        {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%}
+        <p>{{line}}</p>
+        {%endfor%}
+      </td>
+    </tr>
+  </table>
+</div>
+
+<div class="row">
+  <p>If you are sure that is what you want, click the button below to delete the
+    publication</p>
+  <p class="form-text text-small">
+    <small>You will not be able to recover the data if you click
+      delete below.</small></p>
+
+  <form action="{{url_for('publications.delete_publication', publication_id=publication_id)}}"
+        method="POST">
+    <div class="form-group">
+      <input type="submit" value="delete" class="btn btn-danger" />
+    </div>
+  </form>
+</div>
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript">
+  $(function() {});
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/edit-publication.html b/uploader/templates/publications/edit-publication.html
new file mode 100644
index 0000000..97fa134
--- /dev/null
+++ b/uploader/templates/publications/edit-publication.html
@@ -0,0 +1,196 @@
+{%extends "publications/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}View Publication{%endblock%}
+
+{%block pagetitle%}View Publication{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <form id="frm-create-publication"
+        method="POST"
+        action="{{url_for('publications.edit_publication',
+                publication_id=publication_id,
+                next=request.args.get('next', ''))}}"
+        class="form-horizontal">
+
+    <div class="row mb-3">
+      <label for="txt-pubmed-id" class="col-sm-2 col-form-label">
+        PubMed ID</label>
+      <div class="col-sm-10">
+        <div class="input-group">
+          <input type="text"
+                 id="txt-pubmed-id"
+                 name="pubmed-id"
+                 value="{{publication.PubMed_ID or ''}}"
+                 class="form-control" />
+          <div class="input-group-text">
+            <button class="btn btn-outline-primary"
+                    id="btn-search-pubmed-id">search</button>
+          </div>
+        </div>
+        <span id="search-pubmed-id-error"
+              class="form-text text-muted text-danger visually-hidden">
+        </span>
+        <span class="form-text text-muted">This is the publication's ID on
+          <a href="https://pubmed.ncbi.nlm.nih.gov/"
+             title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a>
+        </span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-title" class="col-sm-2 col-form-label">
+        Title</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-title"
+               name="publication-title"
+               value="{{publication.Title}}"
+               class="form-control" />
+        <span class="form-text text-muted">Provide the publication's title here.</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-authors" class="col-sm-2 col-form-label">
+        Authors</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-authors"
+               name="publication-authors"
+               value="{{publication.Authors}}"
+               required="required"
+               class="form-control" />
+        <span class="form-text text-muted">
+          A publication <strong>MUST</strong> have an author. You <em>must</em>
+          provide a value for the authors field.
+        </span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-journal" class="col-sm-2 col-form-label">
+        Journal</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-journal"
+               name="publication-journal"
+               value="{{publication.Journal}}"
+               class="form-control" />
+        <span class="form-text text-muted">Provide the name journal where the
+          publication was done, here.</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="select-publication-month"
+             class="col-sm-2 col-form-label">
+        Month</label>
+      <div class="col-sm-4">
+        <select class="form-control"
+                id="select-publication-month"
+                name="publication-month">
+          <option value="">Select a month</option>
+          {%for month in ("january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"):%}
+          <option value="{{month}}"
+                  {%if publication.Month | lower == month %}
+                  selected="selected"
+                  {%endif%}>
+            {{month | title}}
+          </option>
+          {%endfor%}
+        </select>
+        <span class="form-text text-muted">Month of publication</span>
+      </div>
+
+      <label for="txt-publication-year"
+             class="col-sm-2 col-form-label">
+        Year</label>
+      <div class="col-sm-4">
+        <input type="number"
+               id="txt-publication-year"
+               name="publication-year"
+               value="{{publication.Year}}"
+               class="form-control"
+               min="1960" />
+        <span class="form-text text-muted">Year of publication</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-volume"
+             class="col-sm-2 col-form-label">
+        Volume</label>
+      <div class="col-sm-4">
+        <input type="text"
+               id="txt-publication-volume"
+               name="publication-volume"
+               value="{{publication.Volume}}"
+               class="form-control">
+        <span class="form-text text-muted">Journal volume</span>
+      </div>
+
+      <label for="txt-publication-pages"
+             class="col-sm-2 col-form-label">
+        Pages</label>
+      <div class="col-sm-4">
+        <input type="text"
+               id="txt-publication-pages"
+               name="publication-pages"
+               value="{{publication.Pages}}"
+               class="form-control" />
+        <span class="form-text text-muted">Journal pages for the publication</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label>
+      <div class="col-sm-10">
+        <textarea id="txt-publication-abstract"
+                  name="publication-abstract"
+                  class="form-control"
+                  rows="7">{{publication.Abstract or ""}}</textarea>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <div class="col-sm-2"></div>
+      <div class="col-sm-8">
+        <input type="submit" class="btn btn-primary" value="Save" />
+        <input type="reset" class="btn btn-danger" />
+      </div>
+    </div>
+
+</form>
+</div>
+
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/pubmed.js"></script>
+<script type="text/javascript">
+  $(function() {
+      $("#btn-search-pubmed-id").on("click", (event) => {
+          event.preventDefault();
+          var search_button = event.target;
+          var pubmed_id = $("#txt-pubmed-id").val().trim();
+          remove_class($("#txt-pubmed-id").parent(), "has-error");
+          if(pubmed_id == "") {
+              add_class($("#txt-pubmed-id").parent(), "has-error");
+              return false;
+          }
+
+          search_button.disabled = true;
+          // Fetch publication details
+          fetch_publication_details(pubmed_id,
+                                    [() => {search_button.disabled = false;}]);
+          return false;
+      });
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/index.html b/uploader/templates/publications/index.html
index f846d54..369812b 100644
--- a/uploader/templates/publications/index.html
+++ b/uploader/templates/publications/index.html
@@ -41,6 +41,7 @@
           [
               {data: "index"},
               {
+                  searchable: true,
                   data: (pub) => {
                   if(pub.PubMed_ID) {
                       return `<a href="https://pubmed.ncbi.nlm.nih.gov/` +
@@ -52,6 +53,7 @@
                   }
               },
               {
+                  searchable: true,
                   data: (pub) => {
                   var title = "⸻";
                   if(pub.Title) {
@@ -64,6 +66,7 @@
                   }
               },
               {
+                  searchable: true,
                   data: (pub) => {
                   authors = pub.Authors.split(",").map(
                       (item) => {return item.trim();});
@@ -75,16 +78,21 @@
               }
           ],
           {
+              serverSide: true,
               ajax: {
                   url: "/publications/list",
                   dataSrc: "publications"
               },
               scrollY: 700,
-              paging: false,
+              scroller: true,
+              scrollCollapse: true,
+              paging: true,
               deferRender: true,
               layout: {
                   topStart: "info",
-                  topEnd: "search"
+                  topEnd: "search",
+                  bottomStart: "pageLength",
+                  bottomEnd: false
               }
           });
   });
diff --git a/uploader/templates/publications/sui-base.html b/uploader/templates/publications/sui-base.html
new file mode 100644
index 0000000..64e41ef
--- /dev/null
+++ b/uploader/templates/publications/sui-base.html
@@ -0,0 +1,9 @@
+{%extends "sui-base.html"%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.index')}}"
+     title="Manage publications">Publications</a>
+</li>
+{%endblock%}
diff --git a/uploader/templates/publications/sui-create-publication.html b/uploader/templates/publications/sui-create-publication.html
new file mode 100644
index 0000000..81edca6
--- /dev/null
+++ b/uploader/templates/publications/sui-create-publication.html
@@ -0,0 +1,200 @@
+{%extends "publications/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}View Publication{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.create_publication', **get_args)}}"
+     title="Manage publications">create publication</a>
+</li>
+{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <form id="frm-create-publication"
+        method="POST"
+        action="{{url_for('publications.create_publication', **get_args)}}"
+        class="form-horizontal">
+
+    <div class="row mb-3">
+      <label for="txt-pubmed-id" class="col-sm-2 col-form-label">
+        PubMed ID</label>
+      <div class="col-sm-10">
+        <div class="input-group">
+          <input type="text"
+                 id="txt-pubmed-id"
+                 name="pubmed-id"
+                 class="form-control"/>
+          <div class="input-group-text">
+            <button class="btn btn-outline-primary"
+                    id="btn-search-pubmed-id">search</button>
+          </div>
+        </div>
+        <span id="search-pubmed-id-error"
+              class="form-text text-muted text-danger visually-hidden">
+        </span>
+        <span class="form-text text-muted">This is the publication's ID on
+          <a href="https://pubmed.ncbi.nlm.nih.gov/"
+             title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a>
+        </span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-title" class="col-sm-2 col-form-label">
+        Title</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-title"
+               name="publication-title"
+               class="form-control" />
+        <span class="form-text text-muted">Provide the publication's title here.</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-authors" class="col-sm-2 col-form-label">
+        Authors</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-authors"
+               name="publication-authors"
+               required="required"
+               class="form-control" />
+        <span class="form-text text-muted">
+          A publication <strong>MUST</strong> have an author. You <em>must</em>
+          provide a value for the authors field.
+        </span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-journal" class="col-sm-2 col-form-label">
+        Journal</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-journal"
+               name="publication-journal"
+               class="form-control" />
+        <span class="form-text text-muted">Provide the name journal where the
+          publication was done, here.</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="select-publication-month"
+             class="col-sm-2 col-form-label">
+        Month</label>
+      <div class="col-sm-4">
+        <select class="form-control"
+                id="select-publication-month"
+                name="publication-month">
+          <option value="">Select a month</option>
+          <option value="january">January</option>
+          <option value="february">February</option>
+          <option value="march">March</option>
+          <option value="april">April</option>
+          <option value="may">May</option>
+          <option value="june">June</option>
+          <option value="july">July</option>
+          <option value="august">August</option>
+          <option value="september">September</option>
+          <option value="october">October</option>
+          <option value="november">November</option>
+          <option value="december">December</option>
+        </select>
+        <span class="form-text text-muted">Month of publication</span>
+      </div>
+
+      <label for="txt-publication-year"
+             class="col-sm-2 col-form-label">
+        Year</label>
+      <div class="col-sm-4">
+        <input type="number"
+               id="txt-publication-year"
+               name="publication-year"
+               class="form-control"
+               min="1960" />
+        <span class="form-text text-muted">Year of publication</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-volume"
+             class="col-sm-2 col-form-label">
+        Volume</label>
+      <div class="col-sm-4">
+        <input type="text"
+               id="txt-publication-volume"
+               name="publication-volume"
+               class="form-control">
+        <span class="form-text text-muted">Journal volume</span>
+      </div>
+
+      <label for="txt-publication-pages"
+             class="col-sm-2 col-form-label">
+        Pages</label>
+      <div class="col-sm-4">
+        <input type="text"
+               id="txt-publication-pages"
+               name="publication-pages"
+               class="form-control" />
+        <span class="form-text text-muted">Journal pages for the publication</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label>
+      <div class="col-sm-10">
+        <textarea id="txt-publication-abstract"
+                  name="publication-abstract"
+                  class="form-control"
+                  rows="7"></textarea>
+      </div>
+    </div>
+
+    <div class="row">
+      <div class="col">
+        <input type="submit"
+               class="btn btn-primary"
+               value="create publication" />
+      </div>
+      <div class="col">
+        <input type="reset" class="btn btn-danger" value="reset form" />
+      </div>
+    </div>
+
+</form>
+</div>
+
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/pubmed.js"></script>
+<script type="text/javascript">
+  $(function() {
+      $("#btn-search-pubmed-id").on("click", (event) => {
+          event.preventDefault();
+          var search_button = event.target;
+          var pubmed_id = $("#txt-pubmed-id").val().trim();
+          remove_class($("#txt-pubmed-id").parent(), "has-error");
+          if(pubmed_id == "") {
+              add_class($("#txt-pubmed-id").parent(), "has-error");
+              return false;
+          }
+
+          search_button.disabled = true;
+          // Fetch publication details
+          fetch_publication_details(pubmed_id,
+                                    [() => {search_button.disabled = false;}]);
+          return false;
+      });
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/sui-delete-publication.html b/uploader/templates/publications/sui-delete-publication.html
new file mode 100644
index 0000000..436f2c1
--- /dev/null
+++ b/uploader/templates/publications/sui-delete-publication.html
@@ -0,0 +1,95 @@
+{%extends "publications/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Delete Publication{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.delete_publication',
+           publication_id=publication.Id)}}"
+     title="Manage publications">delete publication</a>
+</li>
+{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+<div class="row">
+  <p>You are about to delete the publication with the following details:</p>
+</div>
+
+<div class="row">
+  <table class="table">
+    <tr>
+      <th>Linked Phenotypes</th>
+      <td>{{linked_phenotypes | count}}</td>
+    </tr>
+    <tr>
+      <th>PubMed</th>
+      <td>
+        {%if publication.PubMed_ID%}
+        <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/"
+           target="_blank">{{publication.PubMed_ID}}</a>
+        {%else%}
+        —
+        {%endif%}
+      </td>
+    </tr>
+    <tr>
+      <th>Title</th>
+      <td>{{publication.Title or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Authors</th>
+      <td>{{publication.Authors or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Journal</th>
+      <td>{{publication.Journal or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Published</th>
+      <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Volume</th>
+      <td>{{publication.Volume or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Pages</th>
+      <td>{{publication.Pages or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Abstract</th>
+      <td>
+        {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%}
+        <p>{{line}}</p>
+        {%endfor%}
+      </td>
+    </tr>
+  </table>
+</div>
+
+<div class="row">
+  <p>If you are sure that is what you want, click the button below to delete the
+    publication</p>
+  <p class="form-text text-small">
+    <small>You will not be able to recover the data if you click
+      delete below.</small></p>
+
+  <form action="{{url_for('publications.delete_publication', publication_id=publication_id)}}"
+        method="POST">
+    <div class="form-group">
+      <input type="submit" value="delete" class="btn btn-danger" />
+    </div>
+  </form>
+</div>
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript">
+  $(function() {});
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/sui-edit-publication.html b/uploader/templates/publications/sui-edit-publication.html
new file mode 100644
index 0000000..847b020
--- /dev/null
+++ b/uploader/templates/publications/sui-edit-publication.html
@@ -0,0 +1,203 @@
+{%extends "publications/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Edit Publication{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('publications.edit_publication',
+           publication_id=publication.Id)}}"
+     title="Edit the publication's details">edit publication</a>
+</li>
+{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <form id="frm-create-publication"
+        method="POST"
+        action="{{url_for('publications.edit_publication',
+                publication_id=publication_id,
+                next=request.args.get('next', ''))}}"
+        class="form-horizontal">
+
+    <div class="row mb-3">
+      <label for="txt-pubmed-id" class="col-sm-2 col-form-label">
+        PubMed ID</label>
+      <div class="col-sm-10">
+        <div class="input-group">
+          <input type="text"
+                 id="txt-pubmed-id"
+                 name="pubmed-id"
+                 value="{{publication.PubMed_ID or ''}}"
+                 class="form-control" />
+          <div class="input-group-text">
+            <button class="btn btn-outline-primary"
+                    id="btn-search-pubmed-id">search</button>
+          </div>
+        </div>
+        <span id="search-pubmed-id-error"
+              class="form-text text-muted text-danger visually-hidden">
+        </span>
+        <span class="form-text text-muted">This is the publication's ID on
+          <a href="https://pubmed.ncbi.nlm.nih.gov/"
+             title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a>
+        </span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-title" class="col-sm-2 col-form-label">
+        Title</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-title"
+               name="publication-title"
+               value="{{publication.Title}}"
+               class="form-control" />
+        <span class="form-text text-muted">Provide the publication's title here.</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-authors" class="col-sm-2 col-form-label">
+        Authors</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-authors"
+               name="publication-authors"
+               value="{{publication.Authors}}"
+               required="required"
+               class="form-control" />
+        <span class="form-text text-muted">
+          A publication <strong>MUST</strong> have an author. You <em>must</em>
+          provide a value for the authors field.
+        </span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-journal" class="col-sm-2 col-form-label">
+        Journal</label>
+      <div class="col-sm-10">
+        <input type="text"
+               id="txt-publication-journal"
+               name="publication-journal"
+               value="{{publication.Journal}}"
+               class="form-control" />
+        <span class="form-text text-muted">Provide the name journal where the
+          publication was done, here.</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="select-publication-month"
+             class="col-sm-2 col-form-label">
+        Month</label>
+      <div class="col-sm-4">
+        <select class="form-control"
+                id="select-publication-month"
+                name="publication-month">
+          <option value="">Select a month</option>
+          {%for month in ("january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"):%}
+          <option value="{{month}}"
+                  {%if publication.Month | lower == month %}
+                  selected="selected"
+                  {%endif%}>
+            {{month | title}}
+          </option>
+          {%endfor%}
+        </select>
+        <span class="form-text text-muted">Month of publication</span>
+      </div>
+
+      <label for="txt-publication-year"
+             class="col-sm-2 col-form-label">
+        Year</label>
+      <div class="col-sm-4">
+        <input type="number"
+               id="txt-publication-year"
+               name="publication-year"
+               value="{{publication.Year}}"
+               class="form-control"
+               min="1960" />
+        <span class="form-text text-muted">Year of publication</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-publication-volume"
+             class="col-sm-2 col-form-label">
+        Volume</label>
+      <div class="col-sm-4">
+        <input type="text"
+               id="txt-publication-volume"
+               name="publication-volume"
+               value="{{publication.Volume}}"
+               class="form-control">
+        <span class="form-text text-muted">Journal volume</span>
+      </div>
+
+      <label for="txt-publication-pages"
+             class="col-sm-2 col-form-label">
+        Pages</label>
+      <div class="col-sm-4">
+        <input type="text"
+               id="txt-publication-pages"
+               name="publication-pages"
+               value="{{publication.Pages}}"
+               class="form-control" />
+        <span class="form-text text-muted">Journal pages for the publication</span>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label>
+      <div class="col-sm-10">
+        <textarea id="txt-publication-abstract"
+                  name="publication-abstract"
+                  class="form-control"
+                  rows="7">{{publication.Abstract or ""}}</textarea>
+      </div>
+    </div>
+
+    <div class="row mb-3">
+      <div class="col-sm-2"></div>
+      <div class="col-sm-8">
+        <input type="submit" class="btn btn-primary" value="Save" />
+        <input type="reset" class="btn btn-danger" />
+      </div>
+    </div>
+
+</form>
+</div>
+
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/pubmed.js"></script>
+<script type="text/javascript">
+  $(function() {
+      $("#btn-search-pubmed-id").on("click", (event) => {
+          event.preventDefault();
+          var search_button = event.target;
+          var pubmed_id = $("#txt-pubmed-id").val().trim();
+          remove_class($("#txt-pubmed-id").parent(), "has-error");
+          if(pubmed_id == "") {
+              add_class($("#txt-pubmed-id").parent(), "has-error");
+              return false;
+          }
+
+          search_button.disabled = true;
+          // Fetch publication details
+          fetch_publication_details(pubmed_id,
+                                    [() => {search_button.disabled = false;}]);
+          return false;
+      });
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/sui-index.html b/uploader/templates/publications/sui-index.html
new file mode 100644
index 0000000..e405dd1
--- /dev/null
+++ b/uploader/templates/publications/sui-index.html
@@ -0,0 +1,109 @@
+{%extends "publications/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Publications{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row" style="padding-bottom: 1em;">
+  <div class="col">
+    <a href="{{url_for('publications.create_publication')}}"
+       class="btn btn-primary"
+       title="Create a new publication.">
+      add new publication</a>
+  </div>
+</div>
+
+<div class="row">
+  <p>Click on title to view more details and to edit details for that
+    publication.</p>
+</div>
+
+<div class="row">
+  <table id="tbl-list-publications" class="table compact stripe">
+    <thead>
+      <tr>
+        <th>#</th>
+        <th>PubMed ID</th>
+        <th>Title</th>
+        <th>Authors</th>
+      </tr>
+    </thead>
+
+    <tbody></tbody>
+  </table>
+</div>
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/urls.js"></script>
+
+<script type="text/javascript">
+  $(function() {
+      var publicationsDataTable = buildDataTable(
+          "#tbl-list-publications",
+          [],
+          [
+              {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
+                      }
+                      url=buildURLFromCurrentURL(
+                          `/publications/view/${pub.Id}`);
+                      return `<a href="${url}" 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"
+              },
+              scrollY: 700,
+              scroller: true,
+              scrollCollapse: true,
+              paging: true,
+              deferRender: true,
+              layout: {
+                  topStart: "info",
+                  topEnd: "search",
+                  bottomStart: "pageLength",
+                  bottomEnd: false
+              }
+          });
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/sui-view-publication.html b/uploader/templates/publications/sui-view-publication.html
new file mode 100644
index 0000000..740fc37
--- /dev/null
+++ b/uploader/templates/publications/sui-view-publication.html
@@ -0,0 +1,80 @@
+{%extends "publications/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}View Publication{%endblock%}
+
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <table class="table">
+    <tr>
+      <th>Linked Phenotypes</th>
+      <td>{{linked_phenotypes | count}}</td>
+    </tr>
+    <tr>
+      <th>PubMed</th>
+      <td>
+        {%if publication.PubMed_ID%}
+        <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/"
+           target="_blank">{{publication.PubMed_ID}}</a>
+        {%else%}
+        —
+        {%endif%}
+      </td>
+    </tr>
+    <tr>
+      <th>Title</th>
+      <td>{{publication.Title or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Authors</th>
+      <td>{{publication.Authors or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Journal</th>
+      <td>{{publication.Journal or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Published</th>
+      <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Volume</th>
+      <td>{{publication.Volume or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Pages</th>
+      <td>{{publication.Pages or "—"}}</td>
+    </tr>
+    <tr>
+      <th>Abstract</th>
+      <td>
+        {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%}
+        <p>{{line}}</p>
+        {%endfor%}
+      </td>
+    </tr>
+  </table>
+</div>
+
+<div class="row">
+  <div>
+  <a href="{{url_for('publications.edit_publication', publication_id=publication.Id)}}"
+     title="Edit details for this publication."
+     class="btn btn-primary">Edit</a>
+  {%if linked_phenotypes | length == 0%}
+  <a href="{{url_for('publications.delete_publication', publication_id=publication.Id)}}"
+     class="btn btn-danger">delete</a>
+  {%endif%}
+  </div>
+</div>
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript">
+  $(function() {});
+</script>
+{%endblock%}
diff --git a/uploader/templates/publications/view-publication.html b/uploader/templates/publications/view-publication.html
index 388547a..0bd7bc5 100644
--- a/uploader/templates/publications/view-publication.html
+++ b/uploader/templates/publications/view-publication.html
@@ -12,6 +12,10 @@
 <div class="row">
   <table class="table">
     <tr>
+      <th>Linked Phenotypes</th>
+      <td>{{linked_phenotypes | count}}</td>
+    </tr>
+    <tr>
       <th>PubMed</th>
       <td>
         {%if publication.PubMed_ID%}
@@ -58,15 +62,15 @@
 </div>
 
 <div class="row">
-  <form id="frm-edit-delete-publication" method="POST" action="#">
-    <input type="hidden" name="publication_id" value="{{publication.Id}}" />
-    <div class="form-group">
-      <input type="submit" value="edit" class="btn btn-primary not-implemented" />
-      {%if linked_phenotypes | length == 0%}
-      <input type="submit" value="delete" class="btn btn-danger not-implemented" />
-      {%endif%}
-    </div>
-  </form>
+  <div>
+  <a href="{{url_for('publications.edit_publication', publication_id=publication.Id)}}"
+     title="Edit details for this publication."
+     class="btn btn-primary">Edit</a>
+  {%if linked_phenotypes | length == 0%}
+  <a href="{{url_for('publications.delete_publication', publication_id=publication.Id)}}"
+     class="btn btn-danger">delete</a>
+  {%endif%}
+  </div>
 </div>
 {%endblock%}
 
diff --git a/uploader/templates/samples/list-samples.html b/uploader/templates/samples/list-samples.html
index 185e784..aed27c3 100644
--- a/uploader/templates/samples/list-samples.html
+++ b/uploader/templates/samples/list-samples.html
@@ -29,6 +29,19 @@
   </p>
 </div>
 
+<div class="row">
+  <p>
+    <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 samples
+    </a>
+  </p>
+</div>
+
 {%if samples | length > 0%}
 <div class="row">
   <p>
@@ -96,32 +109,17 @@
 
   <p>
     <a href="#"
-       title="Add samples for population '{{population.FullName}}' from species
+       title="Delete samples from population '{{population.FullName}}' from species
               '{{species.FullName}}'."
-       class="btn btn-danger">
+       class="btn btn-danger not-implemented">
       delete all samples
     </a>
   </p>
 </div>
-
 {%else%}
-
 <div class="row">
-  <p>
-    There are no samples entered for this population. Do please go ahead and add
-    the samples for this population by clicking on the button below.
-  </p>
-
-  <p>
-    <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 samples
-    </a>
-  </p>
+  <p>There are no samples entered for this population. Click the "Add Samples"
+    button above, to add some new samples.</p>
 </div>
 {%endif%}
 
diff --git a/uploader/templates/samples/sui-base.html b/uploader/templates/samples/sui-base.html
new file mode 100644
index 0000000..8ec7505
--- /dev/null
+++ b/uploader/templates/samples/sui-base.html
@@ -0,0 +1,25 @@
+{%extends "populations/sui-base.html"%}
+{%from "populations/macro-display-population-card.html" import display_sui_population_card%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.populations.samples.list_samples',
+           species_id=species['SpeciesId'],
+           population_id=population.Id)}}">
+    Samples
+  </a>
+</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%}
diff --git a/uploader/templates/samples/sui-upload-failure.html b/uploader/templates/samples/sui-upload-failure.html
new file mode 100644
index 0000000..d950c50
--- /dev/null
+++ b/uploader/templates/samples/sui-upload-failure.html
@@ -0,0 +1,32 @@
+{%extends "sui-base.html"%}
+{%from "cli-output.html" import cli_output%}
+
+{%block title%}Samples Upload Failure{%endblock%}
+
+{%block contents%}
+<div class="row">
+<h2 class="heading">{{job.job_name[0:50]}}&hellip;</h2>
+
+<p>There was a failure attempting to upload the samples.</p>
+
+<p>Here is some information to help with debugging the issue. Provide this
+  information to the developer/maintainer.</p>
+
+<h3>Debugging Information</h3>
+<ul>
+  <li><strong>job id</strong>: {{job.jobid}}</li>
+  <li><strong>status</strong>: {{job.status}}</li>
+  <li><strong>job type</strong>: {{job["job-type"]}}</li>
+</ul>
+</div>
+
+<div class="row">
+<h4>stdout</h4>
+{{cli_output(job, "stdout")}}
+</div>
+
+<div class="row">
+<h4>stderr</h4>
+{{cli_output(job, "stderr")}}
+</div>
+{%endblock%}
diff --git a/uploader/templates/samples/sui-upload-progress.html b/uploader/templates/samples/sui-upload-progress.html
new file mode 100644
index 0000000..8412c46
--- /dev/null
+++ b/uploader/templates/samples/sui-upload-progress.html
@@ -0,0 +1,26 @@
+{%extends "samples/sui-base.html"%}
+{%from "cli-output.html" import cli_output%}
+
+{%block extrameta%}
+<meta http-equiv="refresh" content="5">
+{%endblock%}
+
+{%block title%}Job Status{%endblock%}
+
+{%block contents%}
+<div class="row" style="overflow-x: clip;">
+<h2 class="heading">{{job.job_name[0:50]}}&hellip;</h2>
+
+<p>
+<strong>status</strong>:
+<span>{{job["status"]}} ({{job.get("message", "-")}})</span><br />
+</p>
+
+<p>saving to database...</p>
+</div>
+
+<div class="row">
+  {{cli_output(job, "stdout")}}
+</div>
+
+{%endblock%}
diff --git a/uploader/templates/samples/sui-upload-samples.html b/uploader/templates/samples/sui-upload-samples.html
new file mode 100644
index 0000000..83c2061
--- /dev/null
+++ b/uploader/templates/samples/sui-upload-samples.html
@@ -0,0 +1,153 @@
+{%extends "samples/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Samples &mdash; Upload Samples{%endblock%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.populations.samples.upload_samples',
+           species_id=species['SpeciesId'],
+           population_id=population.Id)}}">
+    Upload
+  </a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <form id="form-samples"
+        method="POST"
+        action="{{url_for('species.populations.samples.upload_samples',
+                species_id=species.SpeciesId,
+                population_id=population.InbredSetId)}}"
+        enctype="multipart/form-data">
+    <legend class="heading">upload samples</legend>
+
+    <input type="hidden" name="species_id" value="{{species.SpeciesId}}" />
+    <input type="hidden" name="population_id" value="{{population.Id}}" />
+
+    <div class="form-group">
+      <label for="file-samples" class="form-label">select file</label>
+      <input type="file" name="samples_file" id="file-samples"
+	     accept="text/csv, text/tab-separated-values, text/plain"
+	     class="form-control" />
+      <small class="form-text text-muted">
+        See the <a href="#docs-samples-upload">documentation below</a> for
+        details on expected file format.</small>
+    </div>
+
+    <div class="form-group">
+      <label for="select-separator" class="form-label">field separator</label>
+      <select id="select-separator"
+	      name="separator"
+	      required="required"
+	      class="form-control">
+        <option value="">Select separator for your file: (default is comma)</option>
+        <option value="&#x0009;">TAB</option>
+        <option value="&#x0020;">Space</option>
+        <option value=",">Comma</option>
+        <option value=";">Semicolon</option>
+        <option value="other">Other</option>
+      </select>
+      <input id="txt-separator"
+	     type="text"
+	     name="other_separator"
+	     class="form-control" />
+      <small class="form-text text-muted">
+        If you select '<strong>Other</strong>' for the field separator value,
+        enter the character that separates the fields in your CSV file in the form
+        field below.
+      </small>
+    </div>
+
+    <div class="form-group form-check">
+      <input id="chk-heading"
+	     type="checkbox"
+	     name="first_line_heading"
+	     class="form-check-input" />
+      <label for="chk-heading" class="form-check-label">
+        first line is a heading?</label>
+      <small class="form-text text-muted">
+        Select this if the first line in your file contains headings for the
+        columns.
+      </small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-delimiter" class="form-label">field delimiter</label>
+      <input id="txt-delimiter"
+	     type="text"
+	     name="field_delimiter"
+	     maxlength="1"
+	     class="form-control" />
+      <small class="form-text text-muted">
+        If there is a character delimiting the string texts within particular
+        fields in your CSV, provide the character here. This can be left blank if
+        no such delimiters exist in your file.
+      </small>
+    </div>
+
+    <button type="submit"
+	    class="btn btn-primary">upload samples file</button>
+  </form>
+</div>
+
+<div class="row">
+  <h3>Preview File Content</h3>
+
+  <table id="tbl:samples-preview" class="table">
+    <caption class="heading">preview content</caption>
+
+    <thead>
+      <tr>
+        <th>Name</th>
+        <th>Name2</th>
+        <th>Symbol</th>
+        <th>Alias</th>
+      </tr>
+    </thead>
+
+    <tbody>
+      <tr id="default-row">
+        <td colspan="4">
+	  Please make some selections in the form above to preview the data.</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+
+
+
+<div class="row" id="docs-samples-upload">
+  <h3 class="subheading">File Format</h3>
+  <p>
+    Upload a <strong>character-separated value (CSV)</strong> file that contains
+    details about your samples. The CSV file should have the following fields:
+    <dl>
+      <dt>Name</dt>
+      <dd>The primary name/identifier for the sample/individual.</dd>
+
+      <dt>Name2</dt>
+      <dd>A secondary name for the sample. This can simply be the same as
+        <strong>Name</strong> above. This field <strong>MUST</strong> contain a
+        value.</dd>
+
+      <dt>Symbol</dt>
+      <dd>A symbol for the sample. This can be a strain name, e.g. 'BXD60' for
+        species that have strains. This field can be left empty for species like
+        Humans that do not have strains..</dd>
+
+      <dt>Alias</dt>
+      <dd>An alias for the sample. Can be an empty field, or take on the same
+        value as that of the Symbol.</dd>
+    </dl>
+  </p>
+</div>
+{%endblock%}
+
+{%block javascript%}
+<script src="/static/js/upload_samples.js" type="text/javascript"></script>
+{%endblock%}
diff --git a/uploader/templates/samples/sui-upload-success.html b/uploader/templates/samples/sui-upload-success.html
new file mode 100644
index 0000000..054bde8
--- /dev/null
+++ b/uploader/templates/samples/sui-upload-success.html
@@ -0,0 +1,31 @@
+{%extends "samples/sui-base.html"%}
+{%from "cli-output.html" import cli_output%}
+
+{%block title%}Job Status{%endblock%}
+
+{%block contents%}
+
+<div class="row" style="overflow-x: clip;">
+  <h2 class="heading">{{job.job_name[0:50]}}&hellip;</h2>
+
+  <p>
+    <strong>status</strong>:
+    <span>{{job["status"]}} ({{job.get("message", "-")}})</span><br />
+  </p>
+
+  <p>Successfully uploaded the samples.</p>
+  <p>
+    <a href="{{url_for('species.populations.samples.list_samples',
+             species_id=species.SpeciesId,
+             population_id=population.Id)}}"
+       title="View population samples">
+      View samples
+    </a>
+  </p>
+</div>
+
+<div class="row">
+  {{cli_output(job, "stdout")}}
+</div>
+
+{%endblock%}
diff --git a/uploader/templates/samples/upload-samples.html b/uploader/templates/samples/upload-samples.html
index 25d3290..4aa2f7f 100644
--- a/uploader/templates/samples/upload-samples.html
+++ b/uploader/templates/samples/upload-samples.html
@@ -65,14 +65,14 @@
 
     <div class="form-group">
       <label for="file-samples" class="form-label">select file</label>
-      <input type="file" name="samples_file" id="file:samples"
-	     accept="text/csv, text/tab-separated-values"
+      <input type="file" name="samples_file" id="file-samples"
+	     accept="text/csv, text/tab-separated-values, text/plain"
 	     class="form-control" />
     </div>
 
     <div class="form-group">
-      <label for="select:separator" class="form-label">field separator</label>
-      <select id="select:separator"
+      <label for="select-separator" class="form-label">field separator</label>
+      <select id="select-separator"
 	      name="separator"
 	      required="required"
 	      class="form-control">
@@ -83,7 +83,7 @@
         <option value=";">Semicolon</option>
         <option value="other">Other</option>
       </select>
-      <input id="txt:separator"
+      <input id="txt-separator"
 	     type="text"
 	     name="other_separator"
 	     class="form-control" />
@@ -95,11 +95,11 @@
     </div>
 
     <div class="form-group form-check">
-      <input id="chk:heading"
+      <input id="chk-heading"
 	     type="checkbox"
 	     name="first_line_heading"
 	     class="form-check-input" />
-      <label for="chk:heading" class="form-check-label">
+      <label for="chk-heading" class="form-check-label">
         first line is a heading?</label>
       <small class="form-text text-muted">
         Select this if the first line in your file contains headings for the
@@ -108,8 +108,8 @@
     </div>
 
     <div class="form-group">
-      <label for="txt:delimiter" class="form-label">field delimiter</label>
-      <input id="txt:delimiter"
+      <label for="txt-delimiter" class="form-label">field delimiter</label>
+      <input id="txt-delimiter"
 	     type="text"
 	     name="field_delimiter"
 	     maxlength="1"
diff --git a/uploader/templates/species/macro-display-species-card.html b/uploader/templates/species/macro-display-species-card.html
index 166c7b9..30c564f 100644
--- a/uploader/templates/species/macro-display-species-card.html
+++ b/uploader/templates/species/macro-display-species-card.html
@@ -20,3 +20,32 @@
   </div>
 </div>
 {%endmacro%}
+
+
+{%macro display_sui_species_card(species)%}
+<div class="row">
+  <table class="table">
+    <caption>Current Species</caption>
+    <tbody>
+      <tr>
+        <th>Name</th>
+        <td>{{species["Name"] | title}}</td>
+      </tr>
+      <tr>
+        <th>Scientific</th>
+        <td>{{species["FullName"]}}</td>
+      </tr>
+      {%if species["TaxonomyId"]%}
+      <tr>
+        <th>Taxonomy ID</th>
+        <td>
+          <a href="https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id={{species.TaxonomyId}}"
+             title="NCBI's Taxonomy Browser page for {{species.Name}}">
+            {{species.TaxonomyId}}</a>
+        </td>
+      </tr>
+    </tbody>
+    {%endif%}
+  </table>
+</div>
+{%endmacro%}
diff --git a/uploader/templates/species/sui-base.html b/uploader/templates/species/sui-base.html
new file mode 100644
index 0000000..f7b4fef
--- /dev/null
+++ b/uploader/templates/species/sui-base.html
@@ -0,0 +1,10 @@
+{%extends "sui-base.html"%}
+
+{%block breadcrumbs%}
+{{super()}}
+<li class="breadcrumb-item">
+  <a href="{{url_for('species.view_species', species_id=species['SpeciesId'])}}">
+    {{species["Name"]|title}}
+  </a>
+</li>
+{%endblock%}
diff --git a/uploader/templates/species/sui-view-species.html b/uploader/templates/species/sui-view-species.html
new file mode 100644
index 0000000..4b6402e
--- /dev/null
+++ b/uploader/templates/species/sui-view-species.html
@@ -0,0 +1,127 @@
+{%extends "species/sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-forms.html" import add_http_feature_flags%}
+{%from "macro-step-indicator.html" import step_indicator%}
+{%from "species/macro-display-species-card.html" import display_sui_species_card%}
+
+{%block title%}View Species{%endblock%}
+
+{%macro add_form_buttons()%}
+<div class="row form-buttons">
+  <div class="col">
+    <input type="submit"
+           value="use selected population"
+           class="btn btn-primary" />
+  </div>
+
+  <div class="col">
+    <a href="url_for('species.population.create_population',
+             species_id=species.SpeciesId,
+             return_to='species.view_species')"
+       title="Create a new population for species '{{species.Name}}'."
+       class="btn btn-outline-info">
+      Create a new population
+    </a>
+  </div>
+</div>
+{%endmacro%}
+
+
+{%block contents%}
+<div class="row">
+  <h2 class="heading">{{species.FullName}} ({{species.Name}})</h2>
+</div>
+
+<div class "row">
+  <ul class="nav nav-tabs" id="species-actions">
+    <li class="nav-item presentation">
+      <button class="nav-link active"
+              id="populations-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#populations-content"
+              type="button"
+              role="tab"
+              aria-controls="populations-content"
+              aria-selected="true">Populations</button>
+    </li>
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="sequencing-platforms-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#sequencing-platforms-content"
+              type="button"
+              role="tab"
+              aria-controls="sequencing-platforms-content"
+              aria-selected="true">Sequencing Platforms</button>
+    </li>
+  </ul>
+</div>
+
+<div class="row">
+  <div class="tab-content" id="species-tabs-content">
+    <div class="tab-pane fade show active"
+         id="populations-content"
+         role="tabpanel"
+         aria-labelledby="populations-content-tab">
+      <p>Data belonging to a particular species is further divided into one or more
+        populations for easier handling. Please select the population you want to work
+        with.</p>
+
+      <form method="GET"
+            action="{{url_for('species.view_species', species_id=species.SpeciesId)}}"
+            class="form-horizontal">
+        {{add_http_feature_flags()}}
+        {{add_form_buttons()}}
+
+        {%if populations | length != 0%}
+        <div style="margin-top:0.3em;">
+          <table id="tbl-select-population" class="table compact stripe"
+                 data-populations-list='{{populations | tojson}}'>
+            <thead>
+              <tr>
+                <th></th>
+                <th>Population</th>
+              </tr>
+            </thead>
+
+            <tbody></tbody>
+          </table>
+        </div>
+
+        {%else%}
+        <p class="form-text">
+          There are no populations currently defined for {{species['FullName']}}
+          ({{species['SpeciesName']}}).</p>
+        {%endif%}
+
+        {{add_form_buttons()}}
+
+      </form>
+    </div>
+    <div class="tab-pane fade"
+         id="sequencing-platforms-content"
+         role="tabpanel"
+         aria-labelledby="sequencing-platforms-content-tab">
+      <p>Upload and manage the sequencing platforms for species
+        '{{species.Name | title}} ({{species.FullName}})'
+        <a href="{{url_for('species.platforms.list_platforms',
+                 species_id=species.SpeciesId)}}"
+           title="Manage sequencing platforms for {{species.Name}}">here</a>.
+      </p>
+    </div>
+  </div>
+</div>
+{%endblock%}
+
+{%block sidebarcontents%}
+<div class="row">
+  <p>You can manage species' populations and sequencing platforms here. Select
+    the tab for the feature you wish to continue working on.</p>
+</div>
+{{display_sui_species_card(species)}}
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript" src="/static/js/populations.js"></script>
+{%endblock%}
diff --git a/uploader/templates/sui-base.html b/uploader/templates/sui-base.html
new file mode 100644
index 0000000..719a646
--- /dev/null
+++ b/uploader/templates/sui-base.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<html lang="en">
+
+  <head>
+
+    <meta charset="UTF-8" />
+    <meta application-name="GeneNetwork Quality-Control Application" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    {%block extrameta%}{%endblock%}
+
+    <title>Data Upload and Quality Control: {%block title%}{%endblock%}</title>
+
+    <link rel="stylesheet" type="text/css"
+	  href="{{url_for('base.bootstrap',
+                filename='css/bootstrap.min.css')}}" />
+    <link rel="stylesheet" type="text/css"
+          href="{{url_for('base.datatables',
+                filename='css/dataTables.bootstrap5.min.css')}}" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-common.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-large.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-medium.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/layout-small.css" />
+    <link rel="stylesheet" type="text/css" href="/static/css/theme.css" />
+
+    {%block css%}{%endblock%}
+
+  </head>
+
+  <body>
+    <header id="header">
+      <span id="header-text">GeneNetwork</span>
+      <nav id="header-nav">
+        <ul class="nav justify-content-end">
+          <li>
+            {%if user_logged_in()%}
+            <a href="{{url_for('oauth2.logout')}}"
+               title="Log out of the system">
+              <span class="glyphicon glyphicon-user"></span>
+              {{user_email()}} Sign Out</a>
+            {%else%}
+            <a href="{{authserver_authorise_uri()}}"
+               title="Log in to the system">Sign In</a>
+            {%endif%}
+          </li>
+        </ul>
+      </nav>
+    </header>
+
+
+    <main id="main" class="main">
+      <nav id="breadcrumbs" aria-label="breadcrumb">
+        <ol class="breadcrumb">
+          {%block breadcrumbs%}
+          <li class="breadcrumb-item">
+            <a href="{{url_for('base.index')}}">Home</a></li>
+          {%endblock%}
+        </ol>
+      </nav>
+
+      <div id="main-content">
+          {%block contents%}{%endblock%}
+        </div>
+
+      <div id="sidebar-content">
+          {%block sidebarcontents%}{%endblock%}
+        </div>
+    </main>
+
+
+
+    <script type="text/javascript" src="/static/js/debug.js"></script>
+    <!--
+        Core dependencies
+      -->
+    <script src="{{url_for('base.jquery',
+                 filename='jquery.min.js')}}"></script>
+    <script src="{{url_for('base.bootstrap',
+                 filename='js/bootstrap.min.js')}}"></script>
+
+    <!--
+        DataTables dependencies
+      -->
+    <script type="text/javascript"
+            src="{{url_for('base.datatables',
+                 filename='js/dataTables.min.js')}}"></script>
+    <script type="text/javascript"
+        src="{{url_for('base.datatables_extensions',
+             filename='scroller/js/dataTables.scroller.min.js')}}"></script>
+    <script type="text/javascript"
+            src="{{url_for('base.datatables_extensions',
+                 filename='buttons/js/dataTables.buttons.min.js')}}"></script>
+    <script type="text/javascript"
+            src="{{url_for('base.datatables_extensions',
+                 filename='select/js/dataTables.select.min.js')}}"></script>
+
+    <!--
+        local dependencies
+      -->
+    <script type="text/javascript" src="/static/js/utils.js"></script>
+    <script type="text/javascript" src="/static/js/datatables.js"></script>
+    {%block javascript%}{%endblock%}
+  </body>
+</html>
diff --git a/uploader/templates/sui-index.html b/uploader/templates/sui-index.html
new file mode 100644
index 0000000..b93bf40
--- /dev/null
+++ b/uploader/templates/sui-index.html
@@ -0,0 +1,161 @@
+{%extends "sui-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-forms.html" import add_http_feature_flags%}
+{%from "macro-step-indicator.html" import step_indicator%}
+
+{%block title%}Home{%endblock%}
+
+{%block pagetitle%}Home{%endblock%}
+
+{%block extra_breadcrumbs%}{%endblock%}
+
+{%block contents%}
+
+{%macro add_form_buttons()%}
+<div class="row form-buttons">
+  <div class="col">
+    <input type="submit"
+           class="btn btn-primary"
+           value="use selected species" />
+  </div>
+  <div class="col">
+    <a href="{{url_for('species.create_species', return_to='base.index')}}"
+       class="btn btn-outline-primary"
+       title="Create a new species.">Create a new Species</a>
+  </div>
+</div>
+{%endmacro%}
+
+<div class="row">{{flash_all_messages()}}</div>
+
+{%if user_logged_in()%}
+
+<div class="row">
+  <ul class="nav nav-tabs" id="index-actions">
+    <li class="nav-item presentation">
+      <button class="nav-link active"
+              id="upload-data-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#upload-data-content"
+              type="button"
+              role="tab"
+              aria-controls="upload-data-content"
+              aria-selected="false">Upload Data</button></li>
+    <li class="nav-item presentation">
+      <button class="nav-link"
+              id="publications-tab"
+              data-bs-toggle="tab"
+              data-bs-target="#publications-content"
+              type="button"
+              role="tab"
+              aria-controls="publications-content"
+              aria-selected="true">Publications</button></li>
+  </ul>
+</div>
+
+<div class="row">
+  <div class="tab-content" id="upload-data-tabs-content">
+    <div class="tab-pane fade show active"
+         id="upload-data-content"
+         role="tabpanel"
+         aria-labelledby="upload-data-content-tab">
+      <h2 class="heading">Species</h2>
+
+      <p>Select the species you want to work with.</p>
+
+      <form method="GET" action="{{url_for('base.index')}}" class="form-horizontal">
+        {{add_http_feature_flags()}}
+
+        {{add_form_buttons()}}
+
+        {%if species | length != 0%}
+        <div style="margin-top:1em;">
+          <table id="tbl-select-species" class="table compact stripe"
+                 data-species-list='{{species | tojson}}'>
+            <thead>
+              <tr>
+                <th></th>
+                <th>Species Name</th>
+              </tr>
+            </thead>
+
+            <tbody></tbody>
+          </table>
+        </div>
+
+        {%else%}
+
+        <label class="control-label" for="rdo-cant-find-species">
+          <input id="rdo-cant-find-species" type="radio" name="species_id"
+                 value="CREATE-SPECIES" />
+          There are no species to select from. Create the first one.</label>
+
+        <div class="col-sm-offset-10 col-sm-2">
+          <input type="submit"
+                 class="btn btn-primary col-sm-offset-1"
+                 value="continue" />
+        </div>
+
+        {%endif%}
+
+        {{add_form_buttons()}}
+
+      </form>
+    </div>
+
+    <div class="tab-pane fade"
+         id="publications-content"
+         role="tabpanel"
+         aria-labelledby="publications-content-tab">
+      <p>View, edit and delete existing publications, and add new
+        publications by clicking on the button below.</p>
+
+      <a href="{{url_for('publications.index')}}"
+         title="Manage publications."
+         class="btn btn-primary">manage publications</a>
+    </div>
+  </div>
+</div>
+
+  {%else%}
+
+  <div class="row">
+    <p>The Genenetwork Uploader (<em>gn-uploader</em>) enables upload of new data
+      into the Genenetwork System. It provides Quality Control over data, and
+      guidance in case you data does not meet the standards for acceptance.</p>
+    <p>
+      <a href="{{authserver_authorise_uri()}}"
+         title="Sign in to the system"
+         class="btn btn-primary">Sign in</a>
+      to get started.</p>
+  </div>
+  {%endif%}
+
+  {%endblock%}
+
+
+
+  {%block sidebarcontents%}
+  <div class="row">
+    <p>The data in Genenetwork is related to one species or another. Use the form
+      provided to select from existing species, or click on the
+      "Create a New Species" button if you cannot find the species you want to
+      work with.</p>
+  </div>
+  <div class="row">
+    <form id="frm-quick-navigation">
+      <legend>Quick Navigation</legend>
+      <div class="form-group">
+        <label for="fqn-species-id">Species</label>
+        <select name="species_id">
+          <option value="">Select species</option>
+        </select>
+      </div>
+    </form>
+  </div>
+  {%endblock%}
+
+
+  {%block javascript%}
+  <script type="text/javascript" src="/static/js/species.js"></script>
+  {%endblock%}
diff --git a/uploader/templates/sui-login.html b/uploader/templates/sui-login.html
new file mode 120000
index 0000000..733bbd3
--- /dev/null
+++ b/uploader/templates/sui-login.html
@@ -0,0 +1 @@
+sui-index.html
\ No newline at end of file