about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--scripts/load_phenotypes_to_db.py49
-rw-r--r--scripts/rqtl2/phenotypes_qc.py7
-rw-r--r--scripts/run_qtlreaper.py20
-rw-r--r--uploader/__init__.py9
-rw-r--r--uploader/default_settings.py3
-rw-r--r--uploader/genotypes/models.py24
-rw-r--r--uploader/genotypes/views.py50
-rw-r--r--uploader/phenotypes/models.py30
-rw-r--r--uploader/publications/models.py14
-rw-r--r--uploader/publications/views.py6
-rw-r--r--uploader/request_checks.py54
-rw-r--r--uploader/templates/genotypes/create-dataset.html6
-rw-r--r--uploader/templates/genotypes/index.html32
-rw-r--r--uploader/templates/genotypes/list-genotypes.html179
-rw-r--r--uploader/templates/genotypes/view-dataset.html5
-rw-r--r--uploader/templates/phenotypes/create-dataset.html2
-rw-r--r--uploader/templates/phenotypes/view-dataset.html26
-rw-r--r--uploader/templates/publications/create-publication.html31
18 files changed, 354 insertions, 193 deletions
diff --git a/scripts/load_phenotypes_to_db.py b/scripts/load_phenotypes_to_db.py
index 9b70fed..31eb715 100644
--- a/scripts/load_phenotypes_to_db.py
+++ b/scripts/load_phenotypes_to_db.py
@@ -7,6 +7,7 @@ import logging
 import argparse
 from pathlib import Path
 from zipfile import ZipFile
+from datetime import datetime
 from typing import Any, Iterable
 from urllib.parse import urljoin
 from functools import reduce, partial
@@ -204,6 +205,8 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
         dataset,
         xrefdata):
     """Grant the user access to their data."""
+    logger.info("Updating authorisation for the data.")
+    logger.debug("Resource details for the authorisation: %s", resource_details)
     authserver, token = auth_details
     _tries = 0
     _delay = 1
@@ -215,14 +218,14 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
         return urljoin(authserver, endpoint)
 
     def __fetch_user_details__():
-        logger.debug("… Fetching user details")
+        logger.info("… Fetching user details")
         return mrequests.get(
             authserveruri("/auth/user/"),
             headers=headers
         )
 
     def __link_data__(user):
-        logger.debug("… linking uploaded data to user's group")
+        logger.info("… linking uploaded data to user's group")
         return mrequests.post(
             authserveruri("/auth/data/link/phenotype"),
             headers=headers,
@@ -245,7 +248,7 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
             }).then(lambda ld_results: (user, ld_results))
 
     def __fetch_phenotype_category_details__(user, linkeddata):
-        logger.debug("… fetching phenotype category details")
+        logger.info("… fetching phenotype category details")
         return mrequests.get(
             authserveruri("/auth/resource/categories"),
             headers=headers
@@ -258,7 +261,7 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
         )
 
     def __create_resource__(user, linkeddata, category):
-        logger.debug("… creating authorisation resource object")
+        logger.info("… creating authorisation resource object")
         return mrequests.post(
             authserveruri("/auth/resource/create"),
             headers=headers,
@@ -269,7 +272,7 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
             }).then(lambda cr_results: (user, linkeddata, cr_results))
 
     def __attach_data_to_resource__(user, linkeddata, resource):
-        logger.debug("… attaching data to authorisation resource object")
+        logger.info("… attaching data to authorisation resource object")
         return mrequests.post(
             authserveruri("/auth/resource/data/link"),
             headers=headers,
@@ -286,8 +289,8 @@ def update_auth(# pylint: disable=[too-many-locals,too-many-positional-arguments
             # This is hacky. If the auth already exists, something went wrong
             # somewhere.
             # This needs investigation to recover correctly.
-            logger.info(
-                "The authorisation for the data was already set up.")
+            logger.error(
+                "Error: The authorisation for the data was already set up.")
             return 0
         logger.error("ERROR: Updating the authorisation for the data failed.")
         logger.debug(
@@ -459,6 +462,25 @@ if __name__ == "__main__":
         logging.getLogger("uploader.phenotypes.models").setLevel(log_level)
 
 
+    def __parse_resource_details__(meta) -> dict:
+        """Parse out details regarding the wrapper resource from the metadata."""
+        _key_mappings_ = {
+            # allow both 'data_*' and 'data*' for the metadata.
+            "data_description": "description",
+            "datadescription": "description"
+        }
+        return {
+            "resource_name": meta.get(
+                "dataname",
+                meta.get("data_name",
+                         "Unnamed phenotypes - " + datetime.now().isoformat())),
+            "resource_metadata": {
+                rkey: meta[mkey]
+                for mkey, rkey in _key_mappings_.items() if mkey in meta
+            }
+        }
+
+
     def main():
         """Entry-point for this script."""
         args = parse_args()
@@ -515,19 +537,6 @@ if __name__ == "__main__":
         logger.info("Updating authorisation.")
         _job_metadata = job["metadata"]
 
-        def __parse_resource_details__(meta) -> dict:
-            _key_mappings_ = {
-                "data_description": "description",
-            }
-            return {
-                "resource_name": _job_metadata["dataname"],
-                "resource_metadata": {
-                    rkey: meta[mkey]
-                    for mkey, rkey in _key_mappings_.items()
-                    if mkey in meta
-                }
-            }
-
         return update_auth((_job_metadata["authserver"],
                             _job_metadata["token"]),
                            __parse_resource_details__(_job_metadata),
diff --git a/scripts/rqtl2/phenotypes_qc.py b/scripts/rqtl2/phenotypes_qc.py
index 72d6c83..084c876 100644
--- a/scripts/rqtl2/phenotypes_qc.py
+++ b/scripts/rqtl2/phenotypes_qc.py
@@ -198,7 +198,7 @@ def qc_phenocovar_file(
                     "-",
                     "-",
                     (f"File {filepath.name} is missing the {heading} heading "
-                     "in the header line."))),)
+                     "in the header row/line."))),)
 
         def collect_errors(errors_and_linecount, line):
             _errs, _lc = errors_and_linecount
@@ -312,8 +312,9 @@ def qc_pheno_file(# pylint: disable=[too-many-locals, too-many-arguments, too-ma
                 "header row",
                 "-",
                 ", ".join(_absent),
-                ("The following phenotype names do not exist in any of the "
-                 f"provided phenocovar files: ({', '.join(_absent)})"))),)
+                ("The following trait names/identifiers do not exist in any of "
+                 "the provided descriptions/covariates files: "
+                 f"({', '.join(_absent)})"))),)
 
         def collect_errors(errors_and_linecount, line):
             _errs, _lc = errors_and_linecount
diff --git a/scripts/run_qtlreaper.py b/scripts/run_qtlreaper.py
index ab19da0..2269ea6 100644
--- a/scripts/run_qtlreaper.py
+++ b/scripts/run_qtlreaper.py
@@ -6,6 +6,7 @@ import time
 import secrets
 import logging
 import subprocess
+import multiprocessing
 from pathlib import Path
 from functools import reduce
 from typing import Union, Iterator
@@ -147,13 +148,17 @@ def dispatch(args: Namespace) -> int:
 
             _qtlreaper_main_output = args.working_dir.joinpath(
                 f"main-output-{secrets.token_urlsafe(15)}.tsv")#type: ignore[attr-defined]
+            _qtlreaper_permu_output = args.working_dir.joinpath(
+                f"permu-output-{secrets.token_urlsafe(15)}.tsv")
             logger.debug("Main output filename: %s", _qtlreaper_main_output)
             with subprocess.Popen(
                     ("qtlreaper",
                      "--n_permutations", "1000",
                      "--geno", _genofile,
                      "--traits", _traitsfile,
-                     "--main_output", _qtlreaper_main_output),
+                     "--main_output", _qtlreaper_main_output,
+                     "--permu_output", _qtlreaper_permu_output,
+                     "--threads", str(int(1+(multiprocessing.cpu_count()/2)))),
                     env=({**os.environ, "RUST_BACKTRACE": "full"}
                          if logger.getEffectiveLevel() == logging.DEBUG
                          else dict(os.environ))) as _qtlreaper:
@@ -172,8 +177,17 @@ def dispatch(args: Namespace) -> int:
             logger.debug("Cleaning up temporary files.")
 
             # short-circuits to delete file if exists
-            _traitsfile.exists() and _traitsfile.unlink()
-            _qtlreaper_main_output.exists() and _qtlreaper_main_output.unlink()
+            if _traitsfile.exists():
+                _traitsfile.unlink()
+                logger.info("Deleted generated traits' file for QTLReaper.")
+
+            if _qtlreaper_main_output.exists():
+                _qtlreaper_main_output.unlink()
+                logger.info("Deleted QTLReaper's main output file.")
+
+            if _qtlreaper_permu_output.exists():
+                _qtlreaper_permu_output.unlink()
+                logger.info("Deleted QTLReaper's permutations file.")
 
             if _qtlreaper.returncode != 0:
                 return _qtlreaper.returncode
diff --git a/uploader/__init__.py b/uploader/__init__.py
index e00c726..afaa78d 100644
--- a/uploader/__init__.py
+++ b/uploader/__init__.py
@@ -131,13 +131,8 @@ def create_app(config: Optional[dict] = None):
         default_timeout=int(app.config["SESSION_FILESYSTEM_CACHE_TIMEOUT"]))
 
     setup_logging(app)
-    setup_modules_logging(app.logger, (
-        "uploader.base_routes",
-        "uploader.flask_extensions",
-        "uploader.publications.models",
-        "uploader.publications.datatables",
-        "uploader.phenotypes.models",
-        "uploader.phenotypes.views"))
+    setup_modules_logging(
+        app.logger, tuple(app.config.get("LOGGABLE_MODULES", [])))
 
     # setup jinja2 symbols
     app.add_template_global(user_logged_in)
diff --git a/uploader/default_settings.py b/uploader/default_settings.py
index 6381a67..04e1c0a 100644
--- a/uploader/default_settings.py
+++ b/uploader/default_settings.py
@@ -39,3 +39,6 @@ JWKS_DELETION_AGE_DAYS = 14 # Days (from creation) to keep a JWK around before d
 
 ## --- Feature flags ---
 FEATURE_FLAGS_HTTP: list[str] = []
+
+## --- Modules for which to log output ---
+LOGGABLE_MODULES: list[str] = []
diff --git a/uploader/genotypes/models.py b/uploader/genotypes/models.py
index 4c3e634..34d2cfe 100644
--- a/uploader/genotypes/models.py
+++ b/uploader/genotypes/models.py
@@ -31,16 +31,28 @@ def genotype_markers(
         species_id: int,
         offset: int = 0,
         limit: Optional[int] = None
-) -> tuple[dict, ...]:
+) -> tuple[tuple[dict, ...], int]:
     """Retrieve markers from the database."""
-    _query = "SELECT * FROM Geno WHERE SpeciesId=%s"
-    if bool(limit) and limit > 0:# type: ignore[operator]
-        _query = _query + f" LIMIT {limit} OFFSET {offset}"
+    _query_template = (
+        "SELECT %%COLS%% FROM Geno AS gno "
+        "WHERE gno.SpeciesId=%s "
+        "%%LIMIT%%")
 
     with conn.cursor(cursorclass=DictCursor) as cursor:
-        cursor.execute(_query, (species_id,))
+        cursor.execute(
+            _query_template.replace("%%LIMIT%%", "").replace(
+                "%%COLS%%", "COUNT(gno.Id) AS total_records"),
+            (species_id,))
+        _total_records = cursor.fetchone()["total_records"]
+        cursor.execute(
+            _query_template.replace("%%COLS%%", "gno.*").replace(
+                "%%LIMIT%%",
+                (f"LIMIT {int(limit)} OFFSET {int(offset)}"
+                 if bool(limit) and limit > 0
+                 else "")),
+            (species_id,))
         debug_query(cursor, app.logger)
-        return tuple(dict(row) for row in cursor.fetchall())
+        return tuple(dict(row) for row in cursor.fetchall()), _total_records
 
 
 def genotype_dataset(
diff --git a/uploader/genotypes/views.py b/uploader/genotypes/views.py
index 3fa2131..f27671c 100644
--- a/uploader/genotypes/views.py
+++ b/uploader/genotypes/views.py
@@ -6,6 +6,7 @@ from pymonad.either import Left, Right, Either
 from gn_libs.mysqldb import database_connection
 from flask import (flash,
                    request,
+                   jsonify,
                    redirect,
                    Blueprint,
                    render_template,
@@ -19,8 +20,8 @@ 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.request_checks import with_species, with_population
 from uploader.population.models import population_by_species_and_id
+from uploader.request_checks import with_species, with_dataset, with_population
 
 from .models import (genotype_markers,
                      genotype_dataset,
@@ -56,34 +57,31 @@ def list_genotypes(species: dict, population: dict, **kwargs):# pylint: disable=
 
 
 @genotypesbp.route(
-    "/<int:species_id>/populations/<int:population_id>/genotypes/list-markers",
+    "/<int:species_id>/populations/<int:population_id>/genotypes/<int:dataset_id>/list-markers",
     methods=["GET"])
 @require_login
-@with_population(species_redirect_uri="species.list_species",
-                 redirect_uri="species.populations.list_species_populations")
-def list_markers(
-        species: dict,
-        population: dict,
-        **kwargs
-):# pylint: disable=[unused-argument]
-    """List a species' genetic markers."""
+@with_species(redirect_uri="species.populations.genotypes.list_genotypes")
+def list_markers(species: dict, **_kwargs):
+    """List the markers that exist for this species."""
+    args = request.args
+    offset = int(args.get("start") or 0)
     with database_connection(app.config["SQL_URI"]) as conn:
-        start_from = max(safe_int(request.args.get("start_from") or 0), 0)
-        count = safe_int(request.args.get("count") or 20)
-        return render_template("genotypes/list-markers.html",
-                               species=species,
-                               population=population,
-                               total_markers=genotype_markers_count(
-                                   conn, species["SpeciesId"]),
-                               start_from=start_from,
-                               count=count,
-                               markers=enumerate_sequence(
-                                   genotype_markers(conn,
-                                                    species["SpeciesId"],
-                                                    offset=start_from,
-                                                    limit=count),
-                                   start=start_from+1),
-                               activelink="list-markers")
+        markers, total_records = genotype_markers(
+            conn,
+            species["SpeciesId"],
+            offset=offset,
+            limit=int(args.get("length") or 0))
+        return jsonify({
+            **({"draw": int(args.get("draw"))}
+               if bool(args.get("draw") or False)
+               else {}),
+            "recordsTotal": total_records,
+            "recordsFiltered": len(markers),
+            "markers": tuple({**marker, "index": idx}
+                             for idx, marker in
+                             enumerate(markers, start=offset+1))
+        })
+
 
 @genotypesbp.route(
     "/<int:species_id>/populations/<int:population_id>/genotypes/datasets/"
diff --git a/uploader/phenotypes/models.py b/uploader/phenotypes/models.py
index 3946a0f..3d656d2 100644
--- a/uploader/phenotypes/models.py
+++ b/uploader/phenotypes/models.py
@@ -96,20 +96,34 @@ def dataset_phenotypes(# pylint: disable=[too-many-arguments, too-many-positiona
         xref_ids: tuple[int, ...] = tuple()
 ) -> tuple[dict, ...]:
     """Fetch the actual phenotypes."""
-    _query = (
-        "SELECT pheno.*, pxr.Id AS xref_id, pxr.InbredSetId, ist.InbredSetCode "
+    _narrow_by_ids = (
+            f" AND pxr.Id IN ({', '.join(['%s'] * len(xref_ids))})"
+            if len(xref_ids) > 0 else "")
+    _narrow_by_limit = (
+        f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")
+    _pub_query = (
+        "SELECT pub.* "
+        "FROM PublishXRef AS pxr "
+        "INNER JOIN  Publication AS pub ON pxr.PublicationId=pub.Id "
+        "WHERE pxr.InbredSetId=%s") + _narrow_by_ids
+    _pheno_query = ((
+        "SELECT pheno.*, pxr.Id AS xref_id, pxr.InbredSetId, pxr.PublicationId, "
+        "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 "
-        "WHERE pxr.InbredSetId=%s AND pf.Id=%s") + (
-            f" AND pxr.Id IN ({', '.join(['%s'] * len(xref_ids))})"
-            if len(xref_ids) > 0 else "") + (
-            f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")
+        "WHERE pxr.InbredSetId=%s AND pf.Id=%s") +
+                    _narrow_by_ids +
+                    _narrow_by_limit)
     with conn.cursor(cursorclass=DictCursor) as cursor:
-        cursor.execute(_query, (population_id, dataset_id) + xref_ids)
+        cursor.execute(_pub_query, (population_id,) + xref_ids)
         debug_query(cursor, logger)
-        return tuple(dict(row) for row in cursor.fetchall())
+        _pubs = {row["Id"]: dict(row) for row in cursor.fetchall()}
+        cursor.execute(_pheno_query, (population_id, dataset_id) + xref_ids)
+        debug_query(cursor, logger)
+        return tuple({**dict(row), "publication": _pubs[row["PublicationId"]]}
+                     for row in cursor.fetchall())
 
 
 def __phenotype_se__(cursor: BaseCursor, xref_id, dataids_and_strainids):
diff --git a/uploader/publications/models.py b/uploader/publications/models.py
index dcfa02b..d913144 100644
--- a/uploader/publications/models.py
+++ b/uploader/publications/models.py
@@ -101,6 +101,20 @@ def fetch_publication_by_id(conn: Connection, publication_id: int) -> dict:
         return dict(_res) if _res else {}
 
 
+def fetch_publications_by_ids(
+        conn: Connection, publications_ids: tuple[int, ...]
+) -> tuple[dict, ...]:
+    """Fetch publications with the given IDs."""
+    if len(publications_ids) == 0:
+        return tuple()
+
+    with conn.cursor(cursorclass=DictCursor) as cursor:
+        paramstr = ", ".join(["%s"] * len(publications_ids))
+        cursor.execute(f"SELECT * FROM Publication WHERE Id IN ({paramstr})",
+                       tuple(publications_ids))
+        return tuple(dict(row) for row in cursor.fetchall())
+
+
 def fetch_publication_phenotypes(
         conn: Connection, publication_id: int) -> Iterable[dict]:
     """Fetch all phenotypes linked to this publication."""
diff --git a/uploader/publications/views.py b/uploader/publications/views.py
index d9eb294..89e9f5d 100644
--- a/uploader/publications/views.py
+++ b/uploader/publications/views.py
@@ -1,5 +1,6 @@
 """Endpoints for publications"""
 import json
+import datetime
 
 from gn_libs.mysqldb import database_connection
 from flask import (
@@ -89,9 +90,12 @@ def create_publication():
     }
 
     if request.method == "GET":
+        now = datetime.datetime.now()
         return render_template(
             "publications/create-publication.html",
-            get_args=_get_args)
+            get_args=_get_args,
+            current_year=now.year,
+            current_month=now.strftime("%B"))
     form = request.form
     authors = form.get("publication-authors").encode("utf8")
     if authors is None or authors == "":
diff --git a/uploader/request_checks.py b/uploader/request_checks.py
index f1d8027..84935f9 100644
--- a/uploader/request_checks.py
+++ b/uploader/request_checks.py
@@ -2,14 +2,20 @@
 
 These are useful for reusability, and hence maintainability of the code.
 """
+import logging
+
+from typing import Callable
 from functools import wraps
 
-from gn_libs.mysqldb import database_connection
+from gn_libs.mysqldb import Connection, database_connection
 from flask import flash, url_for, redirect, current_app as app
 
 from uploader.species.models import species_by_id
 from uploader.population.models import population_by_species_and_id
 
+logger = logging.getLogger(__name__)
+
+
 def with_species(redirect_uri: str):
     """Ensure the species actually exists."""
     def __decorator__(function):
@@ -28,7 +34,7 @@ def with_species(redirect_uri: str):
                               "alert-danger")
                         return redirect(url_for(redirect_uri))
             except ValueError as _verr:
-                app.logger.debug(
+                logger.debug(
                     "Exception converting value to integer: %s",
                     kwargs.get("species_id"),
                     exc_info=True)
@@ -63,7 +69,7 @@ def with_population(species_redirect_uri: str, redirect_uri: str):
                               "alert-danger")
                         return select_population_uri
             except ValueError as _verr:
-                app.logger.debug(
+                logger.debug(
                     "Exception converting value to integer: %s",
                     kwargs.get("population_id"),
                     exc_info=True)
@@ -73,3 +79,45 @@ def with_population(species_redirect_uri: str, redirect_uri: str):
             return function(**{**kwargs, "population": population})
         return __with_population__
     return __decorator__
+
+
+def with_dataset(
+        species_redirect_uri: str,
+        population_redirect_uri: str,
+        redirect_uri: str,
+        dataset_by_id: Callable[
+            [Connection, int, int, int],
+            dict]
+):
+    """Ensure the dataset actually exists."""
+    def __decorator__(func):
+        @wraps(func)
+        @with_population(species_redirect_uri, population_redirect_uri)
+        def __with_dataset__(**kwargs):
+            try:
+                _spcid = int(kwargs["species_id"])
+                _popid = int(kwargs["population_id"])
+                _dsetid = int(kwargs.get("dataset_id"))
+                select_dataset_uri = redirect(url_for(
+                    redirect_uri, species_id=_spcid, population_id=_popid))
+                if not bool(_dsetid):
+                    flash("You need to select a valid 'dataset_id' value.",
+                          "alert-danger")
+                    return select_dataset_uri
+                with database_connection(app.config["SQL_URI"]) as conn:
+                    dataset = dataset_by_id(conn, _spcid, _popid, _dsetid)
+                    if not bool(dataset):
+                        flash("You must select a valid dataset.",
+                              "alert-danger")
+                        return select_dataset_uri
+            except ValueError as _verr:
+                logger.debug(
+                    "Exception converting 'dataset_id' to integer: %s",
+                    kwargs.get("dataset_id"),
+                    exc_info=True)
+                flash("Expected 'dataset_id' value to be an integer."
+                      "alert-danger")
+                return select_dataset_uri
+            return func(**{**kwargs, "dataset": dataset})
+        return __with_dataset__
+    return __decorator__
diff --git a/uploader/templates/genotypes/create-dataset.html b/uploader/templates/genotypes/create-dataset.html
index 5a6d3d2..ff174fb 100644
--- a/uploader/templates/genotypes/create-dataset.html
+++ b/uploader/templates/genotypes/create-dataset.html
@@ -41,9 +41,9 @@
       <small class="form-text text-muted">
         <p>This is a short representative, but constrained name for the genotype
           dataset.<br />
-          The field will only accept letters ('A-Za-z'), numbers (0-9), hyphens
-          and underscores. Any other character will cause the name to be
-          rejected.</p></small>
+          It is used internally by the Genenetwork system. Do not change this
+          value.</p>
+      </small>
     </div>
 
     <div class="form-group">
diff --git a/uploader/templates/genotypes/index.html b/uploader/templates/genotypes/index.html
deleted file mode 100644
index b50ebc5..0000000
--- a/uploader/templates/genotypes/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{%extends "genotypes/base.html"%}
-{%from "flash_messages.html" import flash_all_messages%}
-{%from "species/macro-select-species.html" import select_species_form%}
-
-{%block title%}Genotypes{%endblock%}
-
-{%block pagetitle%}Genotypes{%endblock%}
-
-
-{%block contents%}
-{{flash_all_messages()}}
-
-<div class="row">
-  <p>
-    This section allows you to upload genotype information for your experiments,
-    in the case that you have not previously done so.
-  </p>
-  <p>
-    We'll need to link the genotypes to the species and population, so do please
-    go ahead and select those in the next two steps.
-  </p>
-</div>
-
-<div class="row">
-  {{select_species_form(url_for("species.populations.genotypes.index"),
-  species)}}
-</div>
-{%endblock%}
-
-{%block javascript%}
-<script type="text/javascript" src="/static/js/species.js"></script>
-{%endblock%}
diff --git a/uploader/templates/genotypes/list-genotypes.html b/uploader/templates/genotypes/list-genotypes.html
index be297a4..131576f 100644
--- a/uploader/templates/genotypes/list-genotypes.html
+++ b/uploader/templates/genotypes/list-genotypes.html
@@ -54,12 +54,54 @@
 </div>
 
 <div class="row">
-  <h2 class="subheading">Genetic Markers</h2>
+  <h2>Genotype Dataset</h2>
+</div>
+
+{%if dataset is not none%}
+
+<div class="row">
+  <h3>Dataset Details</h3>
+  <table class="table">
+    <thead>
+      <tr>
+        <th>Name</th>
+        <th>Full Name</th>
+      </tr>
+    </thead>
 
-  <table id="tbl-genetic-markers" class="table compact stripe cell-border"
-         data-genetic-markers='{{genetic_markers | tojson}}'>
+    <tbody>
+      <tr>
+        <td>{{dataset.Name}}</td>
+        <td><a href="{{url_for('species.populations.genotypes.view_dataset',
+                     species_id=species.SpeciesId,
+                     population_id=population.Id,
+                     dataset_id=dataset.Id)}}"
+               title="View details regarding and manage dataset '{{dataset.FullName}}'"
+               target="_blank">
+            {{dataset.FullName}}</a></td>
+      </tr>
+    </tbody>
+  </table>
+
+  <p>
+    To see more information regarding this dataset (e.g. which markers have
+    sample allele data, the allele data itself, etc) click on the "Full Name"
+    link above.</p>
+</div>
+
+<div class="row">
+  <h3>Genotype Markers</h3>
+
+  <div class="row">
+    <p>
+      The table below lists all of the markers that exist for species
+      {{species.SpeciesName}} ({{species.FullName}}), regardless of whether
+      (or not) we have corresponding sample allele data for a particular marker.
+    </p>
+  <table id="tbl-genetic-markers" class="table compact stripe cell-border">
     <thead>
       <tr>
+        <th title="">#</th>
         <th title="">Index</th>
         <th title="">Marker Name</th>
         <th title="Chromosome">Chr</th>
@@ -78,57 +120,24 @@
         <td></td>
         <td></td>
         <td></td>
+        <td></td>
       </tr>
       {%endfor%}
     </tbody>
   </table>
 </div>
 
-<div class="row text-danger">
-  <h3>Some Important Concepts to Consider/Remember</h3>
-  <ul>
-    <li>Reference vs. Non-reference alleles</li>
-    <li>In <em>GenoCode</em> table, items are ordered by <strong>InbredSet</strong></li>
-  </ul>
-  <h3>Possible references</h3>
-  <ul>
-    <li>https://mr-dictionary.mrcieu.ac.uk/term/genotype/</li>
-    <li>https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7363099/</li>
-  </ul>
-</div>
+{%else%}
 
 <div class="row">
-  <h2>Genotype Datasets</h2>
-
-  <p>The genotype data is organised under various genotype datasets. You can
-    click on the link for the relevant dataset to view a little more information
-    about it.</p>
-
-  {%if dataset is not none%}
-    <table class="table">
-      <thead>
-        <tr>
-          <th>Name</th>
-          <th>Full Name</th>
-        </tr>
-      </thead>
-
-      <tbody>
-        <tr>
-          <td>{{dataset.Name}}</td>
-          <td><a href="{{url_for('species.populations.genotypes.view_dataset',
-                       species_id=species.SpeciesId,
-                       population_id=population.Id,
-                       dataset_id=dataset.Id)}}"
-                 title="View details regarding and manage dataset '{{dataset.FullName}}'">
-              {{dataset.FullName}}</a></td>
-        </tr>
-      </tbody>
-    </table>
-  {%else%}
+  <p>
+    Your genotype data will need to be under a dataset. Unfortunately there is
+    currently no dataset defined for this population.
+  </p>
+
   <p class="text-warning">
     <span class="glyphicon glyphicon-exclamation-sign"></span>
-    There is no genotype dataset defined for this population.
+    Click the button below to define the genotype dataset for this population.
   </p>
   <p>
     <a href="{{url_for('species.populations.genotypes.create_dataset',
@@ -137,35 +146,81 @@
        title="Create a new genotype dataset for the '{{population.FullName}}' population for the '{{species.FullName}}' species."
        class="btn btn-primary">
       create new genotype dataset</a></p>
-  {%endif%}
 </div>
-<div class="row text-warning">
-  <p>
-    <span class="glyphicon glyphicon-exclamation-sign"></span>
-    <strong>NOTE</strong>: Currently the GN2 (and related) system(s) expect a
-    single genotype dataset. If there is more than one, the system apparently
-    fails in unpredictable ways.
-  </p>
-  <p>Fix this to allow multiple datasets, each with a different assembly from
-    all the rest.</p>
+
+{%endif%}
+
+<div class="row">
+  <h2>Notes</h2>
+  <div class="row text-danger">
+    <h3>Genetic Markers: Some Important Concepts to Consider/Remember</h3>
+    <ul>
+      <li>Reference vs. Non-reference alleles</li>
+      <li>In <em>GenoCode</em> table, items are ordered by <strong>InbredSet</strong></li>
+    </ul>
+    <h3>Possible references</h3>
+    <ul>
+      <li>https://mr-dictionary.mrcieu.ac.uk/term/genotype/</li>
+      <li>https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7363099/</li>
+    </ul>
+  </div>
+
+  <div class="row text-warning">
+    <h3>Genotype Dataset</h3>
+    <p>
+      <span class="glyphicon glyphicon-exclamation-sign"></span>
+      <strong>NOTE</strong>: Currently the GN2 (and related) system(s) expect a
+      single genotype dataset per population. If there is more than one, the
+      system apparently fails in unpredictable ways.
+    </p>
+  </div>
 </div>
+
 {%endblock%}
 
 
 {%block javascript%}
 <script type="text/javascript">
+
   $(function() {
       var dtGeneticMarkers = buildDataTable(
           "#tbl-genetic-markers",
-          JSON.parse($("#tbl-genetic-markers").attr("data-genetic-markers")),
+          [],
           [
+              {
+                  data: function(marker) {
+                      return `<input type="checkbox" name="selected-markers" ` +
+                          `id="chk-selected-markers-${marker.Id}-${marker.GenoFreezeId}" ` +
+                          `value="${marker.Id}_${marker.GenoFreezeId}" ` +
+                          `class="chk-row-select" />`;
+                  }
+              },
               {data: 'index'},
-              {data: 'Name'},
-              {data: 'Chr'},
-              {data: 'Mb'},
-              {data: 'Source'},
-              {data: 'Source2'}
-          ]);
-  })
+              {data: "Name", searchable: true},
+              {data: "Chr", searchable: true},
+              {data: "Mb", searchable: true},
+              {data: "Source", searchable: true},
+              {data: "Source2", searchable: true}
+          ],
+          {
+              ajax: {
+                  url: "{{url_for('species.populations.genotypes.list_markers', species_id=species.SpeciesId, population_id=population.Id, dataset_id=dataset.Id)}}",
+                  dataSrc: "markers"
+              },
+              paging: true,
+              scroller: true,
+              scrollY: "50vh",
+              scrollCollapse: true,
+              layout: {
+                  top: "info",
+                  topStart: null,
+                  topEnd: null,
+                  bottom: null,
+                  bottomStart: null,
+                  bottomEnd: null
+              }
+          });
+  });
+
 </script>
 {%endblock%}
diff --git a/uploader/templates/genotypes/view-dataset.html b/uploader/templates/genotypes/view-dataset.html
index 1c4eccf..d95a8e3 100644
--- a/uploader/templates/genotypes/view-dataset.html
+++ b/uploader/templates/genotypes/view-dataset.html
@@ -46,8 +46,9 @@
 <div class="row">
   <h2>Genotype Data</h2>
 
-  <p class="text-danger">
-    Provide link to enable uploading of genotype data here.</p>
+  <div class="col" style="margin-bottom: 3px;">
+    <a href="#" class="btn btn-primary not-implemented">upload genotypes</a>
+  </div>
 </div>
 
 {%endblock%}
diff --git a/uploader/templates/phenotypes/create-dataset.html b/uploader/templates/phenotypes/create-dataset.html
index 6eced05..9963953 100644
--- a/uploader/templates/phenotypes/create-dataset.html
+++ b/uploader/templates/phenotypes/create-dataset.html
@@ -49,7 +49,7 @@
              class="form-control"
              {%endif%}
              required="required"
-             disabled="disabled" />
+             readonly="readonly" />
       <small class="form-text text-muted">
         <p>A short representative name for the dataset.</p>
         <p>Recommended: Use the population name and append "Publish" at the end.
diff --git a/uploader/templates/phenotypes/view-dataset.html b/uploader/templates/phenotypes/view-dataset.html
index 3bb2586..fc84757 100644
--- a/uploader/templates/phenotypes/view-dataset.html
+++ b/uploader/templates/phenotypes/view-dataset.html
@@ -148,14 +148,36 @@
                       return `<a href="${url.toString()}" target="_blank">` +
                           `${pheno.InbredSetCode}_${pheno.xref_id}` +
                           `</a>`;
-                  }
+                  },
+                  title: "Record",
+                  visible: true,
+                  searchable: true
               },
               {
                   data: function(pheno) {
                       return (pheno.Post_publication_description ||
                               pheno.Original_description ||
                               pheno.Pre_publication_description);
-                  }
+                  },
+                  title: "Description",
+                  visible: true,
+                  searchable: true
+              },
+              {
+                  data: function(pheno) {
+                      return pheno.publication.Title;
+                  },
+                  title: "Publication Title",
+                  visible: false,
+                  searchable: true
+              },
+              {
+                  data: function(pheno) {
+                      return pheno.publication.Authors;
+                  },
+                  title: "Authors",
+                  visible: false,
+                  searchable: true
               }
           ],
           {
diff --git a/uploader/templates/publications/create-publication.html b/uploader/templates/publications/create-publication.html
index fb0127d..da5889e 100644
--- a/uploader/templates/publications/create-publication.html
+++ b/uploader/templates/publications/create-publication.html
@@ -91,22 +91,22 @@
              class="col-sm-2 col-form-label">
         Month</label>
       <div class="col-sm-4">
-        <select class="form-control"
+        <select class="form-select"
                 id="select-publication-month"
                 name="publication-month">
           <option value="">Select a month</option>
-          <option value="january">January</option>
-          <option value="february">February</option>
-          <option value="march">March</option>
-          <option value="april">April</option>
-          <option value="may">May</option>
-          <option value="june">June</option>
-          <option value="july">July</option>
-          <option value="august">August</option>
-          <option value="september">September</option>
-          <option value="october">October</option>
-          <option value="november">November</option>
-          <option value="december">December</option>
+          <option {%if current_month | lower == "january"%}selected="selected"{%endif%}value="january">January</option>
+          <option {%if current_month | lower == "february"%}selected="selected"{%endif%}value="february">February</option>
+          <option {%if current_month | lower == "march"%}selected="selected"{%endif%}value="march">March</option>
+          <option {%if current_month | lower == "april"%}selected="selected"{%endif%}value="april">April</option>
+          <option {%if current_month | lower == "may"%}selected="selected"{%endif%}value="may">May</option>
+          <option {%if current_month | lower == "june"%}selected="selected"{%endif%}value="june">June</option>
+          <option {%if current_month | lower == "july"%}selected="selected"{%endif%}value="july">July</option>
+          <option {%if current_month | lower == "august"%}selected="selected"{%endif%}value="august">August</option>
+          <option {%if current_month | lower == "september"%}selected="selected"{%endif%}value="september">September</option>
+          <option {%if current_month | lower == "october"%}selected="selected"{%endif%}value="october">October</option>
+          <option {%if current_month | lower == "november"%}selected="selected"{%endif%}value="november">November</option>
+          <option {%if current_month | lower == "december"%}selected="selected"{%endif%}value="december">December</option>
         </select>
         <span class="form-text text-muted">Month of publication</span>
       </div>
@@ -119,7 +119,10 @@
                id="txt-publication-year"
                name="publication-year"
                class="form-control"
-               min="1960" />
+               min="1960"
+               max="{{current_year}}"
+               value="{{current_year or ''}}"
+               required="required" />
         <span class="form-text text-muted">Year of publication</span>
       </div>
     </div>