diff options
33 files changed, 821 insertions, 137 deletions
diff --git a/uploader/__init__.py b/uploader/__init__.py index 347f170..1af159b 100644 --- a/uploader/__init__.py +++ b/uploader/__init__.py @@ -9,6 +9,7 @@ from flask_session import Session from uploader.oauth2.client import user_logged_in, authserver_authorise_uri +from . import session from .base_routes import base from .species import speciesbp from .dbinsert import dbinsertbp @@ -76,6 +77,7 @@ def create_app(): 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") Session(app) diff --git a/uploader/base_routes.py b/uploader/base_routes.py index 88247b2..a20b7ff 100644 --- a/uploader/base_routes.py +++ b/uploader/base_routes.py @@ -1,5 +1,6 @@ """Basic routes required for all pages""" import os +from urllib.parse import urljoin from flask import ( Blueprint, @@ -23,7 +24,9 @@ def favicon(): @base.route("/", methods=["GET"]) def index(): """Load the landing page""" - return render_template("index.html" if user_logged_in() else "login.html") + return render_template("index.html" if user_logged_in() else "login.html", + gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], + "/intro")) def appenv(): """Get app's guix environment path.""" diff --git a/uploader/datautils.py b/uploader/datautils.py index b95a9e0..2ee079d 100644 --- a/uploader/datautils.py +++ b/uploader/datautils.py @@ -1,7 +1,14 @@ """Generic data utilities: Rename module.""" import math +from typing import Sequence from functools import reduce +def enumerate_sequence(seq: Sequence[dict], start:int = 1) -> Sequence[dict]: + """Enumerate sequence beginning at 1""" + return tuple({**item, "sequence_number": seqno} + for seqno, item in enumerate(seq, start=start)) + + def order_by_family(items: tuple[dict, ...], family_key: str = "Family", order_key: str = "FamilyOrderId") -> list: @@ -19,3 +26,13 @@ def order_by_family(items: tuple[dict, ...], return sorted(tuple(reduce(__order__, items, {}).items()), key=lambda item: item[0][0]) + + +def safe_int(val: str) -> int: + """ + Convert val into an integer: if val cannot be converted, return a zero. + """ + try: + return int(val) + except ValueError: + return 0 diff --git a/uploader/genotypes/__init__.py b/uploader/genotypes/__init__.py new file mode 100644 index 0000000..d0025d6 --- /dev/null +++ b/uploader/genotypes/__init__.py @@ -0,0 +1 @@ +"""The Genotypes module.""" diff --git a/uploader/genotypes/models.py b/uploader/genotypes/models.py new file mode 100644 index 0000000..53c5fb8 --- /dev/null +++ b/uploader/genotypes/models.py @@ -0,0 +1,41 @@ +"""Functions for handling genotypes.""" +from typing import Optional + +import MySQLdb as mdb +from MySQLdb.cursors import DictCursor + +from uploader.db_utils import debug_query + +def genocode_by_population( + conn: mdb.Connection, population_id: int) -> tuple[dict, ...]: + """Get the allele/genotype codes.""" + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute("SELECT * FROM GenoCode WHERE InbredSetId=%s", + (population_id,)) + return tuple(dict(item) for item in cursor.fetchall()) + + +def genotype_markers_count(conn: mdb.Connection, species_id: int) -> int: + """Find the total count of the genotype markers for a species.""" + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute( + "SELECT COUNT(Name) AS markers_count FROM Geno WHERE SpeciesId=%s", + (species_id,)) + return int(cursor.fetchone()["markers_count"]) + + +def genotype_markers( + conn: mdb.Connection, + species_id: int, + offset: int = 0, + limit: Optional[int] = None +) -> tuple[dict, ...]: + """Retrieve markers from the database.""" + _query = "SELECT * FROM Geno WHERE SpeciesId=%s" + if bool(limit) and limit > 0: + _query = _query + f" LIMIT {limit} OFFSET {offset}" + + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute(_query, (species_id,)) + debug_query(cursor) + return tuple(dict(row) for row in cursor.fetchall()) diff --git a/uploader/genotypes/views.py b/uploader/genotypes/views.py new file mode 100644 index 0000000..2ff9965 --- /dev/null +++ b/uploader/genotypes/views.py @@ -0,0 +1,127 @@ +"""Views for the genotypes.""" +from flask import (flash, + request, + url_for, + redirect, + Blueprint, + render_template, + current_app as app) + +from uploader.authorisation import require_login +from uploader.db_utils import database_connection +from uploader.species.models import all_species, species_by_id +from uploader.datautils import safe_int, order_by_family, enumerate_sequence +from uploader.population.models import (populations_by_species, + population_by_species_and_id) + +from .models import (genotype_markers, + genotype_markers_count, + genocode_by_population) + +genotypesbp = Blueprint("genotypes", __name__) + +@genotypesbp.route("populations/genotypes", methods=["GET"]) +@require_login +def index(): + """Direct entry-point for genotypes.""" + with database_connection(app.config["SQL_URI"]) as conn: + if not bool(request.args.get("species_id")): + return render_template("genotypes/index.html", + species=order_by_family(all_species(conn)), + activelink="genotypes") + species = species_by_id(conn, request.args.get("species_id")) + if not bool(species): + flash(f"Could not find species with ID '{request.args.get('species_id')}'!", + "alert-danger") + return redirect(url_for("species.populations.genotypes.index")) + return redirect(url_for("species.populations.genotypes.select_population", + species_id=species["SpeciesId"])) + + +@genotypesbp.route("/<int:species_id>/populations/genotypes/select-population", + methods=["GET"]) +@require_login +def select_population(species_id: int): + """Select the population under which the genotypes go.""" + with database_connection(app.config["SQL_URI"]) as conn: + species = species_by_id(conn, species_id) + if not bool(species): + flash("Invalid species provided!", "alert-danger") + return redirect(url_for("species.populations.genotypes.index")) + + if not bool(request.args.get("population_id")): + return render_template("genotypes/select-population.html", + species=species, + populations=order_by_family( + populations_by_species(conn, species_id), + order_key="FamilyOrder"), + activelink="genotypes") + + population = population_by_species_and_id( + conn, species_id, request.args.get("population_id")) + if not bool(population): + flash("Invalid population selected!", "alert-danger") + return redirect(url_for( + "species.populations.genotypes.select_population", + species_id=species_id)) + + return redirect(url_for("species.populations.genotypes.list_genotypes", + species_id=species_id, + population_id=population["Id"])) + + +@genotypesbp.route( + "/<int:species_id>/populations/<int:population_id>/genotypes", + methods=["GET"]) +@require_login +def list_genotypes(species_id: int, population_id: int): + """List genotype details for species and population.""" + with database_connection(app.config["SQL_URI"]) as conn: + species = species_by_id(conn, species_id) + if not bool(species): + flash("Invalid species provided!", "alert-danger") + return redirect(url_for("species.populations.genotypes.index")) + + population = population_by_species_and_id( + conn, species_id, population_id) + if not bool(population): + flash("Invalid population selected!", "alert-danger") + return redirect(url_for( + "species.populations.genotypes.select_population", + species_id=species_id)) + + return render_template("genotypes/list-genotypes.html", + species=species, + population=population, + genocode=genocode_by_population( + conn, population_id), + total_markers=genotype_markers_count( + conn, species_id), + activelink="list-genotypes") + + +@genotypesbp.route("/<int:species_id>/genotypes/list-markers", methods=["GET"]) +@require_login +def list_markers(species_id: int): + """List a species' genetic markers.""" + with database_connection(app.config["SQL_URI"]) as conn: + species = species_by_id(conn, species_id) + if not bool(species): + flash("Invalid species provided!", "alert-danger") + return redirect(url_for("species.populations.genotypes.index")) + + start_from = safe_int(request.args.get("start_from") or 0) + if start_from < 0: + start_from = 0 + count = safe_int(request.args.get("count") or 20) + markers = enumerate_sequence( + genotype_markers(conn, species_id, offset=start_from, limit=count), + start=start_from+1) + return render_template("genotypes/list-markers.html", + species=species, + total_markers=genotype_markers_count( + conn, species_id), + start_from=start_from, + count=count, + markers=markers, + activelink="list-markers") diff --git a/uploader/oauth2/client.py b/uploader/oauth2/client.py index e119cc3..70a32ff 100644 --- a/uploader/oauth2/client.py +++ b/uploader/oauth2/client.py @@ -61,7 +61,7 @@ def __update_auth_server_jwks__(jwks) -> KeySet: def auth_server_jwks() -> KeySet: """Fetch the auth-server JSON Web Keys information.""" - _jwks = session.session_info().get("auth_server_jwks") + _jwks = session.session_info().get("auth_server_jwks") or {} if bool(_jwks): return __update_auth_server_jwks__({ "last-updated": _jwks["last-updated"], diff --git a/uploader/population/models.py b/uploader/population/models.py index 782bc9f..c6c77ae 100644 --- a/uploader/population/models.py +++ b/uploader/population/models.py @@ -44,33 +44,32 @@ def population_genetic_types(conn) -> tuple: return tuple(row["GeneticType"] for row in cursor.fetchall()) -def save_population(conn: mdb.Connection, population_details: dict) -> dict: +def save_population(cursor: mdb.cursors.Cursor, population_details: dict) -> dict: """Save the population details to the db.""" - with conn.cursor(cursorclass=DictCursor) as cursor: - #TODO: Handle FamilyOrder here - cursor.execute( - "INSERT INTO InbredSet(" - "InbredSetId, InbredSetName, Name, SpeciesId, FullName, " - "public, MappingMethodId, GeneticType, Family, MenuOrderId, " - "InbredSetCode, Description" - ") " - "VALUES (" - "%(InbredSetId)s, %(InbredSetName)s, %(Name)s, %(SpeciesId)s, " - "%(FullName)s, %(public)s, %(MappingMethodId)s, %(GeneticType)s, " - "%(Family)s, %(MenuOrderId)s, %(InbredSetCode)s, %(Description)s" - ")", - { - "MenuOrderId": 0, - "InbredSetId": 0, - "public": 2, - **population_details - }) - new_id = cursor.lastrowid - cursor.execute("UPDATE InbredSet SET InbredSetId=%s WHERE Id=%s", - (new_id, new_id)) - return { - **population_details, - "Id": new_id, - "InbredSetId": new_id, - "population_id": new_id - } + #TODO: Handle FamilyOrder here + cursor.execute( + "INSERT INTO InbredSet(" + "InbredSetId, InbredSetName, Name, SpeciesId, FullName, " + "public, MappingMethodId, GeneticType, Family, MenuOrderId, " + "InbredSetCode, Description" + ") " + "VALUES (" + "%(InbredSetId)s, %(InbredSetName)s, %(Name)s, %(SpeciesId)s, " + "%(FullName)s, %(public)s, %(MappingMethodId)s, %(GeneticType)s, " + "%(Family)s, %(MenuOrderId)s, %(InbredSetCode)s, %(Description)s" + ")", + { + "MenuOrderId": 0, + "InbredSetId": 0, + "public": 2, + **population_details + }) + new_id = cursor.lastrowid + cursor.execute("UPDATE InbredSet SET InbredSetId=%s WHERE Id=%s", + (new_id, new_id)) + return { + **population_details, + "Id": new_id, + "InbredSetId": new_id, + "population_id": new_id + } diff --git a/uploader/population/views.py b/uploader/population/views.py index 003787a..39a5762 100644 --- a/uploader/population/views.py +++ b/uploader/population/views.py @@ -2,7 +2,10 @@ import re import json import base64 +import traceback +from requests.models import Response +from MySQLdb.cursors import DictCursor from flask import (flash, request, url_for, @@ -10,10 +13,13 @@ from flask import (flash, Blueprint, current_app as app) +from uploader.samples.views import samplesbp +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.db_utils import database_connection -from uploader.samples.views import samplesbp +from uploader.datautils import enumerate_sequence from uploader.species.models import (all_species, species_by_id, order_species_by_family) @@ -27,6 +33,7 @@ from .models import (save_population, __active_link__ = "populations" popbp = Blueprint("populations", __name__) popbp.register_blueprint(samplesbp, url_prefix="/") +popbp.register_blueprint(genotypesbp, url_prefix="/") render_template = make_template_renderer("populations") @@ -58,7 +65,8 @@ def list_species_populations(species_id: int): return render_template( "populations/list-populations.html", species=species, - populations=populations_by_species(conn, species_id), + populations=enumerate_sequence(populations_by_species( + conn, species_id)), activelink="list-populations") @@ -89,7 +97,8 @@ def valid_population_name(population_name: str) -> bool: @require_login def create_population(species_id: int): """Create a new population.""" - with database_connection(app.config["SQL_URI"]) as conn: + with (database_connection(app.config["SQL_URI"]) as conn, + conn.cursor(cursorclass=DictCursor) as cursor): species = species_by_id(conn, species_id) if request.method == "GET": @@ -100,7 +109,7 @@ def create_population(species_id: int): ).decode("utf8") error_values = json.loads(base64.b64decode( - error_values.encode("utf8")).decode("utf8")) + error_values.encode("utf8")).decode("utf8"))# type: ignore[union-attr] return render_template( "populations/create-population.html", species=species, @@ -119,7 +128,7 @@ def create_population(species_id: int): flash("You must select a species.", "alert-danger") return redirect(url_for("species.populations.index")) - errors = tuple() + errors: tuple[tuple[str, str], ...] = tuple() population_name = (request.form.get( "population_name") or "").strip() @@ -149,7 +158,7 @@ def create_population(species_id: int): species_id=species["SpeciesId"], error_values=values)) - new_population = save_population(conn, { + new_population = save_population(cursor, { "SpeciesId": species["SpeciesId"], "Name": population_name, "InbredSetName": population_fullname, @@ -161,9 +170,39 @@ def create_population(species_id: int): "GeneticType": request.form.get("population_genetic_type") or None }) - return redirect(url_for("species.populations.view_population", - species_id=species["SpeciesId"], - population_id=new_population["InbredSetId"])) + def __handle_error__(error): + error_format = ( + "\n\nThere was an error creating the population:\n\t%s\n\n") + if issubclass(type(error), Exception): + app.logger.debug(error_format, traceback.format_exc()) + raise error + if issubclass(type(error), Response): + try: + _data = error.json() + except Exception as _exc: + raise Exception(error.content) from _exc + raise Exception(_data) + + app.logger.debug(error_format, error) + raise Exception(error) + + def __flash_success__(_success): + flash("Successfully created resource.", "alert-success") + return redirect(url_for( + "species.populations.view_population", + species_id=species["SpeciesId"], + population_id=new_population["InbredSetId"])) + + app.logger.debug("We begin setting up the privileges here…") + return oauth2_post( + "auth/resource/populations/create", + json={ + **dict(request.form), + "species_id": species_id, + "population_id": new_population["Id"], + "public": "on" + } + ).either(__handle_error__, __flash_success__) @popbp.route("/<int:species_id>/populations/<int:population_id>", diff --git a/uploader/samples/views.py b/uploader/samples/views.py index 6e3dc4b..fd3c601 100644 --- a/uploader/samples/views.py +++ b/uploader/samples/views.py @@ -19,9 +19,9 @@ from flask import ( from uploader import jobs from uploader.files import save_file -from uploader.datautils import order_by_family from uploader.authorisation import require_login from uploader.input_validation import is_integer_input +from uploader.datautils import order_by_family, enumerate_sequence from uploader.db_utils import ( with_db_connection, database_connection, @@ -39,6 +39,7 @@ from .models import samples_by_species_and_population samplesbp = Blueprint("samples", __name__) @samplesbp.route("/samples", methods=["GET"]) +@require_login def index(): """Direct entry-point for uploading/handling the samples.""" with database_connection(app.config["SQL_URI"]) as conn: @@ -56,6 +57,7 @@ def index(): @samplesbp.route("<int:species_id>/samples/select-population", methods=["GET"]) +@require_login def select_population(species_id: int): """Select the population to use for the samples.""" with database_connection(app.config["SQL_URI"]) as conn: @@ -86,6 +88,7 @@ def select_population(species_id: int): population_id=population["Id"])) @samplesbp.route("<int:species_id>/populations/<int:population_id>/samples") +@require_login def list_samples(species_id: int, population_id: int): """ List the samples in a particular population and give the ability to upload @@ -104,13 +107,13 @@ def list_samples(species_id: int, population_id: int): "species.populations.samples.select_population", species_id=species_id)) - all_samples = samples_by_species_and_population( - conn, species_id, population_id) + all_samples = enumerate_sequence(samples_by_species_and_population( + conn, species_id, population_id)) total_samples = len(all_samples) offset = int(request.args.get("from") or 0) if offset < 0: offset = 0 - count = int(request.args.get("count") or 10) + count = int(request.args.get("count") or 20) return render_template("samples/list-samples.html", species=species, population=population, @@ -197,6 +200,11 @@ 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? + # 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 + # populations already own the samples. the_job = jobs.launch_job( jobs.initialise_job( rconn, @@ -224,6 +232,7 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma @samplesbp.route("<int:species_id>/populations/<int:population_id>/" "upload-samples/status/<uuid:job_id>", methods=["GET"]) +@require_login def upload_status(species_id: int, population_id: int, job_id: uuid.UUID): """Check on the status of a samples upload job.""" with database_connection(app.config["SQL_URI"]) as conn: @@ -273,6 +282,7 @@ def upload_status(species_id: int, population_id: int, job_id: uuid.UUID): population=population), 400 @samplesbp.route("/upload/failure/<uuid:job_id>", methods=["GET"]) +@require_login def upload_failure(job_id: uuid.UUID): """Display the errors of the samples upload failure.""" job = with_redis_connection(lambda rconn: jobs.job( diff --git a/uploader/session.py b/uploader/session.py index 399f28c..b538187 100644 --- a/uploader/session.py +++ b/uploader/session.py @@ -96,7 +96,7 @@ def user_token() -> Either: def set_auth_server_jwks(keyset: KeySet) -> KeySet: """Update the JSON Web Keys in the session.""" save_session_info({ - **session_info(), + **session_info(),# type: ignore[misc] "auth_server_jwks": { "last-updated": datetime.now().timestamp(), "jwks": keyset.as_dict() diff --git a/uploader/species/models.py b/uploader/species/models.py index 1abed6d..51f941c 100644 --- a/uploader/species/models.py +++ b/uploader/species/models.py @@ -28,7 +28,7 @@ def order_species_by_family(species: tuple[dict, ...]) -> list: **ordered, _key: ordered.get(_key, tuple()) + (current,) } - ordered = reduce(__order__, species, {}) + ordered = reduce(__order__, species, {})# type: ignore[var-annotated] return sorted(tuple(ordered.items()), key=lambda item: item[0][0]) @@ -47,7 +47,7 @@ def save_species(conn: mdb.Connection, common_name: str, scientific_name: str, family: str, - taxon_id: Optional[str] = None) -> int: + taxon_id: Optional[str] = None) -> dict: """ Save a new species to the database. @@ -58,14 +58,14 @@ def save_species(conn: mdb.Connection, common_name: The species' common name. scientific_name; The species' scientific name. """ - genus, species = scientific_name.split(" ") + genus, species_name = scientific_name.split(" ") families = species_families(conn) with conn.cursor() as cursor: cursor.execute("SELECT MAX(OrderId) FROM Species") species = { "common_name": common_name, "common_name_lower": common_name.lower(), - "menu_name": f"{common_name} ({genus[0]}. {species.lower()})", + "menu_name": f"{common_name} ({genus[0]}. {species_name.lower()})", "scientific_name": scientific_name, "family": family, "family_order": families[family], @@ -91,13 +91,15 @@ def save_species(conn: mdb.Connection, } -def update_species(conn: mdb.Connection, - species_id: int, - common_name: str, - scientific_name: str, - family: str, - family_order: int, - species_order: int): +def update_species(# pylint: disable=[too-many-arguments] + conn: mdb.Connection, + species_id: int, + common_name: str, + scientific_name: str, + family: str, + family_order: int, + species_order: int +): """Update a species' details. Parameters @@ -114,12 +116,12 @@ def update_species(conn: mdb.Connection, species_order: The ordering of this species in relation to others """ with conn.cursor(cursorclass=DictCursor) as cursor: - genus, species = scientific_name.split(" ") + genus, species_name = scientific_name.split(" ") species = { "species_id": species_id, "common_name": common_name, "common_name_lower": common_name.lower(), - "menu_name": f"{common_name} ({genus[0]}. {species.lower()})", + "menu_name": f"{common_name} ({genus[0]}. {species_name.lower()})", "scientific_name": scientific_name, "family": family, "family_order": family_order, diff --git a/uploader/species/views.py b/uploader/species/views.py index 55b0dd3..f478505 100644 --- a/uploader/species/views.py +++ b/uploader/species/views.py @@ -8,11 +8,11 @@ from flask import (flash, current_app as app) from uploader.population import popbp -from uploader.datautils import order_by_family from uploader.ui import make_template_renderer from uploader.db_utils import database_connection 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 .models import (all_species, save_species, @@ -27,11 +27,12 @@ render_template = make_template_renderer("species") @speciesbp.route("/", methods=["GET"]) +@require_login def list_species(): """List and display all the species in the database.""" with database_connection(app.config["SQL_URI"]) as conn: return render_template("species/list-species.html", - allspecies=all_species(conn)) + allspecies=enumerate_sequence(all_species(conn))) @speciesbp.route("/<int:species_id>", methods=["GET"]) @require_login diff --git a/uploader/static/css/styles.css b/uploader/static/css/styles.css index 834c563..4565aba 100644 --- a/uploader/static/css/styles.css +++ b/uploader/static/css/styles.css @@ -26,7 +26,7 @@ body { #header .header { font-size: 2em; display: inline-block; - text-align: center; + text-align: start; } #header .header-nav { @@ -71,8 +71,9 @@ body { } .pagetitle h1 { - text-align: center; + text-align: start; text-transform: capitalize; + padding-left: 0.25em; } .pagetitle .breadcrumb { @@ -101,7 +102,7 @@ dd { padding-bottom: 1em; } -input[type="submit"] { +input[type="submit"], .btn { text-transform: capitalize; } diff --git a/uploader/templates/base.html b/uploader/templates/base.html index d68c6c0..886f503 100644 --- a/uploader/templates/base.html +++ b/uploader/templates/base.html @@ -31,7 +31,7 @@ <li class="btn"> {%if user_logged_in()%} <a href="{{url_for('oauth2.logout')}}" - title="Log out of the system">Log Out</a> + title="Log out of the system">{{user_email()}} — Log Out</a> {%else%} <a href="{{authserver_authorise_uri()}}" title="Log in to the system">Log In</a> @@ -46,12 +46,12 @@ <li><a href="/" >Home</a></li> <li><a href="{{url_for('species.list_species')}}" title="View and manage species information.">Species</a></li> - <li><a href="#" - title="Upload Genotype data.">Genotype Data</a></li> <li><a href="{{url_for('species.populations.index')}}" title="View and manage species populations.">Populations</a></li> <li><a href="{{url_for('species.populations.samples.index')}}" title="Upload population samples.">Samples</a></li> + <li><a href="{{url_for('species.populations.genotypes.index')}}" + title="Upload Genotype data.">Genotype Data</a></li> <li><a href="{{url_for('expression-data.index.index')}}" title="Upload expression data.">Expression Data</a></li> <li><a href="#" diff --git a/uploader/templates/genotypes/base.html b/uploader/templates/genotypes/base.html new file mode 100644 index 0000000..1b274bf --- /dev/null +++ b/uploader/templates/genotypes/base.html @@ -0,0 +1,12 @@ +{%extends "populations/base.html"%} + +{%block lvl3_breadcrumbs%} +<li {%if activelink=="genotypes"%} + class="breadcrumb-item active" + {%else%} + class="breadcrumb-item" + {%endif%}> + <a href="{{url_for('species.populations.genotypes.index')}}">Genotypes</a> +</li> +{%block lvl4_breadcrumbs%}{%endblock%} +{%endblock%} diff --git a/uploader/templates/genotypes/index.html b/uploader/templates/genotypes/index.html new file mode 100644 index 0000000..e749f5a --- /dev/null +++ b/uploader/templates/genotypes/index.html @@ -0,0 +1,28 @@ +{%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%} diff --git a/uploader/templates/genotypes/list-genotypes.html b/uploader/templates/genotypes/list-genotypes.html new file mode 100644 index 0000000..31b9eeb --- /dev/null +++ b/uploader/templates/genotypes/list-genotypes.html @@ -0,0 +1,119 @@ +{%extends "genotypes/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "populations/macro-display-population-card.html" import display_population_card%} + +{%block title%}Genotypes{%endblock%} + +{%block pagetitle%}Genotypes{%endblock%} + +{%block lvl4_breadcrumbs%} +<li {%if activelink=="list-genotypes"%} + class="breadcrumb-item active" + {%else%} + class="breadcrumb-item" + {%endif%}> + <a href="{{url_for('species.populations.genotypes.list_genotypes', + species_id=species.SpeciesId, + population_id=population.Id)}}">List genotypes</a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2>Genetic Markers</h2> + <p>There are a total of {{total_markers}} currently registered genetic markers + for the "{{species.FullName}}" species. You can click + <a href="{{url_for('species.populations.genotypes.list_markers', + species_id=species.SpeciesId)}}" + title="View genetic markers for species '{{species.FullName}}"> + this link to view the genetic markers + </a>. + </p> +</div> + +<div class="row"> + <h2>Genotype Encoding</h2> + <p> + The genotype encoding used for the "{{population.FullName}}" population from + the "{{species.FullName}}" species is as shown in the table below. + </p> + <table class="table"> + + <thead> + <tr> + <th>Allele Type</th> + <th>Allele Symbol</th> + <th>Allele Value</th> + </tr> + </thead> + + <tbody> + {%for row in genocode%} + <tr> + <td>{{row.AlleleType}}</td> + <td>{{row.AlleleSymbol}}</td> + <td>{{row.DatabaseValue if row.DatabaseValue is not none else "NULL"}}</td> + </tr> + {%else%} + <tr> + <td colspan="7" class="text-info"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + There is no explicit genotype encoding defined for this population. + </td> + </tr> + {%endfor%} + </tbody> + </table> + + {%if genocode | length < 1%} + <a href="#add-genotype-encoding" + title="Add a genotype encoding system for this population" + class="btn btn-primary"> + add genotype encoding + </a> + {%endif%} +</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> + +<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> + <p>You can also create a new genotype dataset by clicking the button below. + <br /> + <a href="#create-new-genotype-dataset" + 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> + + {%if datasets | length > 0%} + <table class="table"> + </table> + {%else%} + <p class="text-warning"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + There are no genotype datasets to display, yet! + </p> + {%endif%} +</div> +{%endblock%} + +{%block sidebarcontents%} +{{display_population_card(species, population)}} +{%endblock%} diff --git a/uploader/templates/genotypes/list-markers.html b/uploader/templates/genotypes/list-markers.html new file mode 100644 index 0000000..9198b44 --- /dev/null +++ b/uploader/templates/genotypes/list-markers.html @@ -0,0 +1,102 @@ +{%extends "genotypes/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "species/macro-display-species-card.html" import display_species_card%} + +{%block title%}Genotypes: List Markers{%endblock%} + +{%block pagetitle%}Genotypes: List Markers{%endblock%} + +{%block lvl4_breadcrumbs%} +<li {%if activelink=="list-markers"%} + class="breadcrumb-item active" + {%else%} + class="breadcrumb-item" + {%endif%}> + <a href="{{url_for('species.populations.genotypes.list_markers', + species_id=species.SpeciesId)}}">List markers</a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +{%if markers | length > 0%} +<div class="row"> + <p> + There are a total of {{total_markers}} genotype markers for this species. + </p> + <div class="row"> + <div class="col-md-2" style="text-align: start;"> + {%if start_from > 0%} + <a href="{{url_for('species.populations.genotypes.list_markers', + species_id=species.SpeciesId, + start_from=start_from-count, + count=count)}}"> + <span class="glyphicon glyphicon-backward"></span> + Previous + </a> + {%endif%} + </div> + <div class="col-md-8" style="text-align: center;"> + Displaying markers {{start_from+1}} to {{start_from+count if start_from+count < total_markers else total_markers}} of + {{total_markers}} + </div> + <div class="col-md-2" style="text-align: end;"> + {%if start_from + count < total_markers%} + <a href="{{url_for('species.populations.genotypes.list_markers', + species_id=species.SpeciesId, + start_from=start_from+count, + count=count)}}"> + Next + <span class="glyphicon glyphicon-forward"></span> + </a> + {%endif%} + </div> + </div> + <table class="table"> + <thead> + <tr> + <th title="">#</th> + <th title="">Marker Name</th> + <th title="Chromosome">Chr</th> + <th title="Physical location of the marker in megabasepairs"> + Location (Mb)</th> + <th title="">Source</th> + <th title="">Source2</th> + </thead> + + <tbody> + {%for marker in markers%} + <tr> + <td>{{marker.sequence_number}}</td> + <td>{{marker.Marker_Name}}</td> + <td>{{marker.Chr}}</td> + <td>{{marker.Mb}}</td> + <td>{{marker.Source}}</td> + <td>{{marker.Source2}}</td> + </tr> + {%endfor%} + </tbody> + </table> +</div> +{%else%} +<div class="row"> + <p class="text-warning"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + This species does not currently have any genetic markers uploaded, therefore, + there is nothing to display here. + </p> + <p> + <a href="#add-genetic-markers-for-species-{{species.SpeciesId}}" + title="Add genetic markers for this species" + class="btn btn-primary"> + add genetic markers + </a> + </p> +</div> +{%endif%} +{%endblock%} + +{%block sidebarcontents%} +{{display_species_card(species)}} +{%endblock%} diff --git a/uploader/templates/genotypes/select-population.html b/uploader/templates/genotypes/select-population.html new file mode 100644 index 0000000..7c81943 --- /dev/null +++ b/uploader/templates/genotypes/select-population.html @@ -0,0 +1,31 @@ +{%extends "genotypes/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "species/macro-display-species-card.html" import display_species_card%} +{%from "populations/macro-select-population.html" import select_population_form%} + +{%block title%}Genotypes{%endblock%} + +{%block pagetitle%}Genotypes{%endblock%} + + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <p> + You have indicated that you intend to upload the genotypes for species + '{{species.FullName}}'. We now just require the population for your + experiment/study, and you should be good to go. + </p> +</div> + +<div class="row"> + {{select_population_form(url_for("species.populations.genotypes.select_population", + species_id=species.SpeciesId), + populations)}} +</div> +{%endblock%} + +{%block sidebarcontents%} +{{display_species_card(species)}} +{%endblock%} diff --git a/uploader/templates/index.html b/uploader/templates/index.html index e3f5af4..0fa4a1b 100644 --- a/uploader/templates/index.html +++ b/uploader/templates/index.html @@ -18,9 +18,87 @@ <div class="explainer"> <p>Welcome to the <strong>GeneNetwork Data Quality Control and Upload System</strong>. This system is provided to help in uploading your data onto GeneNetwork where you can do analysis on it.</p> - <p>Click on the menu items on the left to select the kind of data you want to upload.</p> + <p>The sections below provide an overview of what features the menu items on + the left provide to you. Please peruse the information to get a good + big-picture understanding of what the system provides you and how to get + the most out of it.</p> {%block extrapageinfo%}{%endblock%} + + <h2>Species</h2> + + <p>The GeneNetwork service provides datasets and tools for doing genetic + studies — from + <a href="{{gn2server_intro}}" + target="_blank" + title="GeneNetwork introduction — opens in a new tab."> + its introduction</a>: + + <blockquote class="blockquote"> + <p>GeneNetwork is a group of linked data sets and tools used to study + complex networks of genes, molecules, and higher order gene function + and phenotypes. …</p> + </blockquote> + </p> + + <p>With this in mind, it follows that the data in the system is centered + aroud a variety of species. The <strong>species section</strong> will + list the currently available species in the system, and give you the + ability to add new ones, if the one you want to work on does not currently + exist on GeneNetwork</p> + + <h2>Populations</h2> + + <p>Your studies will probably focus on a particular subset of the entire + species you are interested in &ndash your population.</p> + <p>Populations are a way to organise the species data so as to link data to + specific know populations for a particular species, e.g. The BXD + population of mice (Mus musculus)</p> + <p>In older GeneNetwork documentation, you might run into the term + <em>InbredSet</em>. Should you run into it, it is a term that we've + deprecated that essentially just means the population.</p> + + <h2>Samples</h2> + + <p>These are the samples or individuals (sometimes cases) that were involved + in the experiment, and from whom the data was derived.</p> + + <h2>Genotype Data</h2> + + <p>This section will allow you to view and upload the genetic markers for + your species, and the genotype encodings used for your particular + population.</p> + <p>While, technically, genetic markers relate to the species in general, and + not to a particular population, the data (allele information) itself + relates to the particular population it was generated from – + specifically, to the actual individuals used in the experiment.</p> + <p>This is the reason why the genotype data information comes under the + population, and will check for the prior existence of the related + samples/individuals before attempting an upload of your data.</p> + + <h2>Expression Data</h2> + + <p class="text-danger"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + <strong>TODO</strong>: Document this …</p> + + <h2>Phenotype Data</h2> + + <p class="text-danger"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + <strong>TODO</strong>: Document this …</p> + + <h2>Individual Data</h2> + + <p class="text-danger"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + <strong>TODO</strong>: Document this …</p> + + <h2>RNA-Seq Data</h2> + + <p class="text-danger"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + <strong>TODO</strong>: Document this …</p> </div> </div> diff --git a/uploader/templates/login.html b/uploader/templates/login.html index bbca42f..1f71416 100644 --- a/uploader/templates/login.html +++ b/uploader/templates/login.html @@ -5,7 +5,7 @@ {%block pagetitle%}log in{%endblock%} {%block extrapageinfo%} -<p> +<p class="text-dark text-primary"> You <strong>do need to be logged in</strong> to upload data onto this system. Please do that by clicking the "Log In" button at the top of the page.</p> {%endblock%} diff --git a/uploader/templates/populations/create-population.html b/uploader/templates/populations/create-population.html index b57afba..b05ce37 100644 --- a/uploader/templates/populations/create-population.html +++ b/uploader/templates/populations/create-population.html @@ -107,7 +107,10 @@ value="{{error_values.population_code or ''}}" class="form-control" /> <small class="form-text text-muted"> - … document what this field is for … + <p class="text-danger"> + <span class="glyphicon glyphicon-exclamation-sign"></span> + What is this field is for? Confirm with Arthur and the rest. + </p> </small> </div> @@ -159,7 +162,10 @@ {%endfor%} </select> <small class="form-text text-muted"> - <p>… provide some documentation on what this field does …</p> + <p> + This is a rough grouping of the populations in GeneNetwork into lists + of common types of populations. + </p> </small> </div> @@ -207,6 +213,28 @@ {%endif%}>{{gtype}}</option> {%endfor%} </select> + <small class="form-text text-muted text-danger"> + <p> + <span class="glyphicon glyphicon-exclamation-sign"></span> + This might be a poorly named field. + </p> + <p> + It probably has more to do with the mating crosses/crossings used to + produce the individuals in the population. I am no biologist, however, + and I'm leaving this here to remind myself to confirm this. + </p> + <p> + I still don't know what riset is.<br /> + … probably something to do with Recombinant Inbred Strains + </p> + <p> + Possible resources for this: + <ul> + <li>https://www.informatics.jax.org/silver/chapters/3-2.shtml</li> + <li>https://www.informatics.jax.org/silver/chapters/9-2.shtml</li> + </ul> + </p> + </small> </div> <div class="form-group"> diff --git a/uploader/templates/populations/index.html b/uploader/templates/populations/index.html index 3314516..4354e02 100644 --- a/uploader/templates/populations/index.html +++ b/uploader/templates/populations/index.html @@ -11,8 +11,14 @@ {{flash_all_messages()}} <div class="row"> - To continue, you need to select the species: + <p> + Your experiment data will relate to a particular population from a + particular species. Let us know what species it is you want to work with + below. + </p> +</div> +<div class="row"> {{select_species_form(url_for("species.populations.index"), species)}} </div> {%endblock%} diff --git a/uploader/templates/populations/list-populations.html b/uploader/templates/populations/list-populations.html index c83c18c..7c7145f 100644 --- a/uploader/templates/populations/list-populations.html +++ b/uploader/templates/populations/list-populations.html @@ -51,6 +51,7 @@ <caption>Populations for {{species.FullName}}</caption> <thead> <tr> + <th>#</th> <th>Name</th> <th>Full Name</th> <th>Description</th> @@ -60,6 +61,7 @@ <tbody> {%for population in populations%} <tr> + <td>{{population["sequence_number"]}}</td> <td> <a href="{{url_for('species.populations.view_population', species_id=species.SpeciesId, diff --git a/uploader/templates/populations/view-population.html b/uploader/templates/populations/view-population.html index 31db54f..1e2964e 100644 --- a/uploader/templates/populations/view-population.html +++ b/uploader/templates/populations/view-population.html @@ -23,6 +23,9 @@ {%block contents%} <div class="row"> <h2>Population Details</h2> + + {{flash_all_messages()}} + <dl> <dt>Name</dt> <dd>{{population.Name}}</dd> @@ -59,6 +62,12 @@ <nav class="nav"> <ul> <li> + <a href="{{url_for('species.populations.genotypes.list_genotypes', + species_id=species.SpeciesId, + population_id=population.Id)}}" + title="Upload genotypes for {{species.FullName}}">Upload Genotypes</a> + </li> + <li> <a href="{{url_for('species.populations.samples.list_samples', species_id=species.SpeciesId, population_id=population.Id)}}" diff --git a/uploader/templates/samples/index.html b/uploader/templates/samples/index.html index 7c88c01..ee4a63e 100644 --- a/uploader/templates/samples/index.html +++ b/uploader/templates/samples/index.html @@ -11,10 +11,8 @@ {{flash_all_messages()}} <div class="row"> - <p>Here, you can upload the samples/individuals that were used in your - experiments.</p> - <p>Since the samples are linked to specific species and populations, we will - need to first select them in the next few steps.</p> + <p>GeneNetwork has a selection of different species of organisms to choose from. Within those species, there are the populations of interest for a variety of experiments, from which you, the researcher, picked your samples (or individuals or cases) from. Here you can provide some basic details about your samples.</p> + <p>To start off, we will need to know what species and population your samples belong to. Please provide that information in the next sections.</p> {{select_species_form(url_for("species.populations.samples.index"), species)}} </div> diff --git a/uploader/templates/samples/list-samples.html b/uploader/templates/samples/list-samples.html index 8f1bf16..13e5cec 100644 --- a/uploader/templates/samples/list-samples.html +++ b/uploader/templates/samples/list-samples.html @@ -24,45 +24,56 @@ <div class="row"> <p> - Samples for population "{{population.FullName}}" from the + You selected the population "{{population.FullName}}" from the "{{species.FullName}}" species. </p> +</div> + +{%if samples | length > 0%} +<div class="row"> + <p> + This population already has <strong>{{total_samples}}</strong> + samples/individuals entered. You can explore the list of samples in this + population in the table below. + </p> +</div> + +<div class="row"> + <div class="col-md-2"> + {%if offset > 0:%} + <a href="{{url_for('species.populations.samples.list_samples', + species_id=species.SpeciesId, + population_id=population.Id, + from=offset-count, + count=count)}}"> + <span class="glyphicon glyphicon-backward"></span> + Previous + </a> + {%endif%} + </div> - {%if samples | length > 0%} - <div class="row"> - <div class="col-md-2"> - {%if offset > 0:%} - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id, - from=offset-count, - count=count)}}"> - <span class="glyphicon glyphicon-backward"></span> - Previous - </a> - {%endif%} - </div> - - <div class="col-md-8" style="text-align: center;"> - Samples {{offset}} — {{offset+(count if offset + count < total_samples else total_samples - offset)}} / {{total_samples}} - </div> - - <div class="col-md-2"> - {%if offset + count < total_samples:%} - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id, - from=offset+count, - count=count)}}"> - Next - <span class="glyphicon glyphicon-forward"></span> - </a> - {%endif%} - </div> + <div class="col-md-8" style="text-align: center;"> + Samples {{offset}} — {{offset+(count if offset + count < total_samples else total_samples - offset)}} / {{total_samples}} + </div> + + <div class="col-md-2"> + {%if offset + count < total_samples:%} + <a href="{{url_for('species.populations.samples.list_samples', + species_id=species.SpeciesId, + population_id=population.Id, + from=offset+count, + count=count)}}"> + Next + <span class="glyphicon glyphicon-forward"></span> + </a> + {%endif%} </div> +</div> +<div class="row"> <table class="table"> <thead> <tr> + <th>#</th> <th>Name</th> <th>Auxilliary Name</th> <th>Symbol</th> @@ -73,6 +84,7 @@ <tbody> {%for sample in samples%} <tr> + <td>{{sample.sequence_number}}</td> <td>{{sample.Name}}</td> <td>{{sample.Name2}}</td> <td>{{sample.Symbol or "-"}}</td> @@ -90,10 +102,14 @@ delete all samples </a> </p> - {%else%} - <p class="text-danger"> - <span class="glyphicon glyphicon-exclamation-sign"></span> - There are no samples for this population at this time. +</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> @@ -106,8 +122,9 @@ add samples </a> </p> - {%endif%} </div> +{%endif%} + {%endblock%} {%block sidebarcontents%} diff --git a/uploader/templates/samples/select-population.html b/uploader/templates/samples/select-population.html index 8e22ac1..f437780 100644 --- a/uploader/templates/samples/select-population.html +++ b/uploader/templates/samples/select-population.html @@ -12,20 +12,25 @@ {{flash_all_messages()}} <div class="row"> - <p>Select the population to use with your samples:</p> + <p>You have selected "{{species.FullName}}" as the species that your data relates to.</p> + <p>Next, we need information regarding the population your data relates to. Do please select the population from the existing ones below</p> +</div> + +<div class="row"> {{select_population_form( url_for("species.populations.samples.select_population", species_id=species.SpeciesId), populations)}} </div> -<div class="row"> - <p><strong>Cannot find your population in the list?</strong></p> - <p>If you cannot find the population you want in the drop-down above, you can - instead, +<div class="row"> + <p> + If you cannot find the population your data relates to in the drop-down + above, you might want to <a href="{{url_for('species.populations.create_population', species_id=species.SpeciesId)}}" title="Create a new population for species '{{species.FullName}},"> - create a new population</a>. + add a new population to GeneNetwork</a> + instead. </div> {%endblock%} diff --git a/uploader/templates/samples/upload-samples.html b/uploader/templates/samples/upload-samples.html index b101b2e..25d3290 100644 --- a/uploader/templates/samples/upload-samples.html +++ b/uploader/templates/samples/upload-samples.html @@ -23,7 +23,12 @@ {{flash_all_messages()}} <div class="row"> - <p>You can now upload a character-separated value (CSV) file that contains + <p> + You can now upload the samples for the "{{population.FullName}}" population + from the "{{species.FullName}}" species here. + </p> + <p> + Upload a <strong>character-separated value (CSV)</strong> file that contains details about your samples. The CSV file should have the following fields: <dl> <dt>Name</dt> diff --git a/uploader/templates/species/list-species.html b/uploader/templates/species/list-species.html index 573bcee..85c9d40 100644 --- a/uploader/templates/species/list-species.html +++ b/uploader/templates/species/list-species.html @@ -29,29 +29,35 @@ <caption>Available Species</caption> <thead> <tr> - <th>Common Name</th> - <th>Scientific Name</th> - <th>TaxonId</th> - <th>Use</th> + <th>#</td> + <th title="A common, layman's name for the species.">Common Name</th> + <th title="The scientific name for the species">Organism Name</th> + <th title="An identifier for the species in the NCBI taxonomy database"> + Taxonomy ID + </th> + <th title="A generic grouping used internally by GeneNetwork for organising species."> + Family + </th> </tr> </thead> <tbody> {%for species in allspecies%} <tr> + <td>{{species["sequence_number"]}}</td> <td>{{species["SpeciesName"]}}</td> - <td>{{species["FullName"]}}</td> - <td> - <a href="https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id={{species['TaxonomyId']}}" - title="View species details on NCBI" - target="_blank">{{species["TaxonomyId"]}}</a> - </td> <td> <a href="{{url_for('species.view_species', species_id=species['SpeciesId'])}}" - title=""> - {{species["SpeciesName"]}} ({{species["FullName"]}}) + title="View details in GeneNetwork on {{species['FullName']}}"> + {{species["FullName"]}} </a> </td> + <td> + <a href="https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id={{species['TaxonomyId']}}" + title="View species details on NCBI" + target="_blank">{{species["TaxonomyId"]}}</a> + </td> + <td>{{species.Family}}</td> </tr> {%else%} <tr> diff --git a/uploader/templates/species/macro-select-species.html b/uploader/templates/species/macro-select-species.html index 3dbfc95..6955134 100644 --- a/uploader/templates/species/macro-select-species.html +++ b/uploader/templates/species/macro-select-species.html @@ -4,7 +4,7 @@ <legend>Select Species</legend> <div class="form-group"> - <label for="select-species" class="form-label">Select Species</label> + <label for="select-species" class="form-label">Species</label> <select id="select-species" name="species_id" class="form-control" diff --git a/uploader/templates/species/view-species.html b/uploader/templates/species/view-species.html index 6942168..b01864d 100644 --- a/uploader/templates/species/view-species.html +++ b/uploader/templates/species/view-species.html @@ -40,16 +40,11 @@ <ol> <li> - <a href="#" - title="Upload genotypes for {{species.FullName}}">Upload Genotypes</a> - </li> - <li> <a href="{{url_for('species.populations.list_species_populations', species_id=species.SpeciesId)}}" title="Create/Edit populations for {{species.FullName}}"> Manage populations</a> </li> - <li><a href="#" title="">any other action, perhaps …</a></li> </ol> |