about summary refs log tree commit diff
path: root/uploader
diff options
context:
space:
mode:
Diffstat (limited to 'uploader')
-rw-r--r--uploader/__init__.py24
-rw-r--r--uploader/background_jobs.py5
-rw-r--r--uploader/base_routes.py36
-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.py42
-rw-r--r--uploader/phenotypes/models.py213
-rw-r--r--uploader/phenotypes/views.py440
-rw-r--r--uploader/platforms/views.py2
-rw-r--r--uploader/population/models.py22
-rw-r--r--uploader/population/rqtl2.py2
-rw-r--r--uploader/population/views.py27
-rw-r--r--uploader/publications/datatables.py52
-rw-r--r--uploader/publications/models.py42
-rw-r--r--uploader/publications/pubmed.py7
-rw-r--r--uploader/publications/views.py121
-rw-r--r--uploader/route_utils.py50
-rw-r--r--uploader/samples/models.py3
-rw-r--r--uploader/samples/views.py10
-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/styles.css6
-rw-r--r--uploader/static/css/theme.css81
-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/templates/background-jobs/default-success-page.html17
-rw-r--r--uploader/templates/base.html2
-rw-r--r--uploader/templates/flash_messages.html12
-rw-r--r--uploader/templates/macro-forms.html9
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-base.html4
-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/review-job-data.html3
-rw-r--r--uploader/templates/phenotypes/view-dataset.html165
-rw-r--r--uploader/templates/phenotypes/view-phenotype.html54
-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.html138
-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.html194
-rw-r--r--uploader/templates/publications/index.html12
-rw-r--r--uploader/templates/publications/view-publication.html22
-rw-r--r--uploader/templates/samples/list-samples.html36
-rw-r--r--uploader/templates/samples/upload-samples.html2
-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.html123
66 files changed, 2480 insertions, 785 deletions
diff --git a/uploader/__init__.py b/uploader/__init__.py
index 97b9af5..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,10 +65,12 @@ 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())
-    logging.getLogger("uploader.publications.models").setLevel(loglevel)
+    for module in modules:
+        _logger = logging.getLogger(module)
+        _logger.setLevel(loglevel)
 
 
 def create_app(config: Optional[dict] = None):
@@ -105,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..d33c498 100644
--- a/uploader/background_jobs.py
+++ b/uploader/background_jobs.py
@@ -56,7 +56,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 +76,7 @@ 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("background-jobs/default-success-page.html", job=job)
 
 
 error_handler = partial(handler, handler_type="error")
diff --git a/uploader/base_routes.py b/uploader/base_routes.py
index 74a3b90..80a15a0 100644
--- a/uploader/base_routes.py
+++ b/uploader/base_routes.py
@@ -1,15 +1,23 @@
 """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.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 +32,29 @@ 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")
+    if not bool(streamlined_ui):# TODO: Remove this section
+        return render_template(
+            "index.html" if user_logged_in() else "login.html",
+            gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], "/intro"))
+
+    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-index.html",# TODO: Rename: sui-index.html, sui_base.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..05f8542 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,8 +29,8 @@ 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("/")
@@ -60,36 +56,14 @@ def authorisation_code():
         return redirect("/")
 
     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")
diff --git a/uploader/phenotypes/models.py b/uploader/phenotypes/models.py
index a6ee694..af06376 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(
+        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..2afd8a3 100644
--- a/uploader/phenotypes/views.py
+++ b/uploader/phenotypes/views.py
@@ -1,61 +1,53 @@
-"""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 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 +58,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")
 
@@ -242,11 +234,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 +242,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 +356,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 +375,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 +389,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,10 +414,7 @@ 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(
@@ -462,7 +449,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(
@@ -640,12 +626,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 +643,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 +657,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 +682,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 +841,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 +941,211 @@ 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("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")
-
+            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):
+    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"]))
 
-    return redirect(url_for("background-jobs.job_status",
-                            job_id=job_id,
-                            job_type="phenotype-bulk-edit"))
+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..ba0f0ef 100644
--- a/uploader/platforms/views.py
+++ b/uploader/platforms/views.py
@@ -4,11 +4,11 @@ from gn_libs.mysqldb import database_connection
 from flask import (
     flash,
     request,
-    url_for,
     redirect,
     Blueprint,
     current_app as app)
 
+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
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..8d4ceb7 100644
--- a/uploader/population/views.py
+++ b/uploader/population/views.py
@@ -7,18 +7,19 @@ 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.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
 from uploader.genotypes.views import genotypesbp
 from uploader.datautils import enumerate_sequence
 from uploader.phenotypes.views import phenotypesbp
+from uploader.phenotypes.models import datasets_by_population
 from uploader.expression_data.views import exprdatabp
 from uploader.species.models import all_species, species_by_id
 from uploader.monadic_requests import make_either_error_handler
@@ -100,7 +101,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 +154,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 +194,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 +213,18 @@ 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))
 
-        return render_template("populations/view-population.html",
+        return render_template(("populations/sui-view-population.html"
+                                if bool(streamlined_ui)
+                                else "populations/view-population.html"),
                                species=species,
                                population=population,
-                               activelink="view-population")
+                               **({"dataset": datasets[0]} if len(datasets) == 1 else {}),
+                               activelink="view-population",
+                               streamlined_ui=streamlined_ui,
+                               view_under_construction=request.args.get("view_under_construction", False))
diff --git a/uploader/publications/datatables.py b/uploader/publications/datatables.py
new file mode 100644
index 0000000..e07fafd
--- /dev/null
+++ b/uploader/publications/datatables.py
@@ -0,0 +1,52 @@
+"""Fetch data for datatables."""
+import logging
+from typing import Optional
+
+from MySQLdb.cursors import DictCursor
+
+from gn_libs.mysqldb import Connection, debug_query
+
+logger = logging.getLogger(__name__)
+
+def fetch_publications(
+        conn: Connection,
+        search: Optional[str] = None,
+        offset: int = 0,
+        limit: int = -1
+) -> tuple[dict, int, int, int]:
+    """Fetch publications from the database."""
+    _query = "SELECT * FROM Publication"
+    _count_query = "SELECT COUNT(*) FROM Publication"
+    _params = None
+    _where_clause = ""
+    _limit_clause = ""
+    if search is not None and bool(search):
+        _where_clause = ("WHERE PubMed_ID LIKE %s "
+                      "OR Authors LIKE %s "
+                      "OR Title LIKE %s")
+        _params = (f"%{search}%",) * 3
+
+    if limit > 0:
+        _limit_clause = f"LIMIT {limit} OFFSET {offset}"
+
+    with conn.cursor(cursorclass=DictCursor) as cursor:
+        cursor.execute("SELECT COUNT(*) FROM Publication")
+        _total_rows = int(cursor.fetchone()["COUNT(*)"])
+
+        cursor.execute(f"{_count_query} {_where_clause}", _params)
+        debug_query(cursor, logger)
+        _result = cursor.fetchone()
+        _total_filtered = int(_result["COUNT(*)"] if bool(_result) else 0)
+
+        cursor.execute(f"{_query} {_where_clause} {_limit_clause}", _params)
+        debug_query(cursor, logger)
+        _current_filtered = tuple(
+            {**dict(row), "index": idx}
+            for idx, row
+            in enumerate(cursor.fetchall(), start=offset+1))
+
+        return (
+            _current_filtered,
+            len(_current_filtered),
+            _total_filtered,
+            _total_rows)
diff --git a/uploader/publications/models.py b/uploader/publications/models.py
index 992ebfa..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,23 +71,25 @@ def update_publications(conn: Connection , publications: tuple[dict, ...]) -> tu
     return tuple()
 
 
-def fetch_publications(
-        conn: Connection,
-        search: Optional[str] = None
-) -> Iterable[dict]:
-    """Fetch publications from the database."""
-    _query = "SELECT * FROM Publication"
-    _params = None
-    if search is not None and bool(search):
-        _query = (f"{_query} "
-                  "WHERE CONCAT(PubMed_ID, ' ', Authors, ' ', Title) "
-                  "LIKE %s")
-        _params = (f"%{search}%",)
-    with conn.cursor(cursorclass=DictCursor) as cursor:
-        cursor.execute(_query, _params)
-        debug_query(_query, logger)
-        for row in cursor.fetchall():
-            yield dict(row)
+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:
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 63acf1b..4ec832f 100644
--- a/uploader/publications/views.py
+++ b/uploader/publications/views.py
@@ -1,26 +1,27 @@
 """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.flask_extensions import url_for
 from uploader.authorisation import require_login
+from uploader.route_utils import redirect_to_next
 
 from .models import (
-    fetch_publications,
+    delete_publications,
+    update_publications,
     fetch_publication_by_id,
     create_new_publications,
     fetch_publication_phenotypes)
 
-from gn_libs.debug import __pk__
+from .datatables import fetch_publications
 
 pubbp = Blueprint("publications", __name__)
 
@@ -29,29 +30,31 @@ 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("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
-    with (database_connection(app.config["SQL_URI"]) as conn,
-          conn.cursor(cursorclass=DictCursor) as cursor):
-        cursor.execute("SELECT COUNT(*) FROM Publication")
-        _totalrows = int(cursor.fetchone()["COUNT(*)"])
-        _publications = tuple({
-            **row, "index": idx
-        } for idx,row in enumerate(
-            fetch_publications(
-                conn, request.args["search[value]"]), start=1))
+    _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:
+        _publications, _current_rows, _totalfiltered, _totalrows = fetch_publications(
+            conn,
+            _search,
+            offset=_start,
+            limit=_length)
 
         return json.dumps({
-            "draw": request.args.get("draw"),
+            "draw": _page,
             "recordsTotal": _totalrows,
-            "recordsFiltered": len(_publications),
+            "recordsFiltered": _totalfiltered,
             "publications": _publications,
             "status": "success"
         })
@@ -62,9 +65,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),
+            publication=publication,
             linked_phenotypes=tuple(fetch_publication_phenotypes(
                 conn, publication_id)))
 
@@ -73,7 +82,7 @@ def view_publication(publication_id: int):
 @require_login
 def create_publication():
     """Create a new publication."""
-    if(request.method == "GET"):
+    if request.method == "GET":
         return render_template("publications/create-publication.html")
     form = request.form
     authors = form.get("publication-authors").encode("utf8")
@@ -83,7 +92,7 @@ def create_publication():
 
     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,
@@ -102,3 +111,75 @@ def create_publication():
     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(
+                "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(
+                "publications.view_publication", publication_id=publication_id))
+
+        if request.method == "GET":
+            return render_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 render_template("publications/delete-publication-success.html")
diff --git a/uploader/route_utils.py b/uploader/route_utils.py
index ce718fb..4449475 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,40 @@ 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."
+    try:
+        next_page = base64_decode_to_dict(request.args.get("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..f8baf7e 100644
--- a/uploader/samples/views.py
+++ b/uploader/samples/views.py
@@ -7,13 +7,13 @@ from pathlib import Path
 from redis import Redis
 from flask import (flash,
                    request,
-                   url_for,
                    redirect,
                    Blueprint,
                    current_app as app)
 
 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
@@ -96,7 +96,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,
@@ -159,7 +159,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 +172,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
@@ -251,7 +251,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))
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..20acd01 100644
--- a/uploader/species/views.py
+++ b/uploader/species/views.py
@@ -4,17 +4,19 @@ 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.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 +43,28 @@ 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(
+                ("species/sui-view-species.html"
+                 if bool(streamlined_ui)
+                 else "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/styles.css b/uploader/static/css/styles.css
index 826ac41..df50dec 100644
--- a/uploader/static/css/styles.css
+++ b/uploader/static/css/styles.css
@@ -179,3 +179,9 @@ table.dataTable thead th, table.dataTable tfoot th{
 table.dataTable tbody tr.selected td {
     background-color: #ffee99 !important;
 }
+
+.form-group {
+    margin-bottom: 2em;
+    padding-bottom: 0.2em;
+    border-bottom: solid gray 1px;
+}
diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css
new file mode 100644
index 0000000..2acce5f
--- /dev/null
+++ b/uploader/static/css/theme.css
@@ -0,0 +1,81 @@
+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;
+}
+
+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;
+}
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/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/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/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/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 928fb84..9909c20 100644
--- a/uploader/templates/phenotypes/add-phenotypes-base.html
+++ b/uploader/templates/phenotypes/add-phenotypes-base.html
@@ -135,9 +135,11 @@
                   dataSrc: "publications"
               },
               select: "single",
+              paging: true,
               scrollY: 700,
-              paging: false,
               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/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/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/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..cc01c9e
--- /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["FullName"]}}
+  </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..3bf8d0d
--- /dev/null
+++ b/uploader/templates/populations/sui-view-population.html
@@ -0,0 +1,138 @@
+{%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)
+        under consideration.</p>
+      <p>This is a convenience feature for when you want to upload phenotypes to
+        the system, but do not have the genotypes data ready yet.</p>
+      <p>Please click the button below to provide the samples that will be used
+        in your data.</p>
+      <a href="{{url_for('species.populations.samples.list_samples',
+               species_id=species.SpeciesId,
+               population_id=population.Id)}}"
+         title="Upload samples for population '{{population['Name']}}'"
+         class="btn btn-primary">Upload Samples</a>
+    </div>
+
+    <div class="tab-pane fade show active"
+         id="phenotypes-content"
+         role="tabpanel"
+         aria-labelledby="phenotypes-content-tab">
+      <p>Upload and manage phenotypes and publications for population
+        "<em>{{population.FullName}} ({{population.Name}})</em>" of species
+        "<em>{{species.FullName}} ({{species.Name}})</em>".</p>
+
+      <p class="text-danger">Tabs will not work nicely here. Maybe present
+        options e.g.:
+      </p>
+      <div class="row">
+        <div class="col">
+          <a href="{{url_for('species.populations.phenotypes.view_dataset',
+                   species_id=species.SpeciesId,
+                   population_id=population.Id,
+                   dataset_id=dataset.Id)}}"
+             title="Upload phenotype data for population '{{population['Name']}}'"
+             class="btn btn-primary">Upload new Phenotypes</a>
+          <!-- Go straight to upload form(s). -->
+        </div>
+        <div class="col">
+          <a href="#"
+             title="List all existing phenotypes for this population."
+             class="btn btn-info not-implemented">list existing phenotypes</a>
+          <!-- Means and QTLReaper will be computed in this page. -->
+        </div>
+        <div class="col">
+          <a href="#"
+             title="List all existing publications for this population."
+             class="btn btn-info not-implemented">list existing publications</a>
+          <!-- Maybe, actually filter publications by population? -->
+          <!-- Provide other features for publications on loaded page. -->
+        </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%}
+
+
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..540ecf1
--- /dev/null
+++ b/uploader/templates/publications/edit-publication.html
@@ -0,0 +1,194 @@
+{%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, **request.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"
+                 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/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/upload-samples.html b/uploader/templates/samples/upload-samples.html
index 25d3290..6422094 100644
--- a/uploader/templates/samples/upload-samples.html
+++ b/uploader/templates/samples/upload-samples.html
@@ -66,7 +66,7 @@
     <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"
+	     accept="text/csv, text/tab-separated-values, text/plain"
 	     class="form-control" />
     </div>
 
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..888823f
--- /dev/null
+++ b/uploader/templates/sui-index.html
@@ -0,0 +1,123 @@
+{%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">
+  <div class="row">
+    <h2 class="heading">Species</h2>
+
+    <p>Select the species you want to work with.</p>
+  </div>
+</div>
+
+<div class="row">
+  <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>
+
+{%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%}