diff options
114 files changed, 2105 insertions, 4746 deletions
diff --git a/.guix-channel b/.guix-channel index 54206b2..f1a8fa6 100644 --- a/.guix-channel +++ b/.guix-channel @@ -35,11 +35,12 @@ (channel (name guix-bioinformatics) (url "https://git.genenetwork.org/guix-bioinformatics") - (commit "903465c85c9b2ae28480b236c3364da873ca8f51")) + (commit "9b0955f14ec725990abb1f6af3b9f171e4943f77")) (channel (name guix-past) (url "https://codeberg.org/guix-science/guix-past") (branch "master") + (commit "473c942b509ab3ead35159d27dfbf2031a36cd4d") (introduction (channel-introduction (version 0) @@ -50,6 +51,7 @@ (name guix-rust-past-crates) (url "https://codeberg.org/guix/guix-rust-past-crates.git") (branch "trunk") + (commit "b8b7ffbd1cec9f56f93fae4da3a74163bbc9c570") (introduction (channel-introduction (version 0) diff --git a/README.org b/README.org index ca77653..efa837b 100644 --- a/README.org +++ b/README.org @@ -219,7 +219,7 @@ To check for correct type usage in the application, run: Run unit tests with: #+BEGIN_SRC shell $ export UPLOADER_CONF=</path/to/configuration/file.py> - $ pytest -m unit_test + $ pytest -m unit_test -n auto #+END_SRC To run ALL tests (not just unit tests): diff --git a/scripts/insert_samples.py b/scripts/insert_samples.py index fc029f9..96ae8e2 100644 --- a/scripts/insert_samples.py +++ b/scripts/insert_samples.py @@ -6,10 +6,10 @@ import argparse import traceback import MySQLdb as mdb -from redis import Redis + from gn_libs.mysqldb import database_connection -from uploader.check_connections import check_db, check_redis +from uploader.check_connections import check_db from uploader.species.models import species_by_id from uploader.population.models import population_by_id from uploader.samples.models import ( @@ -35,7 +35,6 @@ class SeparatorAction(argparse.Action): setattr(namespace, self.dest, (chr(9) if values == "\\t" else values)) def insert_samples(conn: mdb.Connection,# pylint: disable=[too-many-arguments, too-many-positional-arguments] - rconn: Redis,# pylint: disable=[unused-argument] speciesid: int, populationid: int, samplesfile: pathlib.Path, @@ -119,11 +118,6 @@ if __name__ == "__main__": help=("The character used to delimit (surround?) the value in " "each column.")) - # == Script-specific extras == - parser.add_argument("--redisuri", - help="URL to initialise connection to redis", - default="redis:///") - args = parser.parse_args() return args @@ -132,17 +126,13 @@ if __name__ == "__main__": status_code = 1 # Exit with an Exception args = cli_args() check_db(args.databaseuri) - check_redis(args.redisuri) if not args.samplesfile.exists(): logging.error("File not found: '%s'.", args.samplesfile) return 2 - with (Redis.from_url(args.redisuri, decode_responses=True) as rconn, - database_connection(args.databaseuri) as dbconn): - + with database_connection(args.databaseuri) as dbconn: try: status_code = insert_samples(dbconn, - rconn, args.speciesid, args.populationid, args.samplesfile, diff --git a/scripts/phenotypes/__init__.py b/scripts/phenotypes/__init__.py new file mode 100644 index 0000000..73ad839 --- /dev/null +++ b/scripts/phenotypes/__init__.py @@ -0,0 +1 @@ +"Scripts for dealing with phenotypes." diff --git a/scripts/phenotypes/delete_phenotypes.py b/scripts/phenotypes/delete_phenotypes.py new file mode 100644 index 0000000..461f3ec --- /dev/null +++ b/scripts/phenotypes/delete_phenotypes.py @@ -0,0 +1,173 @@ +"""Delete phenotypes.""" +import sys +import logging +from pathlib import Path +from typing import Optional +from urllib.parse import urljoin +from argparse import Namespace, ArgumentParser + +import requests +from MySQLdb.cursors import DictCursor, BaseCursor + +from gn_libs.mysqldb import database_connection + +from uploader.phenotypes.models import delete_phenotypes +from scripts.cli.logging import setup_logging +from scripts.cli.options import (add_logging, + add_mariadb_uri, + add_population_id) + +logger = logging.getLogger(__name__) + +def read_xref_ids_file(filepath: Optional[Path]) -> tuple[int, ...]: + """Read the phenotypes' cross-reference IDS from file.""" + if filepath is None: + return tuple() + + logger.debug("Using file '%s' to retrieve XREF IDs for deletion.", + filepath.name) + _ids: tuple[int, ...] = tuple() + with filepath.open(mode="r") as infile: + for line in infile.readlines(): + try: + _ids += (int(line.strip()),) + except TypeError: + pass + + return _ids + + +def fetch_all_xref_ids( + cursor: BaseCursor, population_id: int) -> tuple[int, ...]: + """Fetch all cross-reference IDs.""" + cursor.execute("SELECT Id FROM PublishXRef WHERE InbredSetId=%s", + (population_id,)) + return tuple(int(row["Id"]) for row in cursor.fetchall()) + + +def update_auth( + auth_details: tuple[str, str], + species_id: int, + population_id: int, + dataset_id: int, + xref_ids: tuple[int, ...] = tuple() +): + """Update the authorisation server: remove items to delete.""" + authserver, token = auth_details + resp = requests.post( + urljoin(authserver, + (f"/auth/data/phenotypes/{species_id}/{population_id}" + f"/{dataset_id}/delete")), + timeout=(9.13, 20), + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + }, + json={"xref_ids": xref_ids}) + resp.raise_for_status() + + +def delete_the_phenotypes( + cursor: BaseCursor, + population_id: int, + xref_ids: tuple[int, ...] = tuple()) -> int: + """Process and delete the phenotypes.""" + delete_phenotypes(cursor, population_id, xref_ids) + + return 0 + +if __name__ == "__main__": + def parse_args() -> Namespace: + """Parse CLI arguments.""" + parser = add_logging( + add_population_id( + add_mariadb_uri( + ArgumentParser( + prog="delete-phenotypes", + description=( + "Script to delete phenotypes from the database."))))) + parser.add_argument( + "dataset_id", + metavar="DATASET-ID", + type=int, + help="The dataset identifier for phenotypes to delete.") + parser.add_argument( + "auth_server_uri", + metavar="AUTH-SERVER-URI", + type=str, + help="URI to the authorisation server.") + parser.add_argument( + "auth_token", + metavar="AUTH-TOKEN", + type=str, + help=("Token to use to update the authorisation system with the " + "deletions done.")) + parser.add_argument( + "--xref_ids_file", + metavar="XREF-IDS-FILE", + type=Path, + help=("Path to a file with phenotypes cross-reference IDs to " + "delete.")) + parser.add_argument( + "--delete-all", + action="store_true", + help=("If no 'XREF-IDS-FILE' is provided, this flag determines " + "whether or not all the phenotypes for the given population " + "will be deleted.")) + return parser.parse_args() + + + def main(): + """The `delete-phenotypes` script's entry point.""" + args = parse_args() + setup_logging(logger, args.log_level.upper(), tuple()) + with (database_connection(args.db_uri) as conn, + conn.cursor(cursorclass=DictCursor) as cursor): + xref_ids = read_xref_ids_file(args.xref_ids_file) + try: + assert not (len(xref_ids) > 0 and args.delete_all) + xref_ids = (fetch_all_xref_ids(cursor, args.population_id) + if args.delete_all else xref_ids) + logger.debug("Will delete %s phenotypes and related data", + len(xref_ids)) + if len(xref_ids) == 0: + print("No cross-reference IDs were provided. Aborting.") + return 0 + + print("Updating authorisations: ", end="") + update_auth((args.auth_server_uri, args.auth_token), + args.species_id, + args.population_id, + args.dataset_id, + xref_ids) + print("OK.") + print("Deleting the data: ", end="") + delete_phenotypes(cursor, args.population_id, xref_ids=xref_ids) + print("OK.") + if args.xref_ids_file is not None: + print("Deleting temporary file: ", end="") + args.xref_ids_file.unlink() + print("OK.") + + return 0 + except AssertionError: + logger.error( + "'DELETE-ALL' and 'XREF-IDS' are mutually exclusive. " + "If you specify the list of XREF-IDS (in a file) to delete " + "and also specify to 'DELETE-ALL' phenotypes in the " + "population, we have no way of knowing what it is you want.") + return 1 + except requests.exceptions.HTTPError as _exc: + resp = _exc.response + resp_data = resp.json() + logger.debug("%s: %s", + resp_data["error"], + resp_data["error_description"], + exc_info=True) + return 1 + except Exception as _exc:# pylint: disable=[broad-exception-caught] + logger.debug("Failed while attempting to delete phenotypes.", + exc_info=True) + return 1 + + sys.exit(main()) diff --git a/scripts/run_qtlreaper.py b/scripts/run_qtlreaper.py index 7d58402..54e5d45 100644 --- a/scripts/run_qtlreaper.py +++ b/scripts/run_qtlreaper.py @@ -169,7 +169,7 @@ def dispatch(args: Namespace) -> int: logger.info("Successfully computed p values for %s traits.", len(_traitsdata)) return 0 except FileNotFoundError as fnf: - logger.error(", ".join(fnf.args), exc_info=False) + logger.error(", ".join(str(arg) for arg in fnf.args), exc_info=False) except AssertionError as aserr: logger.error(", ".join(aserr.args), exc_info=False) except Exception as _exc:# pylint: disable=[broad-exception-caught] diff --git a/uploader/__init__.py b/uploader/__init__.py index 0ba1f81..46689c5 100644 --- a/uploader/__init__.py +++ b/uploader/__init__.py @@ -73,6 +73,28 @@ def setup_modules_logging(app_logger, modules): _logger.setLevel(loglevel) +def __setup_scratch_directory__(app: Flask) -> Flask: + app.config["SCRATCH_DIRECTORY"] = Path( + app.config["SCRATCH_DIRECTORY"]).absolute() + return app + +def __setup_upload_directory__(app: Flask) -> Flask: + if app.config.get("UPLOADS_DIRECTORY", "").strip() == "": + app.config["UPLOADS_DIRECTORY"] = app.config[ + "SCRATCH_DIRECTORY"].joinpath("uploads") + else: + app.config["UPLOADS_DIRECTORY"] = Path( + app.config["UPLOADS_DIRECTORY"].strip()).absolute() + + return app + + +def update_unspecified_defaults(app: Flask) -> Flask: + """Setup the defaults for necessary configurations that do not have values + specified for them.""" + return __setup_upload_directory__(__setup_scratch_directory__(app)) + + def create_app(config: Optional[dict] = None): """The application factory. @@ -100,6 +122,7 @@ def create_app(config: Optional[dict] = None): # Silently ignore secrets if the file does not exist. app.config.from_pyfile(secretsfile) app.config.update(config) # Override everything with passed in config + update_unspecified_defaults(app) ### END: Application configuration app.config["SESSION_CACHELIB"] = FileSystemCache( diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py index 4e1cd13..a71dd44 100644 --- a/uploader/background_jobs.py +++ b/uploader/background_jobs.py @@ -1,38 +1,51 @@ """Generic views and utilities to handle background jobs.""" import uuid +import datetime import importlib from typing import Callable from functools import partial from werkzeug.wrappers.response import Response from flask import ( + flash, + request, redirect, Blueprint, - render_template, current_app as app) from gn_libs import jobs from gn_libs import sqlite3 from gn_libs.jobs.jobs import JobNotFound - -from uploader.sui import sui_template - -from uploader.flask_extensions import url_for +from uploader import session from uploader.authorisation import require_login +from uploader.flask_extensions import url_for, render_template background_jobs_bp = Blueprint("background-jobs", __name__) HandlerType = Callable[[dict], Response] -def __default_error_handler__(job: dict) -> Response: - return redirect(url_for("background-jobs.job_error", job_id=job["job_id"])) +def make_datetime_formatter(dtformat: str = "%A, %d %B %Y at %H:%M %Z") -> Callable[[str], str]: + """Make a datetime formatter with the provided `dtformat`""" + def __formatter__(val: str) -> str: + dt = datetime.datetime.fromisoformat(val) + return dt.strftime(dtformat.strip()) + + return __formatter__ + +__default_datetime_formatter__ = make_datetime_formatter() + + +def __default_handler__(_job): + return render_template("background-jobs/job-summary.html", + job=_job, + display_datetime=__default_datetime_formatter__) def register_handlers( job_type: str, success_handler: HandlerType, # pylint: disable=[redefined-outer-name] - error_handler: HandlerType = __default_error_handler__ + error_handler: HandlerType = __default_handler__ # pylint: disable=[redefined-outer-name] ) -> str: """Register success and error handlers for each job type.""" @@ -64,7 +77,7 @@ def register_job_handlers(job: dict): try: _error_handler = __load_handler__(metadata["error_handler"]) except Exception as _exc:# pylint: disable=[broad-exception-caught] - _error_handler = __default_error_handler__ + _error_handler = __default_handler__ register_handlers( metadata["job-type"], _success_handler, _error_handler) @@ -80,11 +93,7 @@ def handler(job: dict, handler_type: str) -> HandlerType: if bool(_handler): return _handler(job) - def __default_success_handler__(_job): - return render_template( - sui_template("background-jobs/default-success-page.html"), job=_job) - - return __default_success_handler__ + return __default_handler__(job) error_handler = partial(handler, handler_type="error") @@ -101,17 +110,17 @@ def job_status(job_id: uuid.UUID): status = job["metadata"]["status"] register_job_handlers(job) - if status == "error": + if status in ("error", "stopped"): return error_handler(job) if status == "completed": return success_handler(job) - return render_template(sui_template("jobs/job-status.html"), job=job) + return render_template("background-jobs/job-status.html", + job=job, + display_datetime=__default_datetime_formatter__) except JobNotFound as _jnf: - return render_template( - sui_template("jobs/job-not-found.html"), - job_id=job_id) + return render_template("jobs/job-not-found.html", job_id=job_id) @background_jobs_bp.route("/error/<uuid:job_id>") @@ -121,7 +130,96 @@ def job_error(job_id: uuid.UUID): with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: try: job = jobs.job(conn, job_id, fulldetails=True) - return render_template(sui_template("jobs/job-error.html"), job=job) + return render_template("jobs/job-error.html", job=job) + except JobNotFound as _jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) + + +@background_jobs_bp.route("/list") +@require_login +def list_jobs(): + """List background jobs.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + return render_template( + "background-jobs/list-jobs.html", + jobs=jobs.jobs_by_external_id( + conn, session.user_details()["user_id"]), + display_datetime=__default_datetime_formatter__) + + +@background_jobs_bp.route("/summary/<uuid:job_id>") +@require_login +def job_summary(job_id: uuid.UUID): + """Provide a summary for completed jobs.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + status = job["metadata"]["status"] + + if status in ("completed", "error", "stopped"): + return render_template("background-jobs/job-summary.html", + job=job, + display_datetime=__default_datetime_formatter__) + return redirect(url_for( + "background-jobs.job_status", job_id=job["job_id"])) + except JobNotFound as _jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) + + +@background_jobs_bp.route("/delete/<uuid:job_id>", methods=["GET", "POST"]) +@require_login +def delete_single(job_id: uuid.UUID): + """Delete a single job.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + status = job["metadata"]["status"] + if status not in ("completed", "error", "stopped"): + flash("We cannot delete a running job.", "alert alert-danger") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + + if request.method == "GET": + return render_template("background-jobs/delete-job.html", + job=job, + display_datetime=__default_datetime_formatter__) + + if request.form["btn-confirm-delete"] == "delete": + jobs.delete_job(conn, job_id) + flash("Job was deleted successfully.", "alert alert-success") + return redirect(url_for("background-jobs.list_jobs")) + flash("Delete cancelled.", "alert alert-info") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + except JobNotFound as _jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) + + +@background_jobs_bp.route("/stop/<uuid:job_id>", methods=["GET", "POST"]) +@require_login +def stop_job(job_id: uuid.UUID): + """Stop a running job.""" + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + status = job["metadata"]["status"] + if status != "running": + flash("Cannot stop a job that is not running.", "alert alert-danger") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + + if request.method == "GET": + return render_template("background-jobs/stop-job.html", + job=job, + display_datetime=__default_datetime_formatter__) + + if request.form["btn-confirm-stop"] == "stop": + jobs.kill_job(conn, job_id) + flash("Job was stopped successfully.", "alert alert-success") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) + flash("Stop cancelled.", "alert alert-info") + return redirect(url_for( + "background-jobs.job_summary", job_id=job_id)) except JobNotFound as _jnf: - return render_template(sui_template("jobs/job-not-found.html"), - job_id=job_id) + return render_template("jobs/job-not-found.html", job_id=job_id) diff --git a/uploader/base_routes.py b/uploader/base_routes.py index e9f1165..72a8402 100644 --- a/uploader/base_routes.py +++ b/uploader/base_routes.py @@ -11,12 +11,8 @@ from flask import (flash, current_app as app, send_from_directory) - -from uploader.sui import sui_template - from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer -from uploader.oauth2.client import user_logged_in from uploader.species.models import all_species, species_by_id base = Blueprint("base", __name__) @@ -40,10 +36,11 @@ def index(): print("We found a species ID. Processing...") if not bool(request.args.get("species_id")): return render_template( - sui_template("index.html" if user_logged_in() else "login.html"), + "index.html", gn2server_intro=urljoin(app.config["GN2_SERVER_URL"], "/intro"), species=all_species(conn), - streamlined_ui=streamlined_ui) + view_under_construction=request.args.get( + "view_under_construction", False)) species = species_by_id(conn, request.args.get("species_id")) if not bool(species): diff --git a/uploader/configutils.py b/uploader/configutils.py new file mode 100644 index 0000000..c5db50b --- /dev/null +++ b/uploader/configutils.py @@ -0,0 +1,13 @@ +"""Functions to fetch settings.""" +from pathlib import Path + +def fetch_setting(app, setting): + """Fetch a specified configuration `setting` from the `app` object.""" + return app.config[setting] + +def uploads_dir(app) -> Path: + """Fetch the uploads directory""" + _dir = Path(fetch_setting(app, "UPLOADS_DIRECTORY")).absolute() + assert _dir.exists() and _dir.is_dir(), ( + f"'{_dir}' needs to be an existing directory.") + return _dir diff --git a/uploader/default_settings.py b/uploader/default_settings.py index 52cdad5..6381a67 100644 --- a/uploader/default_settings.py +++ b/uploader/default_settings.py @@ -5,8 +5,14 @@ actual configuration file used for the production and staging systems. LOG_LEVEL = "WARNING" SECRET_KEY = b"<Please! Please! Please! Change This!>" -UPLOAD_FOLDER = "/tmp/qc_app_files" -TEMPORARY_DIRECTORY = "/tmp/gn-uploader-tmpdir" + +# Scratch directory and uploads: +# *** The scratch directory *** +# We avoid `/tmp` entirely for the scratch directory to avoid shared global +# mutable state with other users/applications/processes. +SCRATCH_DIRECTORY = "~/tmp/gn-uploader-scratchdir" +UPLOADS_DIRECTORY = ""# If not set, will be under scratch directory. + REDIS_URL = "redis://" JOBS_TTL_SECONDS = 1209600 # 14 days GNQC_REDIS_PREFIX="gn-uploader" diff --git a/uploader/errors.py b/uploader/errors.py index 3e7c893..2ac48b8 100644 --- a/uploader/errors.py +++ b/uploader/errors.py @@ -3,7 +3,8 @@ import traceback from werkzeug.exceptions import HTTPException import MySQLdb as mdb -from flask import Flask, request, render_template, current_app as app +from flask import Flask, request, current_app as app +from uploader.flask_extensions import render_template def handle_general_exception(exc: Exception): """Handle generic exceptions.""" diff --git a/uploader/files/chunks.py b/uploader/files/chunks.py index c4360b5..f63f32f 100644 --- a/uploader/files/chunks.py +++ b/uploader/files/chunks.py @@ -5,6 +5,8 @@ from typing import Iterator from flask import current_app as app from werkzeug.utils import secure_filename +from uploader.configutils import uploads_dir + def chunked_binary_read(filepath: Path, chunksize: int = 2048) -> Iterator: """Read a file in binary mode in chunks.""" @@ -29,4 +31,4 @@ def chunks_directory(uniqueidentifier: str) -> Path: """Compute the directory where chunks are temporarily stored.""" if uniqueidentifier == "": raise ValueError("Unique identifier cannot be empty!") - return Path(app.config["UPLOAD_FOLDER"], f"tempdir_{uniqueidentifier}") + return Path(uploads_dir(app), f"tempdir_{uniqueidentifier}") diff --git a/uploader/files/functions.py b/uploader/files/functions.py index 7b9f06b..68f4e16 100644 --- a/uploader/files/functions.py +++ b/uploader/files/functions.py @@ -8,6 +8,8 @@ from flask import current_app from werkzeug.utils import secure_filename from werkzeug.datastructures import FileStorage +from uploader.configutils import uploads_dir + from .chunks import chunked_binary_read def save_file(fileobj: FileStorage, upload_dir: Path, hashed: bool = True) -> Path: @@ -30,7 +32,7 @@ def save_file(fileobj: FileStorage, upload_dir: Path, hashed: bool = True) -> Pa def fullpath(filename: str): """Get a file's full path. This makes use of `flask.current_app`.""" - return Path(current_app.config["UPLOAD_FOLDER"], filename).absolute() + return Path(uploads_dir(current_app), filename).absolute() def sha256_digest_over_file(filepath: Path) -> str: diff --git a/uploader/files/views.py b/uploader/files/views.py index 29059c7..ea0e827 100644 --- a/uploader/files/views.py +++ b/uploader/files/views.py @@ -6,13 +6,15 @@ from pathlib import Path from flask import request, jsonify, Blueprint, current_app as app +from uploader.configutils import uploads_dir + from .chunks import chunk_name, chunks_directory files = Blueprint("files", __name__) def target_file(fileid: str) -> Path: """Compute the full path for the target file.""" - return Path(app.config["UPLOAD_FOLDER"], fileid) + return Path(uploads_dir(app), fileid) @files.route("/upload/resumable", methods=["GET"]) diff --git a/uploader/flask_extensions.py b/uploader/flask_extensions.py index 30fbad7..83d25aa 100644 --- a/uploader/flask_extensions.py +++ b/uploader/flask_extensions.py @@ -2,19 +2,16 @@ import logging from typing import Any, Optional -from flask import (request, current_app as app, url_for as flask_url_for) +from flask import ( + request, + current_app as app, + url_for as flask_url_for, + render_template as flask_render_template) 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.""" +def __fetch_flags__(): flags = {} for flag in app.config["FEATURE_FLAGS_HTTP"]: flag_value = (request.args.get(flag) or request.form.get(flag) or "").strip() @@ -22,12 +19,33 @@ def url_for( flags[flag] = flag_value continue continue + logger.debug("HTTP FEATURE FLAGS: %s", flags) + return flags - logger.debug("HTTP FEATURE FLAGS: %s, other variables: %s", flags, values) + +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.""" + logger.debug("other variables: %s", values) return flask_url_for(endpoint=endpoint, _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external, **values, - **flags) + **__fetch_flags__()) + + +def render_template(template_name_or_list, **context: Any) -> str: + """Extend flask's `render_template` function""" + return flask_render_template( + template_name_or_list, + **{ + **context, + **__fetch_flags__() # override any flag values + }) diff --git a/uploader/phenotypes/models.py b/uploader/phenotypes/models.py index 7c051d7..06c417f 100644 --- a/uploader/phenotypes/models.py +++ b/uploader/phenotypes/models.py @@ -1,4 +1,6 @@ """Database and utility functions for phenotypes.""" +import time +import random import logging import tempfile from pathlib import Path @@ -6,8 +8,8 @@ from functools import reduce from datetime import datetime from typing import Union, Optional, Iterable -import MySQLdb as mdb -from MySQLdb.cursors import Cursor, DictCursor +from MySQLdb.connections import Connection +from MySQLdb.cursors import Cursor, DictCursor, BaseCursor from gn_libs.mysqldb import debug_query @@ -27,7 +29,7 @@ __PHENO_DATA_TABLES__ = { def datasets_by_population( - conn: mdb.Connection, + conn: Connection, species_id: int, population_id: int ) -> tuple[dict, ...]: @@ -42,7 +44,7 @@ def datasets_by_population( return tuple(dict(row) for row in cursor.fetchall()) -def dataset_by_id(conn: mdb.Connection, +def dataset_by_id(conn: Connection, species_id: int, population_id: int, dataset_id: int) -> dict: @@ -57,7 +59,7 @@ def dataset_by_id(conn: mdb.Connection, return dict(cursor.fetchone()) -def phenotypes_count(conn: mdb.Connection, +def phenotypes_count(conn: Connection, population_id: int, dataset_id: int) -> int: """Count the number of phenotypes in the dataset.""" @@ -85,11 +87,14 @@ def phenotype_publication_data(conn, phenotype_id) -> Optional[dict]: return dict(res) -def dataset_phenotypes(conn: mdb.Connection, - population_id: int, - dataset_id: int, - offset: int = 0, - limit: Optional[int] = None) -> tuple[dict, ...]: +def dataset_phenotypes( + conn: Connection, + population_id: int, + dataset_id: int, + offset: int = 0, + limit: Optional[int] = None, + xref_ids: tuple[int, ...] = tuple() +) -> tuple[dict, ...]: """Fetch the actual phenotypes.""" _query = ( "SELECT pheno.*, pxr.Id AS xref_id, pxr.InbredSetId, ist.InbredSetCode " @@ -98,14 +103,16 @@ def dataset_phenotypes(conn: mdb.Connection, "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 "") with conn.cursor(cursorclass=DictCursor) as cursor: - cursor.execute(_query, (population_id, dataset_id)) + cursor.execute(_query, (population_id, dataset_id) + xref_ids) debug_query(cursor, logger) return tuple(dict(row) for row in cursor.fetchall()) -def __phenotype_se__(cursor: Cursor, xref_id, dataids_and_strainids): +def __phenotype_se__(cursor: BaseCursor, xref_id, dataids_and_strainids): """Fetch standard-error values (if they exist) for a phenotype.""" paramstr = ", ".join(["(%s, %s)"] * len(dataids_and_strainids)) flat = tuple(item for sublist in dataids_and_strainids for item in sublist) @@ -187,7 +194,7 @@ def __merge_pheno_data_and_se__(data, sedata) -> dict: def phenotype_by_id( - conn: mdb.Connection, + conn: Connection, species_id: int, population_id: int, dataset_id: int, @@ -225,7 +232,7 @@ def phenotype_by_id( return None -def phenotypes_data(conn: mdb.Connection, +def phenotypes_data(conn: Connection, population_id: int, dataset_id: int, offset: int = 0, @@ -249,7 +256,7 @@ def phenotypes_data(conn: mdb.Connection, def phenotypes_vector_data(# pylint: disable=[too-many-arguments, too-many-positional-arguments] - conn: mdb.Connection, + conn: Connection, species_id: int, population_id: int, xref_ids: tuple[int, ...] = tuple(), @@ -301,7 +308,7 @@ def phenotypes_vector_data(# pylint: disable=[too-many-arguments, too-many-posit return reduce(__organise__, cursor.fetchall(), {}) -def save_new_dataset(cursor: Cursor, +def save_new_dataset(cursor: BaseCursor, population_id: int, dataset_name: str, dataset_fullname: str, @@ -328,35 +335,6 @@ def save_new_dataset(cursor: Cursor, return {**params, "Id": cursor.lastrowid} -def phenotypes_data_by_ids( - conn: mdb.Connection, - inbred_pheno_xref: dict[str, int] -) -> tuple[dict, ...]: - """Fetch all phenotype data, filtered by the `inbred_pheno_xref` mapping.""" - _paramstr = ",".join(["(%s, %s, %s)"] * len(inbred_pheno_xref)) - _query = ("SELECT " - "pub.PubMed_ID, pheno.*, pxr.*, pd.*, str.*, iset.InbredSetCode " - "FROM Publication AS pub " - "RIGHT JOIN PublishXRef AS pxr0 ON pub.Id=pxr0.PublicationId " - "INNER JOIN Phenotype AS pheno ON pxr0.PhenotypeId=pheno.id " - "INNER JOIN PublishXRef AS pxr ON pheno.Id=pxr.PhenotypeId " - "INNER JOIN PublishData AS pd ON pxr.DataId=pd.Id " - "INNER JOIN Strain AS str ON pd.StrainId=str.Id " - "INNER JOIN StrainXRef AS sxr ON str.Id=sxr.StrainId " - "INNER JOIN PublishFreeze AS pf ON sxr.InbredSetId=pf.InbredSetId " - "INNER JOIN InbredSet AS iset ON pf.InbredSetId=iset.InbredSetId " - f"WHERE (pxr.InbredSetId, pheno.Id, pxr.Id) IN ({_paramstr}) " - "ORDER BY pheno.Id") - with conn.cursor(cursorclass=DictCursor) as cursor: - cursor.execute(_query, tuple(item for row in inbred_pheno_xref - for item in (row["population_id"], - row["phenoid"], - row["xref_id"]))) - debug_query(cursor, logger) - return tuple( - reduce(__organise_by_phenotype__, cursor.fetchall(), {}).values()) - - def __pre_process_phenotype_data__(row): _desc = row.get("description", "") _pre_pub_desc = row.get("pre_publication_description", _desc) @@ -375,7 +353,7 @@ def __pre_process_phenotype_data__(row): def create_new_phenotypes(# pylint: disable=[too-many-locals] - conn: mdb.Connection, + conn: Connection, population_id: int, publication_id: int, phenotypes: Iterable[dict] @@ -503,7 +481,7 @@ def create_new_phenotypes(# pylint: disable=[too-many-locals] def save_phenotypes_data( - conn: mdb.Connection, + conn: Connection, table: str, data: Iterable[dict] ) -> int: @@ -533,7 +511,7 @@ def save_phenotypes_data( def quick_save_phenotypes_data( - conn: mdb.Connection, + conn: Connection, table: str, dataitems: Iterable[dict], tmpdir: Path @@ -563,3 +541,134 @@ def quick_save_phenotypes_data( ")") debug_query(cursor, logger) return _count + + +def __sleep_random__(): + """Sleep a random amount of time chosen from 0.05s to 1s in increments of 0.05""" + time.sleep(random.choice(tuple(i / 20.0 for i in range(1, 21)))) + + +def delete_phenotypes_data( + cursor: BaseCursor, + data_ids: tuple[int, ...] +) -> tuple[int, int, int]: + """Delete numeric data for phenotypes with the given data IDs.""" + if len(data_ids) == 0: + return (0, 0, 0) + + # Loop to handle big deletes i.e. ≥ 10000 rows + _dcount, _secount, _ncount = (0, 0, 0)# Count total rows deleted + while True: + _paramstr = ", ".join(["%s"] * len(data_ids)) + cursor.execute( + "DELETE FROM PublishData " + f"WHERE Id IN ({_paramstr}) " + "ORDER BY Id ASC, StrainId ASC "# Make deletions deterministic + "LIMIT 1000", + data_ids) + _dcount_curr = cursor.rowcount + _dcount += _dcount_curr + + cursor.execute( + "DELETE FROM PublishSE " + f"WHERE DataId IN ({_paramstr}) " + "ORDER BY DataId ASC, StrainId ASC "# Make deletions deterministic + "LIMIT 1000", + data_ids) + _secount_curr = cursor.rowcount + _secount += _secount_curr + + cursor.execute( + "DELETE FROM NStrain " + f"WHERE DataId IN ({_paramstr}) " + "ORDER BY DataId ASC, StrainId ASC "# Make deletions deterministic + "LIMIT 1000", + data_ids) + _ncount_curr = cursor.rowcount + _ncount += _ncount_curr + __sleep_random__() + + if all((_dcount_curr == 0, _secount_curr == 0, _ncount_curr == 0)): + # end loop if there are no more rows to delete. + break + + return (_dcount, _secount, _ncount) + + +def __linked_ids__( + cursor: BaseCursor, + population_id: int, + xref_ids: tuple[int, ...] +) -> tuple[tuple[int, int, int], ...]: + """Retrieve `DataId` values from `PublishXRef` table.""" + _paramstr = ", ".join(["%s"] * len(xref_ids)) + cursor.execute("SELECT PhenotypeId, PublicationId, DataId " + "FROM PublishXRef " + f"WHERE InbredSetId=%s AND Id IN ({_paramstr})", + (population_id,) + xref_ids) + return tuple( + (int(row["PhenotypeId"]), int(row["PublicationId"]), int(row["DataId"])) + for row in cursor.fetchall()) + + +def delete_phenotypes( + conn_or_cursor: Union[Connection, Cursor], + population_id: int, + xref_ids: tuple[int, ...] +) -> tuple[int, int, int, int]: + """Delete phenotypes and all their data.""" + def __delete_phenos__(cursor: BaseCursor, pheno_ids: tuple[int, ...]) -> int: + """Delete data from the `Phenotype` table.""" + _paramstr = ", ".join(["%s"] * len(pheno_ids)) + + _pcount = 0 + while True: + cursor.execute( + "DELETE FROM Phenotype " + f"WHERE Id IN ({_paramstr}) " + "ORDER BY Id " + "LIMIT 1000", + pheno_ids) + _pcount_curr = cursor.rowcount + _pcount += _pcount_curr + __sleep_random__() + if _pcount_curr == 0: + break + + return cursor.rowcount + + def __delete_xrefs__(cursor: BaseCursor) -> int: + _paramstr = ", ".join(["%s"] * len(xref_ids)) + + _xcount = 0 + while True: + cursor.execute( + "DELETE FROM PublishXRef " + f"WHERE InbredSetId=%s AND Id IN ({_paramstr}) " + "ORDER BY Id " + "LIMIT 10000", + (population_id,) + xref_ids) + _xcount_curr = cursor.rowcount + _xcount += _xcount_curr + __sleep_random__() + if _xcount_curr == 0: + break + + return _xcount + + def __with_cursor__(cursor): + _phenoids, _pubids, _dataids = reduce( + lambda acc, curr: (acc[0] + (curr[0],), + acc[1] + (curr[1],), + acc[2] + (curr[2],)), + __linked_ids__(cursor, population_id, xref_ids), + (tuple(), tuple(), tuple())) + __delete_phenos__(cursor, _phenoids) + return (__delete_xrefs__(cursor),) + delete_phenotypes_data( + cursor, _dataids) + + if isinstance(conn_or_cursor, BaseCursor): + return __with_cursor__(conn_or_cursor) + + with conn_or_cursor.cursor(cursorclass=DictCursor) as cursor: + return __with_cursor__(cursor) diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py index 42f2e34..776fa52 100644 --- a/uploader/phenotypes/views.py +++ b/uploader/phenotypes/views.py @@ -31,12 +31,10 @@ from flask import (flash, from r_qtl import r_qtl2_qc as rqc from r_qtl import exceptions as rqe - -from uploader.sui import sui_template - from uploader import jobs from uploader import session from uploader.files import save_file +from uploader.configutils import uploads_dir from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.oauth2.client import oauth2_post @@ -197,7 +195,7 @@ def view_dataset(# pylint: disable=[unused-argument] start_at = max(safe_int(request.args.get("start_at") or 0), 0) count = int(request.args.get("count") or 20) - return render_template(sui_template("phenotypes/view-dataset.html"), + return render_template("phenotypes/view-dataset.html", species=species, population=population, dataset=dataset, @@ -332,7 +330,7 @@ def process_phenotypes_rqtl2_bundle(error_uri): try: ## Handle huge files here... phenobundle = save_file(request.files["phenotypes-bundle"], - Path(app.config["UPLOAD_FOLDER"])) + uploads_dir(app)) rqc.validate_bundle(phenobundle) return phenobundle except AssertionError as _aerr: @@ -355,7 +353,7 @@ def process_phenotypes_individual_files(error_uri): "comment.char": form["file-comment-character"], "na.strings": form["file-na"].split(" "), } - bundlepath = Path(app.config["UPLOAD_FOLDER"], + bundlepath = Path(uploads_dir(app), f"{str(uuid.uuid4()).replace('-', '')}.zip") with ZipFile(bundlepath,mode="w") as zfile: for rqtlkey, formkey, _type in ( @@ -373,7 +371,7 @@ def process_phenotypes_individual_files(error_uri): # Chunked upload of large files was used filedata = json.loads(form[formkey]) zfile.write( - Path(app.config["UPLOAD_FOLDER"], filedata["uploaded-file"]), + Path(uploads_dir(app), filedata["uploaded-file"]), arcname=filedata["original-name"]) cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filedata["original-name"]] else: @@ -385,9 +383,9 @@ def process_phenotypes_individual_files(error_uri): return error_uri filepath = save_file( - _sentfile, Path(app.config["UPLOAD_FOLDER"]), hashed=False) + _sentfile, uploads_dir(app), hashed=False) zfile.write( - Path(app.config["UPLOAD_FOLDER"], filepath), + Path(uploads_dir(app), filepath), arcname=filepath.name) cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filepath.name] @@ -420,9 +418,9 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p if request.method == "GET": today = datetime.date.today() return render_template( - sui_template("phenotypes/add-phenotypes-with-rqtl2-bundle.html" - if use_bundle - else "phenotypes/add-phenotypes-raw-files.html"), + ("phenotypes/add-phenotypes-with-rqtl2-bundle.html" + if use_bundle + else "phenotypes/add-phenotypes-raw-files.html"), species=species, population=population, dataset=dataset, @@ -467,7 +465,7 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p **({"publicationid": request.form["publication-id"]} if request.form.get("publication-id") else {})})}), _redisuri, - f"{app.config['UPLOAD_FOLDER']}/job_errors") + f"{uploads_dir(app)}/job_errors") app.logger.debug("JOB DETAILS: %s", _job) jobstatusuri = url_for("species.populations.phenotypes.job_status", @@ -509,7 +507,7 @@ def job_status( except jobs.JobNotFound as _jnf: job = None - return render_template(sui_template("phenotypes/job-status.html"), + return render_template("phenotypes/job-status.html", species=species, population=population, dataset=dataset, @@ -589,7 +587,7 @@ def review_job_data( for filetype,meta in metadata.items() } _job_metadata = json.loads(job["job-metadata"]) - return render_template(sui_template("phenotypes/review-job-data.html"), + return render_template("phenotypes/review-job-data.html", species=species, population=population, dataset=dataset, @@ -614,6 +612,11 @@ def load_phenotypes_success_handler(job): job_id=job["job_id"])) +def proceed_to_job_status(job): + app.logger.debug("The new job: %s", job) + return redirect(url_for("background-jobs.job_status", job_id=job["job_id"])) + + @phenotypesbp.route( "<int:species_id>/populations/<int:population_id>/phenotypes/datasets" "/<int:dataset_id>/load-data-to-database", @@ -656,11 +659,6 @@ def load_data_to_database( def __handle_error__(resp): return render_template("http-error.html", *resp.json()) - def __handle_success__(load_job): - app.logger.debug("The phenotypes loading job: %s", load_job) - return redirect(url_for( - "background-jobs.job_status", job_id=load_job["job_id"])) - return request_token( token_uri=urljoin(oauth2client.authserver_uri(), "auth/token"), @@ -682,15 +680,16 @@ def load_data_to_database( "success_handler": ( "uploader.phenotypes.views" ".load_phenotypes_success_handler") - }) + }, + external_id=session.logged_in_user_id()) ).then( lambda job: gnlibs_jobs.launch_job( job, _jobs_db, - Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"), + Path(f"{uploads_dir(app)}/job_errors"), worker_manager="gn_libs.jobs.launcher", loglevel=_loglevel) - ).either(__handle_error__, __handle_success__) + ).either(__handle_error__, proceed_to_job_status) def update_phenotype_metadata(conn, metadata: dict): @@ -980,7 +979,7 @@ def load_data_success( (_publication["Title"] or "")) if item != "") return render_template( - sui_template("phenotypes/load-phenotypes-success.html"), + "phenotypes/load-phenotypes-success.html", species=species, population=population, dataset=dataset, @@ -1062,9 +1061,10 @@ def recompute_means(# pylint: disable=[unused-argument] "success_handler": ( "uploader.phenotypes.views." "recompute_phenotype_means_success_handler") - }), + }, + external_id=session.logged_in_user_id()), _jobs_db, - Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"), + Path(f"{uploads_dir(app)}/job_errors"), worker_manager="gn_libs.jobs.launcher", loglevel=_loglevel) return redirect(url_for("background-jobs.job_status", @@ -1106,7 +1106,7 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument] _job_id = uuid.uuid4() _loglevel = logging.getLevelName(app.logger.getEffectiveLevel()).lower() - _workingdir = Path(app.config["TEMPORARY_DIRECTORY"]).joinpath("qtlreaper") + _workingdir = Path(app.config["SCRATCH_DIRECTORY"]).joinpath("qtlreaper") _workingdir.mkdir(exist_ok=True) command = [ sys.executable, @@ -1141,9 +1141,10 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument] "success_handler": ( "uploader.phenotypes.views." "rerun_qtlreaper_success_handler") - }), + }, + external_id=session.logged_in_user_id()), _jobs_db, - Path(f"{app.config['UPLOAD_FOLDER']}/job_errors"), + Path(f"{uploads_dir(app)}/job_errors"), worker_manager="gn_libs.jobs.launcher", loglevel=_loglevel) return redirect(url_for("background-jobs.job_status", @@ -1155,3 +1156,119 @@ def rerun_qtlreaper(# pylint: disable=[unused-argument] def rerun_qtlreaper_success_handler(job): """Handle success (re)running QTLReaper script.""" return return_to_dataset_view_handler(job, "QTLReaper ran successfully!") + + +def delete_phenotypes_success_handler(job): + """Handle success running the 'delete-phenotypes' script.""" + return return_to_dataset_view_handler( + job, "Phenotypes deleted successfully.") + + +@phenotypesbp.route( + "<int:species_id>/populations/<int:population_id>/phenotypes/datasets" + "/<int:dataset_id>/delete", + methods=["GET", "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 delete_phenotypes(# pylint: disable=[unused-argument] + species: dict, + population: dict, + dataset: dict, + **kwargs +): + """Delete the specified phenotype data.""" + _dataset_page = redirect(url_for( + "species.populations.phenotypes.view_dataset", + species_id=species["SpeciesId"], + population_id=population["Id"], + dataset_id=dataset["Id"])) + + def __handle_error__(resp): + flash( + "Error retrieving authorisation token. Phenotype deletion " + "failed. Please try again later.", + "alert alert-danger") + return _dataset_page + + _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"] + with (database_connection(app.config["SQL_URI"]) as conn, + sqlite3.connection(_jobs_db) as jobsconn): + form = request.form + xref_ids = tuple(int(item) for item in set(form.getlist("xref_ids"))) + + match form.get("action"): + case "cancel": + return redirect(url_for( + "species.populations.phenotypes.view_dataset", + species_id=species["SpeciesId"], + population_id=population["Id"], + dataset_id=dataset["Id"])) + case "delete": + _loglevel = logging.getLevelName( + app.logger.getEffectiveLevel()).lower() + if form.get("confirm_delete_all_phenotypes", "") == "on": + _cmd = ["--delete-all"] + else: + # setup phenotypes xref_ids file + _xref_ids_file = Path( + app.config["SCRATCH_DIRECTORY"], + f"delete-phenotypes-{uuid.uuid4()}.txt") + with _xref_ids_file.open(mode="w", encoding="utf8") as ptr: + ptr.write("\n".join(str(_id) for _id in xref_ids)) + + _cmd = ["--xref_ids_file", str(_xref_ids_file)] + + _job_id = uuid.uuid4() + 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( + jobsconn, + _job_id, + [ + sys.executable, + "-u", + "-m", + "scripts.phenotypes.delete_phenotypes", + "--log-level", _loglevel, + app.config["SQL_URI"], + str(species["SpeciesId"]), + str(population["Id"]), + str(dataset["Id"]), + app.config["AUTH_SERVER_URL"], + token["access_token"]] + _cmd, + "delete-phenotypes", + extra_meta={ + "species_id": species["SpeciesId"], + "population_id": population["Id"], + "dataset_id": dataset["Id"], + "success_handler": ( + "uploader.phenotypes.views." + "delete_phenotypes_success_handler") + }, + external_id=session.logged_in_user_id()) + ).then( + lambda _job: gnlibs_jobs.launch_job( + _job, + _jobs_db, + Path(f"{uploads_dir(app)}/job_errors"), + worker_manager="gn_libs.jobs.launcher", + loglevel=_loglevel) + ).either(__handle_error__, proceed_to_job_status) + case _: + _phenos = tuple() + if len(xref_ids) > 0: + _phenos = dataset_phenotypes( + conn, population["Id"], dataset["Id"], xref_ids=xref_ids) + + return render_template( + "phenotypes/confirm-delete-phenotypes.html", + species=species, + population=population, + dataset=dataset, + phenotypes=_phenos) diff --git a/uploader/platforms/views.py b/uploader/platforms/views.py index 0092760..ba0f0ef 100644 --- a/uploader/platforms/views.py +++ b/uploader/platforms/views.py @@ -8,8 +8,6 @@ from flask import ( Blueprint, current_app as app) -from uploader.sui import sui_template - from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer from uploader.authorisation import require_login @@ -62,7 +60,7 @@ def list_platforms(species_id: int): start_from = max(safe_int(request.args.get("start_from") or 0), 0) count = safe_int(request.args.get("count") or 20) return render_template( - sui_template("platforms/list-platforms.html"), + "platforms/list-platforms.html", species=species, platforms=enumerate_sequence( platforms_by_species(conn, @@ -89,7 +87,7 @@ def create_platform(species_id: int): if request.method == "GET": return render_template( - sui_template("platforms/create-platform.html"), + "platforms/create-platform.html", species=species, activelink="create-platform") diff --git a/uploader/population/views.py b/uploader/population/views.py index caee55b..795ce81 100644 --- a/uploader/population/views.py +++ b/uploader/population/views.py @@ -11,8 +11,6 @@ from flask import (flash, Blueprint, current_app as app) -from uploader.sui import sui_template - from uploader.samples.views import samplesbp from uploader.flask_extensions import url_for from uploader.oauth2.client import oauth2_post @@ -244,5 +242,4 @@ def view_population(species_id: int, population_id: int): dataset_phenotypes(conn, population["Id"], _dataset["Id"])) } - return render_template(sui_template("populations/view-population.html"), - **_kwargs) + return render_template("populations/view-population.html", **_kwargs) diff --git a/uploader/publications/views.py b/uploader/publications/views.py index 11732db..d9eb294 100644 --- a/uploader/publications/views.py +++ b/uploader/publications/views.py @@ -10,8 +10,6 @@ from flask import ( render_template, current_app as app) -from uploader.sui import sui_template - from uploader.flask_extensions import url_for from uploader.authorisation import require_login from uploader.route_utils import redirect_to_next @@ -32,7 +30,7 @@ pubbp = Blueprint("publications", __name__) @require_login def index(): """Index page for publications.""" - return render_template(sui_template("publications/index.html")) + return render_template("publications/index.html") @pubbp.route("/list", methods=["GET"]) @@ -74,7 +72,7 @@ def view_publication(publication_id: int): return redirect(url_for('publications.index')) return render_template( - sui_template("publications/view-publication.html"), + "publications/view-publication.html", publication=publication, linked_phenotypes=tuple(fetch_publication_phenotypes( conn, publication_id))) @@ -92,7 +90,7 @@ def create_publication(): if request.method == "GET": return render_template( - sui_template("publications/create-publication.html"), + "publications/create-publication.html", get_args=_get_args) form = request.form authors = form.get("publication-authors").encode("utf8") @@ -130,7 +128,7 @@ def edit_publication(publication_id: int): with database_connection(app.config["SQL_URI"]) as conn: if request.method == "GET": return render_template( - sui_template("publications/edit-publication.html"), + "publications/edit-publication.html", publication=fetch_publication_by_id(conn, publication_id), linked_phenotypes=tuple(fetch_publication_phenotypes( conn, publication_id)), @@ -181,12 +179,12 @@ def delete_publication(publication_id: int): flash("Cannot delete publication with linked phenotypes!", "alert-warning") return redirect(url_for( - sui_template("publications.view_publication"), + "publications.view_publication", publication_id=publication_id)) if request.method == "GET": return render_template( - sui_template("publications/delete-publication.html"), + "publications/delete-publication.html", publication=publication, linked_phenotypes=linked_phenotypes, publication_id=publication_id) diff --git a/uploader/samples/views.py b/uploader/samples/views.py index 93f0c29..ee002ba 100644 --- a/uploader/samples/views.py +++ b/uploader/samples/views.py @@ -1,19 +1,19 @@ """Code regarding samples""" -import os import sys import uuid +import logging from pathlib import Path -from redis import Redis from flask import (flash, request, redirect, Blueprint, current_app as app) -from uploader.sui import sui_template +from gn_libs import jobs +from gn_libs import sqlite3 -from uploader import jobs +from uploader import session from uploader.files import save_file from uploader.flask_extensions import url_for from uploader.ui import make_template_renderer @@ -25,8 +25,7 @@ from uploader.datautils import safe_int, enumerate_sequence from uploader.species.models import all_species, species_by_id from uploader.request_checks import with_species, with_population from uploader.db_utils import (with_db_connection, - database_connection, - with_redis_connection) + database_connection) from .models import samples_by_species_and_population @@ -88,7 +87,7 @@ def list_samples(species: dict, population: dict, **kwargs):# pylint: disable=[u total_samples = len(all_samples) offset = max(safe_int(request.args.get("from") or 0), 0) count = int(request.args.get("count") or 20) - return render_template(sui_template("samples/list-samples.html"), + return render_template("samples/list-samples.html", species=species, population=population, samples=all_samples[offset:offset+count], @@ -98,22 +97,6 @@ def list_samples(species: dict, population: dict, **kwargs):# pylint: disable=[u activelink="list-samples") -def build_sample_upload_job(# pylint: disable=[too-many-arguments, too-many-positional-arguments] - speciesid: int, - populationid: int, - samplesfile: Path, - separator: str, - firstlineheading: bool, - quotechar: str): - """Define the async command to run the actual samples data upload.""" - return [ - sys.executable, "-m", "scripts.insert_samples", app.config["SQL_URI"], - str(speciesid), str(populationid), str(samplesfile.absolute()), - separator, f"--redisuri={app.config['REDIS_URL']}", - f"--quotechar={quotechar}" - ] + (["--firstlineheading"] if firstlineheading else []) - - @samplesbp.route("<int:species_id>/populations/<int:population_id>/upload-samples", methods=["GET", "POST"]) @require_login @@ -149,7 +132,7 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma code=307) if request.method == "GET" or request.files.get("samples_file") is None: - return render_template(sui_template("samples/upload-samples.html"), + return render_template("samples/upload-samples.html", species=species, population=population) @@ -172,102 +155,29 @@ def upload_samples(species_id: int, population_id: int):#pylint: disable=[too-ma quotechar = (request.form.get("field_delimiter", '"') or '"') - redisuri = app.config["REDIS_URL"] - with Redis.from_url(redisuri, decode_responses=True) as rconn: - #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 - # populations already own the samples. - the_job = jobs.launch_job( + _jobs_db = app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"] + with sqlite3.connection(_jobs_db) as conn: + job = jobs.launch_job( jobs.initialise_job( - rconn, - jobs.jobsnamespace(), + conn, str(uuid.uuid4()), - build_sample_upload_job( - species["SpeciesId"], - population["InbredSetId"], - samples_file, + [ + sys.executable, "-m", "scripts.insert_samples", + app.config["SQL_URI"], + str(species["SpeciesId"]), + str(population["InbredSetId"]), + str(samples_file.absolute()), separator, - firstlineheading, - quotechar), + f"--quotechar={quotechar}" + ] + (["--firstlineheading"] if firstlineheading else []), "samples_upload", - app.config["JOBS_TTL_SECONDS"], - {"job_name": f"Samples Upload: {samples_file.name}"}), - redisuri, - f"{app.config['UPLOAD_FOLDER']}/job_errors") - return redirect(url_for( - "species.populations.samples.upload_status", - species_id=species_id, - population_id=population_id, - job_id=the_job["jobid"])) - - -@samplesbp.route("<int:species_id>/populations/<int:population_id>/" - "upload-samples/status/<uuid:job_id>", - methods=["GET"]) -@require_login -@with_population(species_redirect_uri="species.populations.samples.index", - redirect_uri="species.populations.samples.select_population") -def upload_status(species: dict, population: dict, job_id: uuid.UUID, **kwargs):# pylint: disable=[unused-argument] - """Check on the status of a samples upload job.""" - job = with_redis_connection(lambda rconn: jobs.job( - rconn, jobs.jobsnamespace(), job_id)) - if job: - status = job["status"] - if status == "success": - return render_template(sui_template("samples/upload-success.html"), - job=job, - species=species, - population=population,) - - if status == "error": - return redirect(url_for( - "species.populations.samples.upload_failure", - species_id=species["SpeciesId"], - population_id=population["Id"], - job_id=job_id)) - - error_filename = Path(jobs.error_filename( - job_id, f"{app.config['UPLOAD_FOLDER']}/job_errors")) - if error_filename.exists(): - stat = os.stat(error_filename) - if stat.st_size > 0: - return redirect(url_for( - "samples.upload_failure", job_id=job_id)) - - return render_template(sui_template("samples/upload-progress.html"), - species=species, - population=population, - job=job) # maybe also handle this? - - return render_template("no_such_job.html", - job_id=job_id, - species=species, - population=population), 400 - - -@samplesbp.route("<int:species_id>/populations/<int:population_id>/" - "upload-samples/failure/<uuid:job_id>", - methods=["GET"]) -@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):# 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)) - if not bool(job): - return render_template("no_such_job.html", job_id=job_id), 400 - - error_filename = Path(jobs.error_filename( - job_id, f"{app.config['UPLOAD_FOLDER']}/job_errors")) - if error_filename.exists(): - stat = os.stat(error_filename) - if stat.st_size > 0: - return render_template("worker_failure.html", job_id=job_id) - - return render_template(sui_template("samples/upload-failure.html"), - species=species, - population=population, - job=job) + extra_meta={ + "job_name": f"Samples Upload: {samples_file.name}" + }, + external_id=session.logged_in_user_id()), + _jobs_db, + Path(f"{app.config['UPLOAD_FOLDER']}/job_errors").absolute(), + loglevel=logging.getLevelName( + app.logger.getEffectiveLevel()).lower()) + return redirect( + url_for("background-jobs.job_status", job_id=job["job_id"])) diff --git a/uploader/session.py b/uploader/session.py index 5af5827..9872ceb 100644 --- a/uploader/session.py +++ b/uploader/session.py @@ -1,12 +1,15 @@ """Deal with user sessions""" +import logging from uuid import UUID, uuid4 from datetime import datetime from typing import Any, Optional, TypedDict +from flask import session from authlib.jose import KeySet -from flask import request, session from pymonad.either import Left, Right, Either +logger = logging.getLogger(__name__) + class UserDetails(TypedDict): """Session information relating specifically to the user.""" @@ -22,8 +25,6 @@ class SessionInfo(TypedDict): session_id: UUID user: UserDetails anon_id: UUID - user_agent: str - ip_addr: str masquerade: Optional[UserDetails] auth_server_jwks: Optional[dict[str, Any]] @@ -66,9 +67,6 @@ def session_info() -> SessionInfo: "logged_in": False }, "anon_id": anon_id, - "user_agent": request.headers.get("User-Agent"), - "ip_addr": request.environ.get("HTTP_X_FORWARDED_FOR", - request.remote_addr), "masquerading": None })) @@ -91,6 +89,17 @@ def user_details() -> UserDetails: """Retrieve user details.""" return session_info()["user"] + +def logged_in_user_id() -> Optional[UUID]: + """Get user id for logged in user. If user has not logged in, return None.""" + return user_token().then( + lambda _tok: user_details() + ).then( + lambda _user: Either(_user["user_id"], + (None, _user["email"] != "anon@ymous.user")) + ).either(lambda _err: None, lambda uid: uid) + + def user_token() -> Either: """Retrieve the user token.""" return session_info()["user"]["token"] diff --git a/uploader/species/views.py b/uploader/species/views.py index 9b14d01..4bfa7ae 100644 --- a/uploader/species/views.py +++ b/uploader/species/views.py @@ -8,8 +8,6 @@ from flask import (flash, Blueprint, current_app as app) -from uploader.sui import sui_template - from uploader.population import popbp from uploader.platforms import platformsbp from uploader.flask_extensions import url_for @@ -56,7 +54,7 @@ def view_species(species_id: int): species_id=species_id, population_id=population["Id"])) return render_template( - sui_template("species/view-species.html"), + "species/view-species.html", species=species, activelink="view-species", populations=populations_by_species(conn, species["SpeciesId"])) diff --git a/uploader/static/css/layout-common.css b/uploader/static/css/layout-common.css index 36a5735..9c9d034 100644 --- a/uploader/static/css/layout-common.css +++ b/uploader/static/css/layout-common.css @@ -1,3 +1,21 @@ * { box-sizing: border-box; } + +body { + display: grid; + grid-gap: 1em; +} + +#header { + margin: -0.7em; /* Fill entire length of screen */ + /* Define layout for the children elements */ + display: grid; +} + +#header #header-nav { + /* Place it in the parent element */ + grid-column-start: 1; + grid-column-end: 2; + display: flex; +} diff --git a/uploader/static/css/layout-large.css b/uploader/static/css/layout-large.css index 8abd2dd..2d53627 100644 --- a/uploader/static/css/layout-large.css +++ b/uploader/static/css/layout-large.css @@ -1,8 +1,6 @@ @media screen and (min-width: 20.1in) { body { - display: grid; grid-template-columns: 7fr 3fr; - grid-gap: 1em; } #header { @@ -12,7 +10,7 @@ /* Define layout for the children elements */ display: grid; - grid-template-columns: 8fr 2fr; + grid-template-columns: 1fr 9fr; } #header #header-text { @@ -45,6 +43,8 @@ grid-column-start: 1; grid-column-end: 3; padding: 0 3px; + + margin: -0.3em -0.7em 0 -0.7em; } #main #main-content { diff --git a/uploader/static/css/layout-medium.css b/uploader/static/css/layout-medium.css index 2cca711..50ceeb4 100644 --- a/uploader/static/css/layout-medium.css +++ b/uploader/static/css/layout-medium.css @@ -1,8 +1,6 @@ @media screen and (width > 8in) and (max-width: 20in) { body { - display: grid; grid-template-columns: 65fr 35fr; - grid-gap: 1em; } #header { @@ -12,7 +10,7 @@ /* Define layout for the children elements */ display: grid; - grid-template-columns: 8fr 2fr; + grid-template-columns: 2fr 8fr; } #header #header-text { @@ -51,7 +49,6 @@ /* 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%; diff --git a/uploader/static/css/layout-small.css b/uploader/static/css/layout-small.css index 80a3759..2e47217 100644 --- a/uploader/static/css/layout-small.css +++ b/uploader/static/css/layout-small.css @@ -2,7 +2,7 @@ body { display: grid; grid-template-columns: 1fr; - grid-template-rows: 1fr 2fr 7fr; + grid-template-rows: 1fr 90fr; grid-gap: 1em; } @@ -31,6 +31,11 @@ grid-column-end: 2; } + #header #header-nav ul { + display: grid; + grid-template-columns: 1fr; + } + #main { /* Place it in the parent element */ grid-column-start: 1; @@ -38,7 +43,7 @@ display: grid; /* Define layout for the children elements */ - grid-template-rows: 1.5em 80% 20%; + grid-template-rows: 1fr 80fr 20fr; grid-template-columns: 1fr; } diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css index bdac745..45e5d3d 100644 --- a/uploader/static/css/theme.css +++ b/uploader/static/css/theme.css @@ -8,24 +8,27 @@ body { #header { background-color: #336699; color: #FFFFFF; - border-radius: 3px; min-height: 30px; + border-bottom: solid black 1px; } #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; + text-decoration: none; } #main #breadcrumbs { - border-radius:3px; text-align: center; + background-color: #D5D5D5; + padding: 0 1em 0 1em; +} + +#main #breadcrumbs .breadcrumb { + padding-top: 0.5em; } #main #main-content { @@ -34,7 +37,7 @@ body { } #main #sidebar-content { - background: #EEEEEE; + background: #FEFEFE; border-radius: 5px; padding: 10px 5px; @@ -88,3 +91,7 @@ table.dataTable tbody tr.selected td { .breadcrumb-item { text-transform: Capitalize; } + +.breadcrumb-item a { + text-decoration: none; +} diff --git a/uploader/static/js/datatables.js b/uploader/static/js/datatables.js index 82fd696..bfcda2a 100644 --- a/uploader/static/js/datatables.js +++ b/uploader/static/js/datatables.js @@ -11,13 +11,36 @@ var addTableLength = (menuList, lengthToAdd, dataLength) => { var defaultLengthMenu = (data) => { menuList = [] - var lengths = [10, 25, 50, 100, 1000, data.length]; + var lengths = [10, 25, 50, 100, 1000]; + if(data.length > 1000) { + lengths.push(data.length) + } lengths.forEach((len) => { menuList = addTableLength(menuList, len, data.length); }); return menuList; }; +var setRowCheckableProperty = (node, state) => { + /** + * Set a row's (`node`) checkbox's or radio button's checked state to the + * boolean value `state`. + **/ + if(typeof(state) == "boolean") { + var pseudoclass = state == false ? ":checked" : ":not(:checked)"; + var checkable = ( + $(node).find(`input[type="checkbox"]${pseudoclass}`)[0] + || + $(node).find(`input[type="radio"]${pseudoclass}`)[0]); + $(checkable).prop("checked", state); + } else { + throw new Error("`state` *MUST* be a boolean value.") + } +}; + +var setRowChecked = (node) => {setRowCheckableProperty(node, true);}; +var setRowUnchecked = (node) => {setRowCheckableProperty(node, false);}; + var buildDataTable = (tableId, data = [], columns = [], userSettings = {}) => { var defaultSettings = { responsive: true, @@ -35,35 +58,40 @@ var buildDataTable = (tableId, data = [], columns = [], userSettings = {}) => { lengthMenu: "", info: "" }, - data: data, - columns: columns, - drawCallback: (settings) => { - $(this[0]).find("tbody tr").each((idx, row) => { - var arow = $(row); - var checkboxOrRadio = arow.find(".chk-row-select"); - if (checkboxOrRadio) { - if (arow.hasClass("selected")) { - checkboxOrRadio.prop("checked", true); - } else { - checkboxOrRadio.prop("checked", false); - } - } + drawCallback: function (settings) { + var api = this.api(); + api.rows({selected: true}).nodes().each((node, index) => { + setRowChecked(node); + }); + api.rows({selected: false}).nodes().each((node, index) => { + setRowUnchecked(node); }); } } var theDataTable = $(tableId).DataTable({ ...defaultSettings, - ...userSettings + ...userSettings, + ...(data.length == 0 ? {} : {data: data}), + ...(columns.length == 0 ? {} : {columns: columns}) }); - theDataTable.on("select", (event, datatable, type, cell, originalEvent) => { - datatable.rows({selected: true}).nodes().each((node, index) => { - $(node).find(".chk-row-select").prop("checked", true) - }); + theDataTable.on("select", (event, datatable, type, indexes) => { + datatable + .rows(indexes) + .nodes() + .each((node, index) => { + setRowChecked(node); + }); }); - theDataTable.on("deselect", (event, datatable, type, cell, originalEvent) => { - datatable.rows({selected: false}).nodes().each((node, index) => { - $(node).find(".chk-row-select").prop("checked", false) - }); + theDataTable.on("deselect", (event, datatable, type, indexes) => { + datatable + .rows(indexes) + .nodes() + .each(function(node, index) { + setRowUnchecked(node); + }); }); + + theDataTable.selectAll = () => {theDataTable.rows().select()}; + theDataTable.deselectAll = () => {theDataTable.rows().deselect()}; return theDataTable; }; diff --git a/uploader/static/js/utils.js b/uploader/static/js/utils.js index 1b31661..62d3662 100644 --- a/uploader/static/js/utils.js +++ b/uploader/static/js/utils.js @@ -28,7 +28,8 @@ var remove_class = (element, classvalue) => { var add_class = (element, classvalue) => { remove_class(element, classvalue); - element.attr("class", (element.attr("class") || "") + " " + classvalue); + element.attr("class", + ((element.attr("class") || "") + " " + classvalue).trim()); }; $(".not-implemented").click((event) => { diff --git a/uploader/sui.py b/uploader/sui.py deleted file mode 100644 index 8eb863d..0000000 --- a/uploader/sui.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Utilities for streamlined UI. This is a temporary module.""" -from flask import request - -def sui_template(template_path: str) -> str: - """Return the streamlined UI template for given template path.""" - _sui="sui-" if request.args.get("streamlined_ui") else "" - _parts = template_path.split("/") - return "/".join(_parts[:-1] + [f"{_sui}{_parts[-1]}"]) diff --git a/uploader/templates/background-jobs/base.html b/uploader/templates/background-jobs/base.html new file mode 100644 index 0000000..7201207 --- /dev/null +++ b/uploader/templates/background-jobs/base.html @@ -0,0 +1,10 @@ +{%extends "base.html"%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.list_jobs')}}"> + background jobs + </a> +</li> +{%endblock%} diff --git a/uploader/templates/background-jobs/default-success-page.html b/uploader/templates/background-jobs/default-success-page.html deleted file mode 100644 index 5732456..0000000 --- a/uploader/templates/background-jobs/default-success-page.html +++ /dev/null @@ -1,17 +0,0 @@ -{%extends "phenotypes/base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Background Jobs: Success{%endblock%} - -{%block pagetitle%}Background Jobs: Success{%endblock%} - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <p>Job <strong>{{job.job_id}}</strong>, - {%if job.get("metadata", {}).get("job-type")%} - of type '<em>{{job.metadata["job-type"]}}</em> - {%endif%}' completed successfully.</p> -</div> -{%endblock%} diff --git a/uploader/templates/background-jobs/delete-job.html b/uploader/templates/background-jobs/delete-job.html new file mode 100644 index 0000000..242c775 --- /dev/null +++ b/uploader/templates/background-jobs/delete-job.html @@ -0,0 +1,61 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"> + summary + </a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">background jobs: delete?</h2> + + <p class="text-danger">Are you sure you want to delete the job below?</p> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <form id="frm-delete-job" + method="POST" + action="{{url_for('background-jobs.delete_single', job_id=job.job_id)}}"> + <div class="row"> + <div class="col"> + <input type="submit" + class="btn btn-info" + value="cancel" + name="btn-confirm-delete" /> + </div> + <div class="col"> + <input type="submit" + class="btn btn-danger" + value="delete" + name="btn-confirm-delete" /> + </div> + </div> + </form> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>Confirm whether or not you want to delete job + <strong>{{job.job_id}}</strong>.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/job-status.html b/uploader/templates/background-jobs/job-status.html new file mode 100644 index 0000000..2e75c6d --- /dev/null +++ b/uploader/templates/background-jobs/job-status.html @@ -0,0 +1,45 @@ +{%extends "background-jobs/base.html"%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%from "flash_messages.html" import flash_all_messages%} + +{%block extrameta%} +<meta http-equiv="refresh" content="5" /> +{%endblock%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">job status</h2> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <div class="col"> + <a href="{{url_for('background-jobs.stop_job', job_id=job.job_id)}}" + title="Stop/Kill this job." + class="btn btn-danger">stop job</a> + </div> +</div> + +<div class="row"> + <h3 class="subheading">STDOUT</h3> + <div style="max-width: 40em; overflow: scroll"> + <pre>{{job["stdout"]}}</pre> + </div> +</div> + +<div class="row"> + <h3 class="subheading">STDERR</h3> + <div style="max-width: 40em; overflow: scroll"> + <pre>{{job["stderr"]}}</pre> + </div> +</div> + +{%endblock%} diff --git a/uploader/templates/background-jobs/job-summary.html b/uploader/templates/background-jobs/job-summary.html new file mode 100644 index 0000000..ef9ef6c --- /dev/null +++ b/uploader/templates/background-jobs/job-summary.html @@ -0,0 +1,75 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"> + summary + </a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">background jobs: summary</h2> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + {%if view_under_construction%} + <div class="col"> + <a href="#" + class="btn btn-info not-implemented" + title="Update the expiry date and time for this job.">update expiry</a> + </div> + + {%if job.metadata.status in ("stopped",)%} + <div class="col"> + <a href="#" + class="btn btn-warning not-implemented" + title="Create a new copy of this job, and run the copy.">Run Copy</a> + </div> + {%endif%} + {%endif%} + + <div class="col"> + <a href="{{url_for('background-jobs.delete_single', job_id=job.job_id)}}" + class="btn btn-danger" + title="Delete this job.">delete</a> + </div> +</div> + +<div class="row"> + <h3 class="subheading">Script Errors and Logging</h3> + <div style="max-width: 40em; overflow: scroll"> + <pre>{{job["stderr"]}}</pre> + </div> +</div> + +<div class="row"> + <h3 class="subheading">Script Output</h3> + <div style="max-width: 40em; overflow: scroll"> + <pre>{{job["stdout"]}}</pre> + </div> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>This page shows the results of running job '{{job.job_id}}'.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/list-jobs.html b/uploader/templates/background-jobs/list-jobs.html new file mode 100644 index 0000000..c16b850 --- /dev/null +++ b/uploader/templates/background-jobs/list-jobs.html @@ -0,0 +1,79 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"><h2 class="heading">Background Jobs</h2></div> + +<div class="row"> + <div class="table-responsive"> + <table class="table"> + <thead> + <tr class="table-primary"> + <th>Type</th> + <th>Created</th> + <th title="Date and time past which the job's details will be deleted from the system."> + Expires</th> + <th>Status</th> + <th>Actions</th> + </tr> + </thead> + + <tbody> + {%for job in jobs%} + <tr> + <td>{{job.metadata["job-type"]}}</td> + <td>{{display_datetime(job.created)}}</td> + <td title="Date and time past which the job's details will be deleted from the system."> + {{display_datetime(job.expires)}} + </td> + <td {%if job.metadata.status == "completed"%} + class="fw-bold text-capitalize text-success" + {%elif job.metadata.status == "error"%} + class="fw-bold text-capitalize text-danger" + {%elif job.metadata.status == "stopped"%} + class="fw-bold text-capitalize text-warning" + {%else%} + class="fw-bold text-capitalize text-info" + {%endif%}> + <div> + {{job.metadata.status}} + </div> + </td> + <td> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}" + class="btn btn-info" + title="View more detailed information about this job."> + view summary</a> + </td> + </tr> + {%else%} + <tr> + <td colspan="5"> + You do not have any jobs you have run in the background.</td> + </tr> + {%endfor%} + </tbody> + </table> + </div> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>The table lists the jobs that are running in the background, that you + started.</p> + <p>You can use the tools provided on this page to manage the jobs, and to view + each job's details.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/macro-display-job-details.html b/uploader/templates/background-jobs/macro-display-job-details.html new file mode 100644 index 0000000..82e33c0 --- /dev/null +++ b/uploader/templates/background-jobs/macro-display-job-details.html @@ -0,0 +1,29 @@ +{%macro display_job_details(job, display_datetime)%} +<table class="table"> + <thead> + </thead> + + <tbody> + <tr> + <th class="table-primary">Job ID</th> + <td>{{job.job_id}}</td> + </tr> + <tr> + <th class="table-primary">Type</th> + <td>{{job.metadata["job-type"]}}</td> + </tr> + <tr> + <th class="table-primary">Created</th> + <td>{{display_datetime(job.created)}}</td> + </tr> + <tr> + <th class="table-primary">Expires</th> + <td>{{display_datetime(job.expires)}}</td> + </tr> + <tr> + <th class="table-primary">Status</th> + <td>{{job.metadata.status}}</td> + </tr> + </tbody> +</table> +{%endmacro%} diff --git a/uploader/templates/background-jobs/stop-job.html b/uploader/templates/background-jobs/stop-job.html new file mode 100644 index 0000000..fc190ac --- /dev/null +++ b/uploader/templates/background-jobs/stop-job.html @@ -0,0 +1,61 @@ +{%extends "background-jobs/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} +{%from "background-jobs/macro-display-job-details.html" import display_job_details%} + +{%block title%}Background Jobs{%endblock%} + +{%block pagetitle%}Background Jobs{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('background-jobs.job_summary', job_id=job.job_id)}}"> + summary + </a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <h2 class="heading">background jobs: stop?</h2> + + <p class="text-danger">Are you sure you want to stop the job below?</p> + + {{display_job_details(job, display_datetime)}} +</div> + +<div class="row"> + <form id="frm-stop-job" + method="POST" + action="{{url_for('background-jobs.stop_job', job_id=job.job_id)}}"> + <div class="row"> + <div class="col"> + <input type="submit" + class="btn btn-info" + value="cancel" + name="btn-confirm-stop" /> + </div> + <div class="col"> + <input type="submit" + class="btn btn-danger" + value="stop" + name="btn-confirm-stop" /> + </div> + </div> + </form> +</div> +{%endblock%} + + +{%block sidebarcontents%} +<div class="row"> + <h6 class="subheading">What is this?</h6> +</div> +<div class="row"> + <p>Confirm whether or not you want to stop job + <strong>{{job.job_id}}</strong>.</p> +</div> +{{super()}} +{%endblock%} diff --git a/uploader/templates/background-jobs/sui-default-success-page.html b/uploader/templates/background-jobs/sui-default-success-page.html deleted file mode 100644 index 5732456..0000000 --- a/uploader/templates/background-jobs/sui-default-success-page.html +++ /dev/null @@ -1,17 +0,0 @@ -{%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 d521ccb..ae4ecef 100644 --- a/uploader/templates/base.html +++ b/uploader/templates/base.html @@ -16,7 +16,11 @@ <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/styles.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%} @@ -26,14 +30,32 @@ <header id="header"> <span id="header-text">GeneNetwork</span> <nav id="header-nav"> - <ul class="nav justify-content-end"> + <ul class="nav"> + {%if user_logged_in()%} + <li> + <a href="{{url_for('background-jobs.list_jobs')}}" + title="User's background jobs."> + <!-- https://icons.getbootstrap.com/icons/back/ --> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-back" viewBox="0 0 16 16"> + <path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/> + </svg> + Background jobs + </a> + </li> + <li> - {%if user_logged_in()%} <a href="{{url_for('oauth2.logout')}}" title="Log out of the system"> + <!-- https://icons.getbootstrap.com/icons/file-person/ --> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-person" viewBox="0 0 16 16"> + <path d="M12 1a1 1 0 0 1 1 1v10.755S12 11 8 11s-5 1.755-5 1.755V2a1 1 0 0 1 1-1zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/> + <path d="M8 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/> + </svg> <span class="glyphicon glyphicon-user"></span> - {{user_email()}} Sign Out</a> - {%else%} + Sign Out ({{user_email()}})</a> + </li> + {%else%} + <li> <a href="{{authserver_authorise_uri()}}" title="Log in to the system">Sign In</a> {%endif%} @@ -42,91 +64,29 @@ </nav> </header> - <aside id="nav-sidebar"> - <ul class="nav flex-column"> - <li {%if activemenu=="home"%}class="activemenu"{%endif%}> - <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> - <li {%if activemenu=="species"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.list_species')}}" - title="View and manage species information.">Species</a></li> - <li {%if activemenu=="platforms"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.platforms.index')}}" - title="View and manage species platforms.">Sequencing Platforms</a></li> - <li {%if activemenu=="populations"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.populations.index')}}" - title="View and manage species populations.">Populations</a></li> - <li {%if activemenu=="samples"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.populations.samples.index')}}" - title="Upload population samples.">Samples</a></li> - <li {%if activemenu=="genotypes"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.populations.genotypes.index')}}" - title="Upload Genotype data.">Genotype Data</a></li> - <!-- - TODO: Maybe include menus here for managing studies and dataset or - maybe have the studies/datasets managed under their respective - sections, e.g. "Publish*" studies/datasets under the "Phenotypes" - section, "ProbeSet*" studies/datasets under the "Expression Data" - sections, etc. - --> - <li {%if activemenu=="phenotypes"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.populations.phenotypes.index')}}" - title="Upload phenotype data.">Phenotype Data</a></li> - <!-- - <li {%if activemenu=="expression-data"%}class="activemenu"{%endif%}> - <a href="{{url_for('species.populations.expression-data.index')}}" - title="Upload expression data." - class="not-implemented">Expression Data</a></li> - <li {%if activemenu=="individuals"%}class="activemenu"{%endif%}> - <a href="#" - class="not-implemented" - title="Upload individual data.">Individual Data</a></li> - <li {%if activemenu=="rna-seq"%}class="activemenu"{%endif%}> - <a href="#" - class="not-implemented" - title="Upload RNA-Seq data.">RNA-Seq Data</a></li> - <li {%if activemenu=="async-jobs"%}class="activemenu"{%endif%}> - <a href="#" - class="not-implemented" - title="View and manage the backgroud jobs you have running"> - Background Jobs</a></li> - --> - </ul> - </aside> <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="pagetitle" class="pagetitle"> - <span class="title">Data Upload and Quality Control: {%block pagetitle%}{%endblock%}</span> - <!-- - <nav> - <ol class="breadcrumb"> - <li {%if activelink is not defined or activelink=="home"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('base.index')}}">Home</a> - </li> - {%block lvl1_breadcrumbs%}{%endblock%} - </ol> - </nav> - --> - </div> - - <div id="all-content"> - <div id="main-content"> + <div id="main-content"> {%block contents%}{%endblock%} </div> - <div id="sidebar-content"> + + <div id="sidebar-content"> {%block sidebarcontents%}{%endblock%} </div> - </div> </main> + + <script type="text/javascript" src="/static/js/debug.js"></script> <!-- Core dependencies --> diff --git a/uploader/templates/genotypes/base.html b/uploader/templates/genotypes/base.html index 7d61312..8d1b951 100644 --- a/uploader/templates/genotypes/base.html +++ b/uploader/templates/genotypes/base.html @@ -1,23 +1,18 @@ {%extends "populations/base.html"%} +{%from "populations/macro-display-population-card.html" import display_sui_population_card%} -{%block lvl3_breadcrumbs%} -<li {%if activelink=="genotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - {%if population is mapping%} +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.genotypes.list_genotypes', - species_id=species.SpeciesId, - population_id=population.Id)}}"> - {%if dataset is defined and dataset is mapping%} - {{dataset.Name}} - {%else%} - Genotypes - {%endif%}</a> - {%else%} - <a href="{{url_for('species.populations.genotypes.index')}}">Genotypes</a> - {%endif%} + species_id=species['SpeciesId'], + population_id=population['Id'])}}"> + genotype + </a> </li> -{%block lvl4_breadcrumbs%}{%endblock%} +{%endblock%} + + +{%block sidebarcontents%} +{{display_sui_population_card(species, population)}} {%endblock%} diff --git a/uploader/templates/genotypes/list-genotypes.html b/uploader/templates/genotypes/list-genotypes.html index 0f074fd..a2b98c8 100644 --- a/uploader/templates/genotypes/list-genotypes.html +++ b/uploader/templates/genotypes/list-genotypes.html @@ -1,23 +1,10 @@ {%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()}} @@ -143,7 +130,3 @@ all the rest.</p> </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 index a705ae3..5f3dd6f 100644 --- a/uploader/templates/genotypes/list-markers.html +++ b/uploader/templates/genotypes/list-markers.html @@ -1,20 +1,18 @@ {%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%}> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.genotypes.list_markers', - species_id=species.SpeciesId, - population_id=population.Id)}}">List markers</a> + species_id=species['SpeciesId'], + population_id=population['Id'])}}"> + markers + </a> </li> {%endblock%} @@ -99,7 +97,3 @@ </div> {%endif%} {%endblock%} - -{%block sidebarcontents%} -{{display_species_card(species)}} -{%endblock%} diff --git a/uploader/templates/genotypes/view-dataset.html b/uploader/templates/genotypes/view-dataset.html index e7ceb36..1c4eccf 100644 --- a/uploader/templates/genotypes/view-dataset.html +++ b/uploader/templates/genotypes/view-dataset.html @@ -1,21 +1,17 @@ {%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: View Dataset{%endblock%} {%block pagetitle%}Genotypes: View Dataset{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="view-dataset"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.genotypes.view_dataset', species_id=species.SpeciesId, population_id=population.Id, - dataset_id=dataset.Id)}}">view dataset</a> + dataset_id=dataset.Id)}}">dataset</a> </li> {%endblock%} @@ -55,7 +51,3 @@ </div> {%endblock%} - -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/index.html b/uploader/templates/index.html index aa1414e..e426732 100644 --- a/uploader/templates/index.html +++ b/uploader/templates/index.html @@ -1,107 +1,162 @@ {%extends "base.html"%} {%from "flash_messages.html" import flash_all_messages%} +{%from "macro-forms.html" import add_http_feature_flags%} {%block title%}Home{%endblock%} {%block pagetitle%}Home{%endblock%} -{%block contents%} - -<div class="row"> - {{flash_all_messages()}} - <div class="explainer"> - <p>Welcome to the <strong>GeneNetwork Data Upload and Quality Control - System</strong>.</p> - <p>This tool helps you prepare and upload research data to GeneNetwork for - analysis.</p> - - <h2 class="heading">Getting Started</h2> - <p>The sections below explain the features of the system. Review this guide - to learn how to use the system.</p> - - {%block extrapageinfo%}{%endblock%} - - <h3 class="subheading">Species</h3> - - <p>GeneNetwork supports genetic studies across multiple species (e.g. mice - [Mus musculus], human [homo sapiens], rats [Rattus norvegicus], etc.) . - Here you can:</p> - <ul> - <li>View all species that are currently supported</li> - <li>Add new species not yet in the system</li> - </ul> - - <h3 class="subheading">Populations</h3> - - <p>A "population" refers to a specific subgroup within a species that you’re - studying (e.g., BXD mice). Here you can:</p> - <ul> - <li>View the populations that exist for a selected species</li> - <li>Add new populations of study for a selected species</li> - </ul> - - <h3 class="subheading">Samples</h3> - - <p>Manage individual specimens or cases used in your experiments. These - include:</p> - - <ul> - <li>Experimental subjects</li> - <li>Data sources (e.g., tissue samples, clinical cases)</li> - <li>Strain means (instead of entering multiple BXD1 individuals, for - example, the mean would be entered for a single BXD1 strain)</li> - </ul> - - - <h3 class="subheading">Genotype Data</h3> - - <p>Upload and review genetic markers and allele encodings for your - population. Key details:</p> +{%block extra_breadcrumbs%}{%endblock%} - <ul> - <li>Markers are species-level (e.g., mouse SNP databases).</li> - <li>Allele data is population-specific (tied to your experimental - samples).</li> - </ul> - - <p><strong>Requirement</strong>: Samples must already have been registered - in the system before uploading genotype data.</p> - - <h3 class="subheading">Phenotype Data</h3> +{%block contents%} - <p>Phenotypes are the visible traits or features of a living thing. For - example, phenotypes include:</p> +{%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%} - <ul> - <li>Weight</li> - <li>Height</li> - <li>Color (such as the color of fur or eyes)</li> - </ul> +<div class="row">{{flash_all_messages()}}</div> - <p>This part of the system will allow you to upload and manage the values - for different phenotypes from various samples in your studies.</p> +{%if user_logged_in()%} - <!-- +<div class="row"> + <ul class="nav nav-tabs" id="index-actions"> + <li class="nav-item presentation"> + <button class="nav-link active" + id="upload-data-tab" + data-bs-toggle="tab" + data-bs-target="#upload-data-content" + type="button" + role="tab" + aria-controls="upload-data-content" + aria-selected="false">Upload Data</button></li> + <li class="nav-item presentation"> + <button class="nav-link" + id="publications-tab" + data-bs-toggle="tab" + data-bs-target="#publications-content" + type="button" + role="tab" + aria-controls="publications-content" + aria-selected="true">Publications</button></li> + </ul> +</div> - <h3 class="subheading">Expression Data</h3> +<div class="row"> + <div class="tab-content" id="upload-data-tabs-content"> + <div class="tab-pane fade show active" + id="upload-data-content" + role="tabpanel" + aria-labelledby="upload-data-content-tab"> + <h2 class="heading">Species</h2> + + <p>Select the species you want to work with.</p> + + <form method="GET" action="{{url_for('base.index')}}" class="form-horizontal"> + {{add_http_feature_flags()}} + + {{add_form_buttons()}} + + {%if species | length != 0%} + <div style="margin-top:1em;"> + <table id="tbl-select-species" class="table compact stripe" + data-species-list='{{species | tojson}}'> + <thead> + <tr> + <th></th> + <th>Species Name</th> + </tr> + </thead> + + <tbody></tbody> + </table> + </div> + + {%else%} + + <label class="control-label" for="rdo-cant-find-species"> + <input id="rdo-cant-find-species" type="radio" name="species_id" + value="CREATE-SPECIES" /> + There are no species to select from. Create the first one.</label> + + <div class="col-sm-offset-10 col-sm-2"> + <input type="submit" + class="btn btn-primary col-sm-offset-1" + value="continue" /> + </div> + + {%endif%} + + {{add_form_buttons()}} + + </form> + </div> + + <div class="tab-pane fade" + id="publications-content" + role="tabpanel" + aria-labelledby="publications-content-tab"> + <p>View, edit and delete existing publications, and add new + publications by clicking on the button below.</p> + + <a href="{{url_for('publications.index')}}" + title="Manage publications." + class="btn btn-primary">manage publications</a> + </div> + </div> +</div> - <p class="text-danger"> - <span class="glyphicon glyphicon-exclamation-sign"></span> - <strong>TODO</strong>: Document this …</p> + {%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%} - <h3 class="subheading">Individual Data</h3> + {%endblock%} - <p class="text-danger"> - <span class="glyphicon glyphicon-exclamation-sign"></span> - <strong>TODO</strong>: Document this …</p> - <h3 class="subheading">RNA-Seq Data</h3> - <p class="text-danger"> - <span class="glyphicon glyphicon-exclamation-sign"></span> - <strong>TODO</strong>: Document this …</p> + {%block sidebarcontents%} + {%if view_under_construction%} + <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> + <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> + {%endif%} + {%endblock%} + -{%endblock%} + {%block javascript%} + <script type="text/javascript" src="/static/js/species.js"></script> + {%endblock%} diff --git a/uploader/templates/jobs/sui-job-error.html b/uploader/templates/jobs/sui-job-error.html deleted file mode 100644 index 1a839a6..0000000 --- a/uploader/templates/jobs/sui-job-error.html +++ /dev/null @@ -1,17 +0,0 @@ -{%extends "sui-base.html"%} - -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Background Jobs: Error{%endblock%} - -{%block pagetitle%}Background Jobs: Error{%endblock%} - -{%block contents%} - -<h1>Background Jobs: Error</h1> -<p>Job <strong>{{job["job_id"]}}</strong> failed!</p> -<p>The error details are in the "STDERR" section below.</p> - -<h2>STDERR</h2> -<pre>{{job["stderr"]}}</pre> -{%endblock%} diff --git a/uploader/templates/jobs/sui-job-not-found.html b/uploader/templates/jobs/sui-job-not-found.html deleted file mode 100644 index 96c8586..0000000 --- a/uploader/templates/jobs/sui-job-not-found.html +++ /dev/null @@ -1,11 +0,0 @@ -{%extends "sui-base.html"%} - -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Background Jobs{%endblock%} - -{%block pagetitle%}Background Jobs{%endblock%} - -{%block contents%} -<p>Could not find job with ID: {{job_id}}</p> -{%endblock%} diff --git a/uploader/templates/jobs/sui-job-status.html b/uploader/templates/jobs/sui-job-status.html deleted file mode 100644 index fc5e532..0000000 --- a/uploader/templates/jobs/sui-job-status.html +++ /dev/null @@ -1,24 +0,0 @@ -{%extends "sui-base.html"%} - -{%from "flash_messages.html" import flash_all_messages%} - -{%block extrameta%} -<meta http-equiv="refresh" content="5" /> -{%endblock%} - -{%block title%}Background Jobs{%endblock%} - -{%block pagetitle%}Background Jobs{%endblock%} - -{%block contents%} - -<p>Status: {{job["metadata"]["status"]}}</p> -<p>Job Type: {{job["metadata"]["job-type"]}}</p> - -<h2>STDOUT</h2> -<pre>{{job["stdout"]}}</pre> - -<h2>STDERR</h2> -<pre>{{job["stderr"]}}</pre> - -{%endblock%} diff --git a/uploader/templates/login.html b/uploader/templates/login.html deleted file mode 100644 index e76c644..0000000 --- a/uploader/templates/login.html +++ /dev/null @@ -1,12 +0,0 @@ -{%extends "index.html"%} - -{%block title%}Data Upload{%endblock%} - -{%block pagetitle%}log in{%endblock%} - -{%block extrapageinfo%} -<p class="text-dark"> - You <strong>need to - <a href="{{authserver_authorise_uri()}}" - title="Sign in to the system">sign in</a></strong> to use this system.</p> -{%endblock%} diff --git a/uploader/templates/phenotypes/add-phenotypes-base.html b/uploader/templates/phenotypes/add-phenotypes-base.html index 9909c20..c74a0fa 100644 --- a/uploader/templates/phenotypes/add-phenotypes-base.html +++ b/uploader/templates/phenotypes/add-phenotypes-base.html @@ -1,26 +1,13 @@ {%extends "phenotypes/base.html"%} {%from "flash_messages.html" import flash_all_messages%} {%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%} {%block title%}Phenotypes{%endblock%} {%block pagetitle%}Phenotypes{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="add-phenotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">Add Phenotypes</a> -</li> -{%endblock%} - {%block contents%} +{{super()}} {{flash_all_messages()}} <div class="row"> @@ -42,8 +29,7 @@ {%block frm_add_phenotypes_elements%}{%endblock%} - <fieldset id="fldset-publication-info"> - <legend>Publication Information</legend> + <h4>Publication Information</h4> <input type="hidden" name="publication-id" id="txt-publication-id" /> <span class="form-text text-muted"> Select a publication for your data. <br /> @@ -66,7 +52,6 @@ <tbody></tbody> </table> - </fieldset> <div class="form-group"> <input type="submit" @@ -83,6 +68,8 @@ {%endblock%} + + {%block javascript%} <script type="text/javascript"> $(function() { diff --git a/uploader/templates/phenotypes/add-phenotypes-raw-files.html b/uploader/templates/phenotypes/add-phenotypes-raw-files.html index 67b56e3..b1322b2 100644 --- a/uploader/templates/phenotypes/add-phenotypes-raw-files.html +++ b/uploader/templates/phenotypes/add-phenotypes-raw-files.html @@ -1,7 +1,6 @@ {%extends "phenotypes/add-phenotypes-base.html"%} {%from "flash_messages.html" import flash_all_messages%} {%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%} {%from "phenotypes/macro-display-preview-table.html" import display_preview_table%} {%from "phenotypes/macro-display-resumable-elements.html" import display_resumable_elements%} @@ -9,19 +8,6 @@ {%block pagetitle%}Phenotypes{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="add-phenotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">Add Phenotypes</a> -</li> -{%endblock%} - {%block frm_add_phenotypes_documentation%} <p>This page will allow you to upload all the separate files that make up your phenotypes. Here, you will have to upload each separate file individually. If @@ -35,8 +21,7 @@ {%endblock%} {%block frm_add_phenotypes_elements%} -<fieldset id="fldset-file-metadata"> - <legend>File(s) Metadata</legend> + <h4>File(s) Metadata</h4> <div class="form-group"> <label for="txt-file-separator" class="form-label">File Separator</label> <div class="input-group"> @@ -103,12 +88,9 @@ <a href="#docs-file-na" title="Documentation for no-value fields"> documentation for more information</a>.</span> </div> -</fieldset> -<fieldset id="fldset-files"> <legend>Data File(s)</legend> - <fieldset id="fldset-descriptions-file"> <div class="form-group"> <div class="form-check"> <input id="chk-phenotype-descriptions-transposed" @@ -159,10 +141,8 @@ {{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" @@ -210,11 +190,9 @@ on the expected format for the file provided here.</p>')}} {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}} </div> - </fieldset> {%if population.Family in families_with_se_and_n%} - <fieldset id="fldset-se-file"> <div class="form-group"> <div class="form-check"> <input id="chk-phenotype-se-transposed" @@ -261,10 +239,8 @@ {{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" @@ -311,8 +287,6 @@ {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}} </div> - </fieldset> -</fieldset> {%endif%} {%endblock%} @@ -447,10 +421,6 @@ {%endblock%} -{%block sidebarcontents%} -{{display_pheno_dataset_card(species, population, dataset)}} -{%endblock%} - {%block more_javascript%} <script src="{{url_for('base.node_modules', @@ -495,7 +465,7 @@ .map((field) => { var value = field.trim(); if(navalues.includes(value)) { - return "⋘NULâ‹™"; + return "[NO-VALUE]"; } return value; }) diff --git a/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html b/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html index 898fc0c..4afd6ab 100644 --- a/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html +++ b/uploader/templates/phenotypes/add-phenotypes-with-rqtl2-bundle.html @@ -1,25 +1,11 @@ {%extends "phenotypes/add-phenotypes-base.html"%} {%from "flash_messages.html" import flash_all_messages%} {%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%} {%block title%}Phenotypes{%endblock%} {%block pagetitle%}Phenotypes{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="add-phenotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">Add Phenotypes</a> -</li> -{%endblock%} - {%block frm_add_phenotypes_documentation%} <p>Select the zip file bundle containing information on the phenotypes you wish to upload, then click the "Upload Phenotypes" button below to @@ -201,7 +187,3 @@ <em>phenotypes × individuals</em>.</p> </div> {%endblock%} - -{%block sidebarcontents%} -{{display_pheno_dataset_card(species, population, dataset)}} -{%endblock%} diff --git a/uploader/templates/phenotypes/base.html b/uploader/templates/phenotypes/base.html index adbc012..fe7ccd3 100644 --- a/uploader/templates/phenotypes/base.html +++ b/uploader/templates/phenotypes/base.html @@ -1,19 +1,25 @@ {%extends "populations/base.html"%} +{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_sui_pheno_dataset_card%} -{%block lvl3_breadcrumbs%} -<li {%if activelink=="phenotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - {%if dataset is mapping%} +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.phenotypes.view_dataset', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">{{dataset.Name}}</a> - {%else%} - <a href="{{url_for('species.populations.phenotypes.index')}}">Phenotypes</a> - {%endif%} + species_id=species['SpeciesId'], + population_id=population['Id'], + dataset_id=dataset['Id'])}}"> + {{dataset["Name"]}} + </a> </li> -{%block lvl4_breadcrumbs%}{%endblock%} +{%endblock%} + +{%block contents%} +<div class="row"> + <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2> +</div> +{%endblock%} + + +{%block sidebarcontents%} +{{display_sui_pheno_dataset_card(species, population, dataset)}} {%endblock%} diff --git a/uploader/templates/phenotypes/confirm-delete-phenotypes.html b/uploader/templates/phenotypes/confirm-delete-phenotypes.html new file mode 100644 index 0000000..e6d67c7 --- /dev/null +++ b/uploader/templates/phenotypes/confirm-delete-phenotypes.html @@ -0,0 +1,196 @@ +{%extends "phenotypes/base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Phenotypes{%endblock%} + +{%block pagetitle%}Delete Phenotypes{%endblock%} + +{%block lvl4_breadcrumbs%} +<li {%if activelink=="view-dataset"%} + class="breadcrumb-item active" + {%else%} + class="breadcrumb-item" + {%endif%}> + <a href="{{url_for('species.populations.phenotypes.view_dataset', + species_id=species.SpeciesId, + population_id=population.Id, + dataset_id=dataset.Id)}}">View</a> +</li> +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"><h2>Delete Phenotypes</h2></div> + +{%if phenotypes | length > 0%} +<div class="row"> + <p>You have requested to delete the following phenotypes:</p> +</div> + +<div class="row"> + <div class="col"> + <a id="btn-select-all-phenotypes" + href="#" + class="btn btn-info" + title="Select all phenotypes">select all</a> + </div> + <div class="col"> + <a id="btn-deselect-all-phenotypes" + href="#" + class="btn btn-warning" + title="Deselect all phenotypes">deselect all</a> + </div> +</div> + +<div class="row"> + <table id="tbl-delete-phenotypes" class="table"> + <thead> + <tr> + <th>#</th> + <th>Record ID</th> + <th>Description</th> + </tr> + </thead> + <tbody> + {%for phenotype in phenotypes%} + <tr> + <td> + <input id="chk-xref-id-{{phenotype.xref_id}}" + name="xref_ids" + type="checkbox" + value="{{phenotype.xref_id}}" + class="chk-row-select" /> + </td> + <td>{{phenotype.xref_id}}</td> + <td>{{phenotype.Post_publication_description or + phenotype.Pre_publication_description or + phenotype.original_description}}</td> + </tr> + {%endfor%} + </tbody> + </table> +</div> + +<div class="row"> + <form id="frm-delete-phenotypes-selected" + method="POST" + action="{{url_for('species.populations.phenotypes.delete_phenotypes', + species_id=species.SpeciesId, + population_id=population.Id, + dataset_id=dataset.Id)}}"> + <div class="row"> + <div class="col"> + <input class="btn btn-info" + type="submit" + title="Cancel delete and return to dataset page." + name="action" + value="cancel" /></div> + <div class="col"> + <input id="btn-delete-phenotypes-selected" + class="btn btn-danger" + type="submit" + title="Delete the selected phenotypes from this dataset." + name="action" + value="delete" /> + </div> + </div> + </form> +</div> +{%else%} +<div class="row"> + <p>You did not select any phenotypes to delete. Delete everything?</p> +</div> + +<div class="row"> + <form id="frm-delete-phenotypes-all" + method="POST" + action="{{url_for('species.populations.phenotypes.delete_phenotypes', + species_id=species.SpeciesId, + population_id=population.Id, + dataset_id=dataset.Id)}}"> + <div class="form-check"> + <input class="form-check-input" + type="checkbox" + name="confirm_delete_all_phenotypes" + id="chk-confirm-delete-all-phenotypes" /> + <label class="form-check-label" + for="chk-confirm-delete-all-phenotypes"> + delete all phenotypes?</label> + </div> + + <div class="row"> + <div class="col"> + <input class="btn btn-info" + type="submit" + title="Cancel delete and return to dataset page." + name="action" + value="cancel" /></div> + <div class="col"> + <input class="btn btn-danger" + type="submit" + title="Delete all phenotypes in this dataset." + name="action" + value="delete" /> + </div> + </div> + </form> +</div> +{%endif%} + +{%endblock%} + +{%block javascript%} +<script type="text/javascript"> + $(function() { + var dt = buildDataTable( + "#tbl-delete-phenotypes", + data=[], + columns=[], + userSettings={ + responsive: true, + select: { + style: "os", + info: false + }, + initComplete: function(setting, json) { + var api = this.api(); + api.rows().select(); + api.rows({selected: true}).nodes().each((node, index) => { + setRowChecked(node); + }); + } + }); + + $("#btn-select-all-phenotypes").on("click", function(event) { + dt.selectAll(); + }); + + $("#btn-deselect-all-phenotypes").on("click", function(event) { + dt.deselectAll(); + }); + + $("#btn-delete-phenotypes-selected").on("click", function(event) { + event.preventDefault(); + form = $("#frm-delete-phenotypes-selected"); + form.find(".dynamically-added-element").remove(); + dt.rows({selected: true}).nodes().each(function(node, index) { + var xref_id = $(node) + .find('input[type="checkbox"]:checked') + .val(); + var chk = $('<input type="checkbox">'); + chk.attr("class", "dynamically-added-element"); + chk.attr("value", xref_id); + chk.attr("name", "xref_ids"); + chk.attr("style", "display: none"); + chk.prop("checked", true); + form.append(chk); + }); + form.append( + $('<input type="hidden" name="action" value="delete" />')); + form.submit(); + }) + }); +</script> +{%endblock%} + diff --git a/uploader/templates/phenotypes/job-status.html b/uploader/templates/phenotypes/job-status.html index 257f726..0bbe8e0 100644 --- a/uploader/templates/phenotypes/job-status.html +++ b/uploader/templates/phenotypes/job-status.html @@ -2,7 +2,6 @@ {%from "cli-output.html" import cli_output%} {%from "flash_messages.html" import flash_all_messages%} {%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%} {%block extrameta%} {%if job and job.status not in ("success", "completed:success", "error", "completed:error")%} @@ -14,23 +13,13 @@ {%block pagetitle%}Phenotypes{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="add-phenotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">View Datasets</a> -</li> -{%endblock%} - {%block contents%} {%if job%} -<h4 class="subheading">Progress</h4> +<div class="row"> + <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2> + <h3 class="subheading">upload progress</h3> +</div> <div class="row" style="overflow:scroll;"> <p><strong>Process Status:</strong> {{job.status}}</p> {%if metadata%} @@ -80,7 +69,7 @@ {%endif%} </div> -<h4 class="subheading">Errors</h4> +<h3 class="subheading">upload errors</h3> <div class="row" style="max-height: 20em; overflow: scroll;"> {%if errors | length == 0 %} <p class="text-info"> @@ -149,7 +138,3 @@ </div> {%endif%} {%endblock%} - -{%block sidebarcontents%} -{{display_pheno_dataset_card(species, population, dataset)}} -{%endblock%} diff --git a/uploader/templates/phenotypes/load-phenotypes-success.html b/uploader/templates/phenotypes/load-phenotypes-success.html index 645be16..1fb0e61 100644 --- a/uploader/templates/phenotypes/load-phenotypes-success.html +++ b/uploader/templates/phenotypes/load-phenotypes-success.html @@ -1,26 +1,14 @@ {%extends "phenotypes/base.html"%} {%from "flash_messages.html" import flash_all_messages%} {%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%} {%block title%}Phenotypes{%endblock%} {%block pagetitle%}Phenotypes{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="load-phenotypes-success"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">Add Phenotypes</a> -</li> -{%endblock%} - {%block contents%} +{{super()}} + <div class="row"> <p>You have successfully loaded <!-- maybe indicate the number of phenotypes here? -->your @@ -34,9 +22,5 @@ </div> {%endblock%} -{%block sidebarcontents%} -{{display_pheno_dataset_card(species, population, dataset)}} -{%endblock%} - {%block more_javascript%}{%endblock%} diff --git a/uploader/templates/phenotypes/macro-display-preview-table.html b/uploader/templates/phenotypes/macro-display-preview-table.html index 5a4c422..6dffe9f 100644 --- a/uploader/templates/phenotypes/macro-display-preview-table.html +++ b/uploader/templates/phenotypes/macro-display-preview-table.html @@ -1,19 +1,11 @@ {%macro display_preview_table(tableid, filetype)%} -<div class="card"> - <div class="card-body"> - <h5 class="card-title">{{filetype | title}}: File Preview</h5> - <div class="card-text" style="overflow: scroll;"> - <table id="{{tableid}}" class="table table-condensed table-responsive"> - <thead> - <tr> - </tr> - <tbody> - <tr> - <td class="data-row-template text-info"></td> - </tr> - </tbody> - </table> - </div> - </div> +<div class="table-responsive" + style="max-width:39.2em;border-radius:5px;border: solid 1px;overflow-x: scroll;"> + <h5>{{filetype | title}}: File Preview</h5> + <table id="{{tableid}}" class="table"> + <thead><tr></tr></thead> + + <tbody></tbody> + </table> </div> {%endmacro%} diff --git a/uploader/templates/phenotypes/review-job-data.html b/uploader/templates/phenotypes/review-job-data.html index 859df74..c8355b2 100644 --- a/uploader/templates/phenotypes/review-job-data.html +++ b/uploader/templates/phenotypes/review-job-data.html @@ -104,10 +104,6 @@ {%endif%} {%endblock%} -{%block sidebarcontents%} -{{display_pheno_dataset_card(species, population, dataset)}} -{%endblock%} - {%block javascript%} <script type="text/javascript"> diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-base.html b/uploader/templates/phenotypes/sui-add-phenotypes-base.html deleted file mode 100644 index 1e71267..0000000 --- a/uploader/templates/phenotypes/sui-add-phenotypes-base.html +++ /dev/null @@ -1,155 +0,0 @@ -{%extends "phenotypes/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block contents%} -{{super()}} -{{flash_all_messages()}} - -<div class="row"> - <form id="frm-add-phenotypes" - method="POST" - enctype="multipart/form-data" - action="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id, - use_bundle=use_bundle)}}" - data-resumable-target="{{url_for('files.resumable_upload_post')}}"> - <legend>Add New Phenotypes</legend> - - <div class="form-text help-block"> - {%block frm_add_phenotypes_documentation%}{%endblock%} - <p><strong class="text-warning">This will not update any existing phenotypes!</strong></p> - </div> - - {%block frm_add_phenotypes_elements%}{%endblock%} - - <fieldset id="fldset-publication-info"> - <legend>Publication Information</legend> - <input type="hidden" name="publication-id" id="txt-publication-id" /> - <span class="form-text text-muted"> - Select a publication for your data. <br /> - Can't find a publication you can use? Go ahead and - <a href="{{url_for( - 'publications.create_publication', - return_to='species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">create a new publication</a>.</span> - <table id="tbl-select-publication" class="table compact stripe"> - <thead> - <tr> - <th>#</th> - <th>PubMed ID</th> - <th>Title</th> - <th>Authors</th> - </tr> - </thead> - - <tbody></tbody> - </table> - </fieldset> - - <div class="form-group"> - <input type="submit" - value="upload phenotypes" - class="btn btn-primary" /> - </div> - </form> -</div> - -<div class="row"> - {%block page_documentation%}{%endblock%} -</div> - -{%endblock%} - - - - -{%block javascript%} -<script type="text/javascript"> - $(function() { - var publicationsDataTable = buildDataTable( - "#tbl-select-publication", - [], - [ - {data: "index"}, - { - searchable: true, - data: (pub) => { - if(pub.PubMed_ID) { - return `<a href="https://pubmed.ncbi.nlm.nih.gov/` + - `${pub.PubMed_ID}/" target="_blank" ` + - `title="Link to publication on NCBI.">` + - `${pub.PubMed_ID}</a>`; - } - return ""; - } - }, - { - searchable: true, - data: (pub) => { - var title = "⸻"; - if(pub.Title) { - title = pub.Title - } - return `<a href="/publications/view/${pub.Id}" ` + - `target="_blank" ` + - `title="Link to view publication details">` + - `${title}</a>`; - } - }, - { - searchable: true, - data: (pub) => { - authors = pub.Authors.split(",").map( - (item) => {return item.trim();}); - if(authors.length > 1) { - return authors[0] + ", et. al."; - } - return authors[0]; - } - } - ], - { - serverSide: true, - ajax: { - url: "/publications/list", - dataSrc: "publications" - }, - select: "single", - paging: true, - scrollY: 700, - deferRender: true, - scroller: true, - scrollCollapse: true, - layout: { - topStart: "info", - topEnd: "search" - } - }); - publicationsDataTable.on("select", (event, datatable, type, indexes) => { - indexes.forEach((element, index, thearray) => { - let row = datatable.row(element).node(); - console.debug(datatable.row(element).data()); - $("#frm-add-phenotypes #txt-publication-id").val( - datatable.row(element).data().Id); - }); - }); - publicationsDataTable.on("deselect", (event, datatable, type, indexes) => { - indexes.forEach((element, index, thearray) => { - let row = datatable.row(element).node(); - $("#frm-add-phenotypes #txt-publication-id").val(null); - }); - }); - }); -</script> - -{%block more_javascript%}{%endblock%} -{%endblock%} diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html b/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html deleted file mode 100644 index 6038617..0000000 --- a/uploader/templates/phenotypes/sui-add-phenotypes-raw-files.html +++ /dev/null @@ -1,829 +0,0 @@ -{%extends "phenotypes/sui-add-phenotypes-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-preview-table.html" import display_preview_table%} -{%from "phenotypes/macro-display-resumable-elements.html" import display_resumable_elements%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block frm_add_phenotypes_documentation%} -<p>This page will allow you to upload all the separate files that make up your - phenotypes. Here, you will have to upload each separate file individually. If - you want instead to upload all your files as a single ZIP file, - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id, - use_bundle=true)}}" - title="">click here</a>.</p> -{%endblock%} - -{%block frm_add_phenotypes_elements%} -<fieldset id="fldset-file-metadata"> - <legend>File(s) Metadata</legend> - <div class="form-group"> - <label for="txt-file-separator" class="form-label">File Separator</label> - <div class="input-group"> - <input id="txt-file-separator" - name="file-separator" - type="text" - value="	" - class="form-control" - maxlength="1" /> - <span class="input-group-btn"> - <button id="btn-reset-file-separator" class="btn btn-info">Reset Default</button> - </span> - </div> - <span class="form-text text-muted"> - Provide the character that separates the fields in your file(s). It should - be the same character for all files (if more than one is provided).<br /> - A tab character will be assumed if you leave this field blank. See - <a href="#docs-file-separator" - title="Documentation for file-separator characters"> - documentation for more information</a>. - </span> - </div> - - <div class="form-group"> - <label for="txt-file-comment-character" class="form-label">File Comment-Character</label> - <div class="input-group"> - <input id="txt-file-comment-character" - name="file-comment-character" - type="text" - value="#" - class="form-control" - maxlength="1" /> - <span class="input-group-btn"> - <button id="btn-reset-file-comment-character" class="btn btn-info"> - Reset Default</button> - </span> - </div> - <span class="form-text text-muted"> - This specifies that lines that begin with the character provided will be - considered comment lines and ignored in their entirety. See - <a href="#docs-file-comment-character" - title="Documentation for comment characters"> - documentation for more information</a>. - </span> - </div> - - <div class="form-group"> - <label for="txt-file-na" class="form-label">File "No-Value" Indicators</label> - <div class="input-group"> - <input id="txt-file-na" - name="file-na" - type="text" - value="- NA N/A" - class="form-control" /> - <span class="input-group-btn"> - <button id="btn-reset-file-na" class="btn btn-info">Reset Default</button> - </span> - </div> - <span class="form-text text-muted"> - This specifies strings in your file indicate that there is no value for a - particular cell (a cell is where a column and row intersect). Provide a - space-separated list of strings if you have more than one way of - indicating no values. See - <a href="#docs-file-na" title="Documentation for no-value fields"> - documentation for more information</a>.</span> - </div> -</fieldset> - -<fieldset id="fldset-files"> - <legend>Data File(s)</legend> - - <fieldset id="fldset-descriptions-file"> - <div class="form-group"> - <div class="form-check"> - <input id="chk-phenotype-descriptions-transposed" - name="phenotype-descriptions-transposed" - type="checkbox" - class="form-check-input" - style="border: solid #8EABF0" /> - <label for="chk-phenotype-descriptions-transposed" - class="form-check-label"> - Description file transposed?</label> - </div> - - <div class="non-resumable-elements"> - <label for="finput-phenotype-descriptions" class="form-label"> - Phenotype Descriptions</label> - <input id="finput-phenotype-descriptions" - name="phenotype-descriptions" - class="form-control" - type="file" - data-preview-table="tbl-preview-pheno-desc" - required="required" /> - <span class="form-text text-muted"> - Provide a file that contains only the phenotype descriptions, - <a href="#docs-file-phenotype-description" - title="Documentation of the phenotype data file format."> - the documentation for the expected format of the file</a>.</span> - </div> - {{display_resumable_elements( - "resumable-phenotype-descriptions", - "phenotype descriptions", - '<p>Drag and drop the CSV file that contains the descriptions of your - phenotypes here.</p> - - <p>The CSV file should be a matrix of - <strong>phenotypes × descriptions</strong> i.e. The first column - contains the phenotype names/identifiers whereas the first row is a list - of metadata fields like, "description", "units", etc.</p> - - <p>If the format is transposed (i.e. - <strong>descriptions × phenotypes</strong>) select the checkbox above. - </p> - - <p>Please see the - <a href="#docs-file-phenotype-description" - title="Documentation of the phenotype data file format."> - "Phenotypes Descriptions" documentation</a> section below for more - information on the expected format of the file provided here.</p>')}} - {{display_preview_table( - "tbl-preview-pheno-desc", "phenotype descriptions")}} - </div> - </fieldset> - - - <fieldset id="fldset-data-file"> - <div class="form-group"> - <div class="form-check"> - <input id="chk-phenotype-data-transposed" - name="phenotype-data-transposed" - type="checkbox" - class="form-check-input" - style="border: solid #8EABF0" /> - <label for="chk-phenotype-data-transposed" class="form-check-label"> - Data file transposed?</label> - </div> - - <div class="non-resumable-elements"> - <label for="finput-phenotype-data" class="form-label">Phenotype Data</label> - <input id="finput-phenotype-data" - name="phenotype-data" - class="form-control" - type="file" - data-preview-table="tbl-preview-pheno-data" - required="required" /> - <span class="form-text text-muted"> - Provide a file that contains only the phenotype data. See - <a href="#docs-file-phenotype-data" - title="Documentation of the phenotype data file format."> - the documentation for the expected format of the file</a>.</span> - </div> - - {{display_resumable_elements( - "resumable-phenotype-data", - "phenotype data", - '<p>Drag and drop a CSV file that contains the phenotypes numerical data - here. You can click the "Browse" button (below and to the right) to - select the file from your computer.</p> - - <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>, - i.e. The first column contains the samples identifiers while the first - row is the list of phenotypes identifiers occurring in the phenotypes - descriptions file.</p> - - <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>) - select the checkbox above.</p> - <p>Please see the - <a href="#docs-file-phenotype-data" - title="Documentation of the phenotype data file format."> - "Phenotypes Data" documentation</a> section below for more information - on the expected format for the file provided here.</p>')}} - {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}} - </div> - </fieldset> - - - {%if population.Family in families_with_se_and_n%} - <fieldset id="fldset-se-file"> - <div class="form-group"> - <div class="form-check"> - <input id="chk-phenotype-se-transposed" - name="phenotype-se-transposed" - type="checkbox" - class="form-check-input" - style="border: solid #8EABF0" /> - <label for="chk-phenotype-se-transposed" class="form-check-label"> - Standard-Errors file transposed?</label> - </div> - <div class="group non-resumable-elements"> - <label for="finput-phenotype-se" class="form-label">Phenotype: Standard Errors</label> - <input id="finput-phenotype-se" - name="phenotype-se" - class="form-control" - type="file" - data-preview-table="tbl-preview-pheno-se" - required="required" /> - <span class="form-text text-muted"> - Provide a file that contains only the standard errors for the phenotypes, - computed from the data above.</span> - </div> - - {{display_resumable_elements( - "resumable-phenotype-se", - "standard errors", - '<p>Drag and drop a CSV file that contains the phenotypes standard-errors - data here. You can click the "Browse" button (below and to the right) to - select the file from your computer.</p> - - <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>, - i.e. The first column contains the samples identifiers while the first - row is the list of phenotypes identifiers occurring in the phenotypes - descriptions file.</p> - - <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>) - select the checkbox above.</p> - - <p>Please see the - <a href="#docs-file-phenotype-se" - title="Documentation of the phenotype data file format."> - "Phenotypes Data" documentation</a> section below for more information - on the expected format of the file provided here.</p>')}} - - {{display_preview_table("tbl-preview-pheno-se", "standard errors")}} - </div> - </fieldset> - - - <fieldset id="fldset-n-file"> - <div class="form-group"> - <div class="form-check"> - <input id="chk-phenotype-n-transposed" - name="phenotype-n-transposed" - type="checkbox" - class="form-check-input" - style="border: solid #8EABF0" /> - <label for="chk-phenotype-n-transposed" class="form-check-label"> - Counts file transposed?</label> - </div> - <div class="non-resumable-elements"> - <label for="finput-phenotype-n" class="form-label">Phenotype: Number of Samples/Individuals</label> - <input id="finput-phenotype-n" - name="phenotype-n" - class="form-control" - type="file" - data-preview-table="tbl-preview-pheno-n" - required="required" /> - <span class="form-text text-muted"> - Provide a file that contains only the number of samples/individuals used in - the computation of the standard errors above.</span> - </div> - - {{display_resumable_elements( - "resumable-phenotype-n", - "number of samples/individuals", - '<p>Drag and drop a CSV file that contains the samples\' phenotypes counts - data here. You can click the "Browse" button (below and to the right) to - select the file from your computer.</p> - - <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>, - i.e. The first column contains the samples identifiers while the first - row is the list of phenotypes identifiers occurring in the phenotypes - descriptions file.</p> - - <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>) - select the checkbox above.</p> - - <p>Please see the - <a href="#docs-file-phenotype-se" - title="Documentation of the phenotype data file format."> - "Phenotypes Data" documentation</a> section below for more information - on the expected format of the file provided here.</p>')}} - - {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}} - </div> - </fieldset> -</fieldset> -{%endif%} -{%endblock%} - - -{%block page_documentation%} -<div class="row"> - <h2 class="heading" id="docs-help">Help</h2> - <h3 class="subheading">Common Features</h3> - <p>The following are the common expectations for <strong>ALL</strong> the - files provided in the form above: - <ul> - <li>The file <strong>MUST</strong> be character-separated values (CSV) - text file</li> - <li>The first row in the file <strong>MUST</strong> be a heading row, and - will be composed of the list identifiers for all of - samples/individuals/cases involved in your study.</li> - <li>The first column of data in the file <strong>MUST</strong> be the - identifiers for all of the phenotypes you wish to upload.</li> - </ul> - </p> - - <p>If you do not specify the separator character, then we will assume a - <strong>TAB</strong> character was used as your separator.</p> - - <p>We also assume you might include comments lines in your files. In that - case, if you do not specify what character denotes that a line in your files - is a comment line, we will assume the <strong>#</strong> character.<br /> - A comment <strong>MUST ALWAYS</strong> begin at the start of the line marked - with the comment character specified.</p> - - <h3 class="subheading" id="docs-file-metadata">File Metadata</h3> - <p>We request some details about your files to help us parse and process the - files correctly. The details we collect are:</p> - <dl> - <dt id="docs-file-separator">File separator</dt> - <dd>The files you provide should be character-separated value (CSV) files. - We need to know what character you used to separate the values in your - file. Some common ones are the Tab character, the comma, etc.<br /> - Providing that information makes it possible for the system to parse and - process your files correctly.<br> - <strong>NOTE:</strong> All the files you upload MUST use the same - separator.</dd> - - <dt id="docs-file-comment-character">Comment character</dt> - <dd>We support use of comment lines in your files. We only support one type - of comment style, the <em>line comment</em>.<br /> - This mean the comment begins at the start of the line, and the end of that - line indicates the end of that comment. If you have a really long comment, - then you need to break it across multiple lines, marking each line a - comment line.<br /> - The "comment character" is the character at the start of the line that - indicates that the line is a line comment.</dd> - - <dt id="docs-file-na">No-Value indicator(s)</dt> - <dd>Data in the real world is messy, and in some cases, entirely absent. You - need to indicate, in your files, that a particular field did not have a - value, and once you do that, you then need to let the system know how you - mark such fields. Common ways of indicating "empty values" are, leaving - the field blank, using a character such as '-', or using strings like - "NA", "N/A", "NULL", etc.<br /> - Providing this information will help with parsing and processing such - no-value fields the correct way.</dd> - </dl> - - <h3 class="subheading" id="docs-file-phenotype-description"> - file: Phenotypes Descriptions</h3> - <p>The data in this file is a matrix of <em>phenotypes × metadata-fields</em>. - Please note we use the term "metadata-fields" above loosely, due to lack of - a good word for this.</p> - <p>The file <strong>MUST</strong> have columns in this order: - <dl> - <dt>Phenotype Identifiers</dt> - <dd>These are the names/identifiers for your phenotypes. These - names/identifiers are the same ones you will have in all the other files you are - uploading.</dd> - - <dt>Descriptions</dt> - <dd>Each phenotype will need a description. Good description are necessary - to inform other people of what the data is about. Good description are - hard to construct, so we provide - <a href="https://info.genenetwork.org/faq.php#q-22" - title="How to write phenotype descriptions"> - advice on describing your phenotypes.</a></dd> - - <dt>Units</dt> - <dd>Each phenotype will need units for the measurements taken. If there are - none, then indicate the field is a no-value field.</dd> - </dl></p> - <p>You can add more columns after those three if you want to, but these 3 - <strong>MUST</strong> be present.</p> - <p>The file would, for example, look like the following:</p> - <code>id,description,units,…<br /> - pheno10001|Central nervous system, behavior, cognition; …|mg|…<br /> - pheno10002|Aging, metabolism, central nervous system: …|mg|…<br /> - â‹®<br /></code> - - <p><strong>Note 01</strong>: The first usable row is the heading row.</p> - <p><strong>Note 02: </strong>This example demonstrates a subtle issue that - could make your CSV file invalid — the choice of your field separator - character.<br > - In the example above, we use the pipe character (<code>|</code>) as our - field separator. This is because, if we follow the advice on how to write - good descriptions, then we cannot use the comma as our separator – if - we did, then our CSV file would be invalid because the system would have no - way to tell the difference between the comma as a field separator, and the - comma as a way to separate the "general category and ontology terms".</p> - - <h3 class="subheading">file: Phenotype Data, Standard Errors and/or Sample Counts</h3> - <span id="docs-file-phenotype-data"></span> - <span id="docs-file-phenotype-se"></span> - <span id="docs-file-phenotype-n"></span> - <p>The data is a matrix of <em>samples(or individuals) × phenotypes</em>, e.g.</p> - <code> - # num-cases: 2549 - # num-phenos: 13 - id,pheno10001,pheno10002,pheno10003,pheno10004,53.099998,…<br /> - IND001,61.400002,49,62.5,55.099998,…<br /> - IND002,54.099998,50.099998,53.299999,55.099998,…<br /> - IND003,483,403,501,403,…<br /> - IND004,49.799999,45.5,62.900002,NA,…<br /> - â‹®<br /></code> - - <p>where <code>IND001,IND002,IND003,IND004,…</code> are the - samples/individuals/cases in your study, and - <code>pheno10001,pheno10002,pheno10004,pheno10004,…</code> are the - identifiers for your phenotypes.</p> - <p>The lines beginning with the "<em>#</em>" symbol (i.e. - <code># num-cases: 2549</code> and <code># num-phenos: 13</code> are comment - lines and will be ignored</p> - <p>In this example, the comma (,) is used as the file separator.</p> -</div> - -{%endblock%} - - -{%block more_javascript%} -<script src="{{url_for('base.node_modules', - filename='resumablejs/resumable.js')}}"></script> -<script type="text/javascript" src="/static/js/files.js"></script> - -<script type="text/javascript"> - $("#btn-reset-file-separator").on("click", (event) => { - event.preventDefault(); - $("#txt-file-separator").val("\t"); - $("#txt-file-separator").trigger("change"); - }); - $("#btn-reset-file-comment-character").on("click", (event) => { - event.preventDefault(); - $("#txt-file-comment-character").val("#"); - $("#txt-file-comment-character").trigger("change"); - }); - $("#btn-reset-file-na").on("click", (event) => { - event.preventDefault(); - $("#txt-file-na").val("- NA N/A"); - $("#txt-file-na").trigger("change"); - }); - - var update_preview = (table, filedata, formdata, numrows) => { - table.find("thead tr").remove() - table.find(".data-row").remove(); - var linenum = 0; - var tableheader = table.find("thead"); - var tablebody = table.find("tbody"); - var numheadings = 0; - var navalues = formdata - .na_strings - .split(" ") - .map((v) => {return v.trim();}) - .filter((v) => {return Boolean(v);}); - filedata.forEach((line) => { - if(line.startsWith(formdata.comment_char) || linenum >= numrows) { - return false; - } - var row = $("<tr></tr>"); - line.split(formdata.separator) - .map((field) => { - var value = field.trim(); - if(navalues.includes(value)) { - return "⋘NULâ‹™"; - } - return value; - }) - .filter((field) => { - return (field !== "" && field != undefined && field != null); - }) - .forEach((field) => { - if(linenum == 0) { - numheadings += 1; - var tablefield = $("<th></th>"); - tablefield.text(field); - row.append(tablefield); - } else { - add_class(row, "data-row"); - var tablefield = $("<td></td>"); - tablefield.text(field); - row.append(tablefield); - } - }); - - if(linenum == 0) { - tableheader.append(row); - } else { - tablebody.append(row); - } - linenum += 1; - }); - - if(table.find("tbody tr.data-row").length > 0) { - add_class(table.find(".data-row-template"), "visually-hidden"); - } else { - remove_class(table.find(".data-row-template"), "visually-hidden"); - } - }; - - var makePreviewUpdater = (preview_table) => { - return (data) => { - update_preview( - preview_table, - data, - filesMetadata(), - PREVIEW_ROWS); - }; - }; - - var preview_tables_to_elements_map = { - "#tbl-preview-pheno-desc": "#finput-phenotype-descriptions", - "#tbl-preview-pheno-data": "#finput-phenotype-data", - "#tbl-preview-pheno-se": "#finput-phenotype-se", - "#tbl-preview-pheno-n": "#finput-phenotype-n" - }; - - var filesMetadata = () => { - return { - "separator": $("#txt-file-separator").val(), - "comment_char": $( - "#txt-file-comment-character").val(), - "na_strings": $("#txt-file-na").val() - } - }; - - var PREVIEW_ROWS = 5; - - var handler_update_previews = (event) => { - Object.entries(preview_tables_to_elements_map).forEach((mapentry) => { - var preview_table = $(mapentry[0]); - var file_input = $(mapentry[1]); - if(file_input[0].files.length > 0) { - readFirstNLines( - file_input[0].files[0], - 10, - [makePreviewUpdater(preview_table)]); - } - }); - - if(typeof(resumables) !== "undefined") { - resumables.forEach((resumable) => { - if(resumable.files.length > 0) { - readFirstNLines( - resumable.files[0].file, - 10, - [makePreviewUpdater(resumable.preview_table)]); - } - }); - } - }; - - [ - "#txt-file-separator", - "#txt-file-comment-character", - "#txt-file-na" - ].forEach((elementid) => { - $(elementid).on("change", handler_update_previews); - }); - - [ - "#finput-phenotype-descriptions", - "#finput-phenotype-data", - "#finput-phenotype-se", - "#finput-phenotype-n" - ].forEach((elementid) => { - $(elementid).on("change", (event) => { - readFirstNLines( - event.target.files[0], - 10, - [makePreviewUpdater( - $("#" + event.target.getAttribute("data-preview-table")))]); - }); - }); - - - var resumableDisplayFiles = (display_area, files) => { - files.forEach((file) => { - display_area.find(".file-display").remove(); - var display_element = display_area - .find(".file-display-template") - .clone(); - remove_class(display_element, "visually-hidden"); - remove_class(display_element, "file-display-template"); - add_class(display_element, "file-display"); - display_element.find(".filename").text(file.name - || file.fileName - || file.relativePath - || file.webkitRelativePath); - display_element.find(".filesize").text( - (file.size / (1024*1024)).toFixed(2) + "MB"); - display_element.find(".fileuniqueid").text(file.uniqueIdentifier); - display_element.find(".filemimetype").text(file.file.type); - display_area.append(display_element); - }); - }; - - - var indicateProgress = (resumable, progress_bar) => { - return () => {/*Has no event!*/ - var progress = (resumable.progress() * 100).toFixed(2); - var pbar = progress_bar.find(".progress-bar"); - remove_class(progress_bar, "visually-hidden"); - pbar.css("width", progress+"%"); - pbar.attr("aria-valuenow", progress); - pbar.text("Uploading: " + progress + "%"); - }; - }; - - var retryUpload = (retry_button, cancel_button) => { - retry_button.on("click", (event) => { - resumable.files.forEach((file) => {file.retry();}); - add_class(retry_button, "visually-hidden"); - remove_class(cancel_button, "visually-hidden"); - add_class(browse_button, "visually-hidden"); - }); - }; - - var cancelUpload = (cancel_button, retry_button) => { - cancel_button.on("click", (event) => { - resumable.files.forEach((file) => { - if(file.isUploading()) { - file.abort(); - } - }); - add_class(cancel_button, "visually-hidden"); - remove_class(retry_button, "visually-hidden"); - remove_class(browse_button, "visually-hidden"); - }); - }; - - - var startUpload = (browse_button, retry_button, cancel_button) => { - return (event) => { - remove_class(cancel_button, "visually-hidden"); - add_class(retry_button, "visually-hidden"); - add_class(browse_button, "visually-hidden"); - }; - }; - - var processForm = (form) => { - var formdata = new FormData(form); - uploaded_files.forEach((msg) => { - formdata.delete(msg["file-input-name"]); - formdata.append(msg["file-input-name"], JSON.stringify({ - "uploaded-file": msg["uploaded-file"], - "original-name": msg["original-name"] - })); - }); - formdata.append("resumable-upload", "true"); - formdata.append("publication-id", $("#txt-publication-id").val()); - return formdata; - } - - var uploaded_files = new Set(); - var submitForm = (new_file) => { - uploaded_files.add(new_file); - if(uploaded_files.size === resumables.length) { - var form = $("#frm-add-phenotypes"); - if(form.length !== 1) { - // TODO: Handle error somehow? - alert("Could not find form!!!"); - return false; - } - - $.ajax({ - "url": form.attr("action"), - "type": "POST", - "data": processForm(form[0]), - "processData": false, - "contentType": false, - "success": (data, textstatus, jqxhr) => { - // TODO: Redirect to endpoint that should come as part of the - // success/error message. - console.log("SUCCESS DATA: ", data); - console.log("SUCCESS STATUS: ", textstatus); - console.log("SUCCESS jqXHR: ", jqxhr); - window.location.assign(window.location.origin + data["redirect-to"]); - }, - }); - return false; - } - return false; - }; - - var uploadSuccess = (file_input_name) => { - return (file, message) => { - submitForm({...JSON.parse(message), "file-input-name": file_input_name}); - }; - }; - - - var uploadError = () => { - return (message, file) => { - $("#frm-add-phenotypes input[type=submit]").removeAttr("disabled"); - console.log("THE FILE:", file); - console.log("THE ERROR MESSAGE:", message); - }; - }; - - - - var makeResumableObject = (form_id, file_input_id, resumable_element_id, preview_table_id) => { - var the_form = $("#" + form_id); - var file_input = $("#" + file_input_id); - var submit_button = the_form.find("input[type=submit]"); - if(file_input.length != 1) { - return false; - } - var r = errorHandler( - fileSuccessHandler( - uploadStartHandler( - filesAddedHandler( - markResumableDragAndDropElement( - makeResumableElement( - the_form.attr("data-resumable-target"), - file_input.parent(), - $("#" + resumable_element_id), - submit_button, - ["csv", "tsv", "txt"]), - file_input.parent(), - $("#" + resumable_element_id), - $("#" + resumable_element_id + "-browse-button")), - (files) => { - // TODO: Also trigger preview! - resumableDisplayFiles( - $("#" + resumable_element_id + "-selected-files"), files); - files.forEach((file) => { - readFirstNLines( - file.file, - 10, - [makePreviewUpdater( - $("#" + preview_table_id))]) - }); - }), - startUpload($("#" + resumable_element_id + "-browse-button"), - $("#" + resumable_element_id + "-retry-button"), - $("#" + resumable_element_id + "-cancel-button"))), - uploadSuccess(file_input.attr("name"))), - uploadError()); - - /** Setup progress indicator **/ - progressHandler( - r, - indicateProgress(r, $("#" + resumable_element_id + "-progress-bar"))); - - return r; - }; - - var resumables = [ - ["frm-add-phenotypes", "finput-phenotype-descriptions", "resumable-phenotype-descriptions", "tbl-preview-pheno-desc"], - ["frm-add-phenotypes", "finput-phenotype-data", "resumable-phenotype-data", "tbl-preview-pheno-data"], - ["frm-add-phenotypes", "finput-phenotype-se", "resumable-phenotype-se", "tbl-preview-pheno-se"], - ["frm-add-phenotypes", "finput-phenotype-n", "resumable-phenotype-n", "tbl-preview-pheno-n"], - ].map((row) => { - r = makeResumableObject(row[0], row[1], row[2], row[3]); - r.preview_table = $("#" + row[3]); - return r; - }).filter((val) => { - return Boolean(val); - }); - - $("#frm-add-phenotypes input[type=submit]").on("click", (event) => { - event.preventDefault(); - console.debug(); - if ($("#txt-publication-id").val() == "") { - alert("You MUST provide a publication for the phenotypes."); - return false; - } - // TODO: Check all the relevant files exist - // TODO: Verify that files are not duplicated - var filenames = []; - var nondupfiles = []; - resumables.forEach((r) => { - var fname = r.files[0].file.name; - filenames.push(fname); - if(!nondupfiles.includes(fname)) { - nondupfiles.push(fname); - } - }); - - // Check that all files were provided - if(resumables.length !== filenames.length) { - window.alert("You MUST provide all the files requested."); - event.target.removeAttribute("disabled"); - return false; - } - - // Check that there are no duplicate files - var duplicates = Object.entries(filenames.reduce( - (acc, curr, idx, arr) => { - acc[curr] = (acc[curr] || 0) + 1; - return acc; - }, - {})).filter((entry) => {return entry[1] !== 1;}); - if(duplicates.length > 0) { - var msg = "The file(s):\r\n"; - msg = msg + duplicates.reduce( - (msgstr, afile) => { - return msgstr + " • " + afile[0] + "\r\n"; - }, - ""); - msg = msg + "is(are) duplicated. Please fix and try again."; - window.alert(msg); - event.target.removeAttribute("disabled"); - return false; - } - // TODO: Check all fields - // Start the uploads. - event.target.setAttribute("disabled", "disabled"); - resumables.forEach((r) => {r.upload();}); - }); -</script> -{%endblock%} diff --git a/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html b/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html deleted file mode 100644 index 29a8dea..0000000 --- a/uploader/templates/phenotypes/sui-add-phenotypes-with-rqtl2-bundle.html +++ /dev/null @@ -1,189 +0,0 @@ -{%extends "phenotypes/sui-add-phenotypes-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block frm_add_phenotypes_documentation%} -<p>Select the zip file bundle containing information on the phenotypes you - wish to upload, then click the "Upload Phenotypes" button below to - upload the data.</p> -<p>If you wish to upload the files individually instead, - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}" - title="">click here</a>.</p> -<p>See the <a href="#section-file-formats">File Formats</a> section below - to get an understanding of what is expected of the bundle files you - upload.</p> -{%endblock%} - -{%block frm_add_phenotypes_elements%} -<div class="form-group"> - <label for="finput-phenotypes-bundle" class="form-label"> - Phenotypes Bundle</label> - <input type="file" - id="finput-phenotypes-bundle" - name="phenotypes-bundle" - accept="application/zip, .zip" - required="required" - class="form-control" /> -</div> -{%endblock%} - -{%block page_documentation%} -<div class="row"> - <h2 class="heading" id="section-file-formats">File Formats</h2> - <p>We accept an extended form of the - <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html#format-of-the-data-files" - title="R/qtl2 software input file format documentation"> - input files' format used with the R/qtl2 software</a> as a single ZIP - file</p> - <p>The files that are used for this feature are: - <ul> - <li>the <em>control</em> file</li> - <li><em>pheno</em> file(s)</li> - <li><em>phenocovar</em> file(s)</li> - <li><em>phenose</em> files(s)</li> - </ul> - </p> - <p>Other files within the bundle will be ignored, for this feature.</p> - <p>The following section will detail the expectations for each of the - different file types within the uploaded ZIP file bundle for phenotypes:</p> - - <h3 class="subheading">Control File</h3> - <p>There <strong>MUST be <em>one, and only one</em></strong> file that acts - as the control file. This file can be: - <ul> - <li>a <em>JSON</em> file, or</li> - <li>a <em>YAML</em> file.</li> - </ul> - </p> - - <p>The control file is useful for defining things about the bundle such as:</p> - <ul> - <li>The field separator value (default: <code>sep: ','</code>). There can - only ever be one field separator and it <strong>MUST</strong> be the same - one for <strong>ALL</strong> files in the bundle.</li> - <li>The comment character (default: <code>comment.char: '#'</code>). Any - line that starts with this character will be considered a comment line and - be ignored in its entirety.</li> - <li>Code for missing values (default: <code>na.strings: 'NA'</code>). You - can specify more than one code to indicate missing values, e.g. - <code>{…, "na.strings": ["NA", "N/A", "-"], …}</code></li> - </ul> - - <h3 class="subheading"><em>pheno</em> File(s)</h3> - <p>These files are the main data files. You must have at least one of these - files in your bundle for it to be valid for this step.</p> - <p>The data is a matrix of <em>individuals × phenotypes</em> by default, as - below:<br /> - <code> - id,10001,10002,10003,10004,…<br /> - BXD1,61.400002,54.099998,483,49.799999,…<br /> - BXD2,49,50.099998,403,45.5,…<br /> - BXD5,62.5,53.299999,501,62.900002,…<br /> - BXD6,53.099998,55.099998,403,NA,…<br /> - â‹®<br /></code> - </p> - <p>If the <code>pheno_transposed</code> value is set to <code>True</code>, - then the data will be a <em>phenotypes × individuals</em> matrix as in the - example below:<br /> - <code> - id,BXD1,BXD2,BXD5,BXD6,…<br /> - 10001,61.400002,49,62.5,53.099998,…<br /> - 10002,54.099998,50.099998,53.299999,55.099998,…<br /> - 10003,483,403,501,403,…<br /> - 10004,49.799999,45.5,62.900002,NA,…<br /> - â‹® - </code> - </p> - - - <h3 class="subheading"><em>phenocovar</em> File(s)</h3> - <p>At least one phenotypes metadata file with the metadata values such as - descriptions, PubMed Identifier, publication titles (if present), etc.</p> - <p>The data in this/these file(s) is a matrix of - <em>phenotypes × phenotypes-covariates</em>. The first column is always the - phenotype names/identifiers — same as in the R/qtl2 format.</p> - <p><em>phenocovar</em> files <strong>should never be transposed</strong>!</p> - <p>This file <strong>MUST</strong> be present in the bundle, and have data for - the bundle to be considered valid by our system for this step.<br /> - In addition to that, the following are the fields that <strong>must be - present</strong>, and - have values, in the file before the file is considered valid: - <ul> - <li><em>description</em>: A description for each phenotype. Useful - for users to know what the phenotype is about.</li> - <li><em>units</em>: The units of measurement for the phenotype, - e.g. milligrams for brain weight, centimetres/millimetres for - tail-length, etc.</li> - </ul></p> - - <p>The following <em>optional</em> fields can also be provided: - <ul> - <li><em>pubmedid</em>: A PubMed Identifier for the publication where - the phenotype is published. If this field is not provided, the system will - assume your phenotype is not published.</li> - </ul> - </p> - <p>These files will be marked up in the control file with the - <code>phenocovar</code> key, as in the examples below: - <ol> - <li>JSON: single file<br /> - <code>{<br /> - â‹®,<br /> - "phenocovar": "your_covariates_file.csv",<br /> - â‹®<br /> - } - </code> - </li> - <li>JSON: multiple files<br /> - <code>{<br /> - â‹®,<br /> - "phenocovar": [<br /> - "covariates_file_01.csv",<br /> - "covariates_file_01.csv",<br /> - â‹®<br /> - ],<br /> - â‹®<br /> - } - </code> - </li> - <li>YAML: single file or<br /> - <code> - â‹®<br /> - phenocovar: your_covariates_file.csv<br /> - â‹® - </code> - </li> - <li>YAML: multiple files<br /> - <code> - â‹®<br /> - phenocovar:<br /> - - covariates_file_01.csv<br /> - - covariates_file_02.csv<br /> - - covariates_file_03.csv<br /> - …<br /> - â‹® - </code> - </li> - </ol> - </p> - - <h3 class="subheading"><em>phenose</em> and <em>phenonum</em> File(s)</h3> - <p>These are extensions to the R/qtl2 standard, i.e. these types ofs file are - not supported by the original R/qtl2 file format</p> - <p>We use these files to upload the standard errors (<em>phenose</em>) when - the data file (<em>pheno</em>) is average data. In that case, the - <em>phenonum</em> file(s) contains the number of individuals that were - involved when computing the averages.</p> - <p>Both types of files are matrices of <em>individuals × phenotypes</em> by - default. Like the related <em>pheno</em> files, if - <code>pheno_transposed: True</code>, then the file will be a matrix of - <em>phenotypes × individuals</em>.</p> -</div> -{%endblock%} diff --git a/uploader/templates/phenotypes/sui-base.html b/uploader/templates/phenotypes/sui-base.html deleted file mode 100644 index d7d980f..0000000 --- a/uploader/templates/phenotypes/sui-base.html +++ /dev/null @@ -1,25 +0,0 @@ -{%extends "populations/sui-base.html"%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_sui_pheno_dataset_card%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('species.populations.phenotypes.view_dataset', - species_id=species['SpeciesId'], - population_id=population['Id'], - dataset_id=dataset['Id'])}}"> - {{dataset["Name"]}} - </a> -</li> -{%endblock%} - -{%block contents%} -<div class="row"> - <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2> -</div> -{%endblock%} - - -{%block sidebarcontents%} -{{display_sui_pheno_dataset_card(species, population, dataset)}} -{%endblock%} diff --git a/uploader/templates/phenotypes/sui-job-status.html b/uploader/templates/phenotypes/sui-job-status.html deleted file mode 100644 index bca87d5..0000000 --- a/uploader/templates/phenotypes/sui-job-status.html +++ /dev/null @@ -1,140 +0,0 @@ -{%extends "phenotypes/sui-base.html"%} -{%from "cli-output.html" import cli_output%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} - -{%block extrameta%} -{%if job and job.status not in ("success", "completed:success", "error", "completed:error")%} -<meta http-equiv="refresh" content="5" /> -{%endif%} -{%endblock%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block contents%} - -{%if job%} -<div class="row"> - <h2 class="heading">{{dataset.FullName}} ({{dataset.Name}})</h2> - <h3 class="subheading">upload progress</h3> -</div> -<div class="row" style="overflow:scroll;"> - <p><strong>Process Status:</strong> {{job.status}}</p> - {%if metadata%} - <table class="table table-responsive"> - <thead> - <tr> - <th>File</th> - <th>Status</th> - <th>Lines Processed</th> - <th>Total Errors</th> - </tr> - </thead> - - <tbody> - {%for file,meta in metadata.items()%} - <tr> - <td>{{file}}</td> - <td>{{meta.status}}</td> - <td>{{meta.linecount}}</td> - <td>{{meta["total-errors"]}}</td> - </tr> - {%endfor%} - </tbody> - </table> - {%endif%} -</div> - -<div class="row"> - {%if job.status in ("completed:success", "success")%} - <p> - {%if errors | length == 0%} - <a href="{{url_for('species.populations.phenotypes.review_job_data', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id, - job_id=job_id)}}" - class="btn btn-primary" - title="Continue to process data">Continue</a> - {%else%} - <span class="text-muted" - disabled="disabled" - style="border: solid 2px;border-radius: 5px;padding: 0.3em;"> - Cannot continue due to errors. Please fix the errors first. - </span> - {%endif%} - </p> - {%endif%} -</div> - -<h3 class="subheading">upload errors</h3> -<div class="row" style="max-height: 20em; overflow: scroll;"> - {%if errors | length == 0 %} - <p class="text-info"> - <span class="glyphicon glyphicon-info-sign"></span> - No errors found so far - </p> - {%else%} - <table class="table table-responsive"> - <thead style="position: sticky; top: 0; background: white;"> - <tr> - <th>File</th> - <th>Row</th> - <th>Column</th> - <th>Value</th> - <th>Message</th> - </tr> - </thead> - - <tbody style="font-size: 0.9em;"> - {%for error in errors%} - <tr> - <td>{{error.filename}}</td> - <td>{{error.rowtitle}}</td> - <td>{{error.coltitle}}</td> - <td>{%if error.cellvalue is not none and error.cellvalue | length > 25%} - {{error.cellvalue[0:24]}}… - {%else%} - {{error.cellvalue}} - {%endif%} - </td> - <td> - {%if error.message | length > 250 %} - {{error.message[0:249]}}… - {%else%} - {{error.message}} - {%endif%} - </td> - </tr> - {%endfor%} - </tbody> - </table> - {%endif%} -</div> - -<div class="row"> - {{cli_output(job, "stdout")}} -</div> - -<div class="row"> - {{cli_output(job, "stderr")}} -</div> - -{%else%} -<div class="row"> - <h3 class="text-danger">No Such Job</h3> - <p>Could not find a job with the ID: {{job_id}}</p> - <p> - Please go back to - <a href="{{url_for('species.populations.phenotypes.view_dataset', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}" - title="'{{dataset.Name}}' dataset page"> - the '{{dataset.Name}}' dataset page</a> - to upload new phenotypes or edit existing ones.</p> -</div> -{%endif%} -{%endblock%} diff --git a/uploader/templates/phenotypes/sui-load-phenotypes-success.html b/uploader/templates/phenotypes/sui-load-phenotypes-success.html deleted file mode 100644 index dff0682..0000000 --- a/uploader/templates/phenotypes/sui-load-phenotypes-success.html +++ /dev/null @@ -1,26 +0,0 @@ -{%extends "phenotypes/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block contents%} -{{super()}} - -<div class="row"> - <p>You have successfully loaded - <!-- maybe indicate the number of phenotypes here? -->your - new phenotypes into the database.</p> - <!-- TODO: Maybe notify user that they have sole access. --> - <!-- TODO: Maybe provide a link to go to GeneNetwork to view the data. --> - <p>View your data - <a href="{{search_page_uri}}" - target="_blank">on GeneNetwork2</a>. - You might need to login to GeneNetwork2 to view specific traits.</p> -</div> -{%endblock%} - - -{%block more_javascript%}{%endblock%} diff --git a/uploader/templates/phenotypes/sui-review-job-data.html b/uploader/templates/phenotypes/sui-review-job-data.html deleted file mode 100644 index ea4183d..0000000 --- a/uploader/templates/phenotypes/sui-review-job-data.html +++ /dev/null @@ -1,121 +0,0 @@ -{%extends "phenotypes/sui-base.html"%} -{%from "cli-output.html" import cli_output%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} -{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%} - -{%block extrameta%} -{%if not job%} -<meta http-equiv="refresh" - content="20; url={{url_for('species.populations.phenotypes.view_dataset', species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}" /> -{%endif%} -{%endblock%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block lvl4_breadcrumbs%} -<li {%if activelink=="add-phenotypes"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">View Datasets</a> -</li> -{%endblock%} - -{%block contents%} - -{%if job%} -<div class="row"> - <h3 class="heading">Data Review</h3> - <p class="text-info"><strong> - The data has <em>NOT</em> been added/saved yet. Review the details below - and click "Continue" to save the data.</strong></p> - <p>The “<strong>{{dataset.FullName}}</strong>” dataset from the - “<strong>{{population.FullName}}</strong>” population of the - species “<strong>{{species.SpeciesName}} ({{species.FullName}})</strong>” - will be updated as follows:</p> - - <ul> - {%if publication%} - <li>All {{summary.get("pheno", {}).get("total-data-rows", "0")}} phenotypes - are linked to the following publication: - <ul> - <li><strong>Publication Title:</strong> - {{publication.Title or "—"}}</li> - <li><strong>Author(s):</strong> - {{publication.Authors or "—"}}</li> - </ul> - </li> - {%endif%} - {%for ftype in ("phenocovar", "pheno", "phenose", "phenonum")%} - {%if summary.get(ftype, False)%} - <li>A total of {{summary[ftype]["number-of-files"]}} files will be processed - adding {%if ftype == "phenocovar"%}(possibly){%endif%} - {{summary[ftype]["total-data-rows"]}} new - {%if ftype == "phenocovar"%} - phenotypes - {%else%} - {{summary[ftype]["description"]}} rows - {%endif%} - to the database. - </li> - {%endif%} - {%endfor%} - </ul> - - <form id="frm-review-phenotype-data" - method="POST" - action="{{url_for('species.populations.phenotypes.load_data_to_database', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}"> - <input type="hidden" name="data-qc-job-id" value="{{job.jobid}}" /> - <input type="submit" - value="continue" - class="btn btn-primary" /> - </form> -</div> -{%else%} -<div class="row"> - <h4 class="subheading">Invalid Job</h3> - <p class="text-danger"> - Could not find a job with the ID: <strong>{{job_id}}.</p> - <p>You will be redirected in - <span id="countdown-element" class="text-info">20</span> second(s)</p> - <p class="text-muted"> - <small> - If you are not redirected, please - <a href="{{url_for( - 'species.populations.phenotypes.view_dataset', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">click here</a> to continue - </small> - </p> -</div> -{%endif%} -{%endblock%} - - -{%block javascript%} -<script type="text/javascript"> - $(document).ready(function() { - var countdown = 20; - var countdown_element = $("#countdown-element"); - if(countdown_element.length === 1) { - intv = window.setInterval(function() { - countdown = countdown - 1; - countdown_element.html(countdown); - }, 1000); - } - }); -</script> -{%endblock%} diff --git a/uploader/templates/phenotypes/sui-view-dataset.html b/uploader/templates/phenotypes/sui-view-dataset.html deleted file mode 100644 index f858c4e..0000000 --- a/uploader/templates/phenotypes/sui-view-dataset.html +++ /dev/null @@ -1,207 +0,0 @@ -{%extends "phenotypes/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-table-pagination.html" import table_pagination%} - -{%block title%}Phenotypes{%endblock%} - -{%block pagetitle%}Phenotypes{%endblock%} - -{%block lvl4_breadcrumbs%} -<li {%if activelink=="view-dataset"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.view_dataset', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}">View</a> -</li> -{%endblock%} - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <p>The basic dataset details are:</p> - - <table class="table"> - <thead> - <tr> - <th>Name</th> - <th>Full Name</th> - <th>Short Name</th> - </tr> - </thead> - - <tbody> - <tr> - <td>{{dataset.Name}}</td> - <td>{{dataset.FullName}}</td> - <td>{{dataset.ShortName}}</td> - </tr> - </tbody> - </table> -</div> - -<div class="row"> - <div class="col"> - <a href="{{url_for('species.populations.phenotypes.add_phenotypes', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id)}}" - title="Add a bunch of phenotypes" - class="btn btn-primary">Add phenotypes</a> - </div> - - <div class="col"> - <form id="frm-recompute-phenotype-means" - method="POST" - action="{{url_for( - 'species.populations.phenotypes.recompute_means', - species_id=species['SpeciesId'], - population_id=population['Id'], - dataset_id=dataset['Id'])}}" - class="d-flex flex-row align-items-center flex-wrap" - style="display: inline;"> - <input type="submit" - title="Compute/Recompute the means for all phenotypes." - class="btn btn-info" - value="(rec/c)ompute means" - id="submit-frm-recompute-phenotype-means" /> - </form> - </div> - - <div class="col"> - <form id="frm-run-qtlreaper" - method="POST" - action="{{url_for( - 'species.populations.phenotypes.rerun_qtlreaper', - species_id=species['SpeciesId'], - population_id=population['Id'], - dataset_id=dataset['Id'])}}" - class="d-flex flex-row align-items-center flex-wrap" - style="display: inline;"> - <input type="submit" - title="Run/Rerun QTLReaper." - class="btn btn-info" - value="(re)run QTLReaper" - id="submit-frm-rerun-qtlreaper" /> - </form> - </div> -</div> - -<div class="row"> - <h2>Phenotype Data</h2> - - <p>Click on any of the phenotypes in the table below to view and edit that - phenotype's data.</p> - <p>Use the search to filter through all the phenotypes and find specific - phenotypes of interest.</p> -</div> - - -<div class="row"> - - <table id="tbl-phenotypes-list" class="table compact stripe cell-border"> - <thead> - <tr> - <th></th> - <th>Index</th> - <th>Record</th> - <th>Description</th> - </tr> - </thead> - - <tbody></tbody> - </table> -</div> -{%endblock%} - - -{%block javascript%} -<script type="text/javascript" src="/static/js/urls.js"></script> - -<script type="text/javascript"> - $(function() { - var species_id = {{species.SpeciesId}}; - var population_id = {{population.Id}}; - var dataset_id = {{dataset.Id}}; - var dataset_name = "{{dataset.Name}}"; - var data = {{phenotypes | tojson}}; - - var dtPhenotypesList = buildDataTable( - "#tbl-phenotypes-list", - data, - [ - { - data: function(pheno) { - return `<input type="checkbox" name="selected-phenotypes" ` - + `id="chk-selected-phenotypes-${pheno.InbredSetCode}_${pheno.xref_id}" ` - + `value="${pheno.InbredSetCode}_${pheno.xref_id}" ` - + `class="chk-row-select" />` - } - }, - {data: "sequence_number"}, - { - data: function(pheno, type, set, meta) { - var spcs_id = {{species.SpeciesId}}; - var pop_id = {{population.Id}}; - var dtst_id = {{dataset.Id}}; - var url = buildURLFromCurrentURL( - (`/species/${spcs_id}` + - `/populations/${pop_id}` + - `/phenotypes/datasets/${dtst_id}` + - `/phenotype/${pheno.xref_id}`)); - return `<a href="${url.toString()}" target="_blank">` + - `${pheno.InbredSetCode}_${pheno.xref_id}` + - `</a>`; - } - }, - { - data: function(pheno) { - return (pheno.Post_publication_description || - pheno.Original_description || - pheno.Pre_publication_description); - } - } - ], - { - select: "multi+shift", - layout: { - top1Start: { - pageLength: { - text: "Show _MENU_ of _TOTAL_" - } - }, - topStart: "info", - top1End: null - }, - rowId: function(pheno) { - return `${pheno.InbredSetCode}_${pheno.xref_id}`; - } - }); - - - $("#submit-frm-rerun-qtlreaper").on( - "click", - function(event) { - // (Re)run the QTLReaper script for selected phenotypes. - event.preventDefault(); - var form = $("#frm-run-qtlreaper"); - form.find(".dynamically-added-element").remove(); - dtPhenotypesList.rows({selected: true}).nodes().each((node, index) => { - _cloned = $(node).find(".chk-row-select").clone(); - _cloned.removeAttr("id"); - _cloned.removeAttr("class"); - _cloned.attr("style", "display: none;"); - _cloned.attr("data-type", "dynamically-added-element"); - _cloned.attr("class", "dynamically-added-element checkbox"); - _cloned.prop("checked", true); - form.append(_cloned); - }); - form.submit(); - }); - }); -</script> -{%endblock%} diff --git a/uploader/templates/phenotypes/view-dataset.html b/uploader/templates/phenotypes/view-dataset.html index c634a48..3bb2586 100644 --- a/uploader/templates/phenotypes/view-dataset.html +++ b/uploader/templates/phenotypes/view-dataset.html @@ -1,7 +1,6 @@ {%extends "phenotypes/base.html"%} {%from "flash_messages.html" import flash_all_messages%} {%from "macro-table-pagination.html" import table_pagination%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block title%}Phenotypes{%endblock%} @@ -24,25 +23,12 @@ {{flash_all_messages()}} <div class="row"> - <p>The basic dataset details are:</p> - - <table class="table"> - <thead> - <tr> - <th>Name</th> - <th>Full Name</th> - <th>Short Name</th> - </tr> - </thead> + <h2>Phenotype Data</h2> - <tbody> - <tr> - <td>{{dataset.Name}}</td> - <td>{{dataset.FullName}}</td> - <td>{{dataset.ShortName}}</td> - </tr> - </tbody> - </table> + <p>Click on any of the phenotypes in the table below to view and edit that + phenotype's data.</p> + <p>Use the search to filter through all the phenotypes and find specific + phenotypes of interest.</p> </div> <div class="row"> @@ -68,7 +54,7 @@ <input type="submit" title="Compute/Recompute the means for all phenotypes." class="btn btn-info" - value="(rec/c)ompute means" + value="compute means" id="submit-frm-recompute-phenotype-means" /> </form> </div> @@ -86,24 +72,29 @@ <input type="submit" title="Run/Rerun QTLReaper." class="btn btn-info" - value="(re)run QTLReaper" + value="run QTLReaper" id="submit-frm-rerun-qtlreaper" /> </form> </div> -</div> - -<div class="row"> - <h2>Phenotype Data</h2> - <p>Click on any of the phenotypes in the table below to view and edit that - phenotype's data.</p> - <p>Use the search to filter through all the phenotypes and find specific - phenotypes of interest.</p> + <div class="col"> + <form id="frm-delete-phenotypes" + method="POST" + action="{{url_for( + 'species.populations.phenotypes.delete_phenotypes', + species_id=species['SpeciesId'], + population_id=population['Id'], + dataset_id=dataset['Id'])}}"> + <input type="submit" + class="btn btn-danger" + id="btn-delete-phenotypes" + title="Delete phenotypes from this dataset. If no phenotypes are selected in the table, this will delete ALL the phenotypes." + value="delete phenotypes" /> + </form> + </div> </div> - -<div class="row"> - +<div class="row" style="margin-top: 0.5em;"> <table id="tbl-phenotypes-list" class="table compact stripe cell-border"> <thead> <tr> @@ -119,12 +110,10 @@ </div> {%endblock%} -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} - {%block javascript%} +<script type="text/javascript" src="/static/js/urls.js"></script> + <script type="text/javascript"> $(function() { var species_id = {{species.SpeciesId}}; @@ -151,11 +140,12 @@ var spcs_id = {{species.SpeciesId}}; var pop_id = {{population.Id}}; var dtst_id = {{dataset.Id}}; - return `<a href="/species/${spcs_id}` + + var url = buildURLFromCurrentURL( + (`/species/${spcs_id}` + `/populations/${pop_id}` + `/phenotypes/datasets/${dtst_id}` + - `/phenotype/${pheno.xref_id}` + - `" target="_blank">` + + `/phenotype/${pheno.xref_id}`)); + return `<a href="${url.toString()}" target="_blank">` + `${pheno.InbredSetCode}_${pheno.xref_id}` + `</a>`; } @@ -204,6 +194,33 @@ }); form.submit(); }); + + $("#btn-delete-phenotypes").on( + "click", + function(event) { + // Collect selected phenotypes for deletion, if any. + event.preventDefault(); + form = $("#frm-delete-phenotypes"); + form.find(".dynamically-added-element").remove(); + $("#tbl-phenotypes-list") + .DataTable() + .rows({selected: true}). + nodes().each(function(node, index) { + var parts = $(node) + .find(".chk-row-select") + .val() + .split("_"); + var xref_id = parts[parts.length - 1].trim(); + var chk = $('<input type="checkbox">'); + chk.attr("class", "dynamically-added-element"); + chk.attr("value", xref_id); + chk.attr("name", "xref_ids"); + chk.attr("style", "display: none"); + chk.prop("checked", true); + form.append(chk); + }); + form.submit(); + }); }); </script> {%endblock%} diff --git a/uploader/templates/phenotypes/view-phenotype.html b/uploader/templates/phenotypes/view-phenotype.html index 75e3c1e..a69b024 100644 --- a/uploader/templates/phenotypes/view-phenotype.html +++ b/uploader/templates/phenotypes/view-phenotype.html @@ -1,25 +1,10 @@ {%extends "phenotypes/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block title%}Phenotypes{%endblock%} {%block pagetitle%}Phenotypes{%endblock%} -{%block lvl4_breadcrumbs%} -<li {%if activelink=="view-phenotype"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.phenotypes.view_phenotype', - species_id=species.SpeciesId, - population_id=population.Id, - dataset_id=dataset.Id, - xref_id=xref_id)}}">View Phenotype</a> -</li> -{%endblock%} - {%block contents%} {{flash_all_messages()}} @@ -153,7 +138,3 @@ or "group:resource:delete-resource" in privileges%} </div> {%endblock%} - -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/platforms/base.html b/uploader/templates/platforms/base.html index dac965f..d4f3686 100644 --- a/uploader/templates/platforms/base.html +++ b/uploader/templates/platforms/base.html @@ -1,13 +1,17 @@ {%extends "species/base.html"%} +{%from "species/macro-display-species-card.html" import display_sui_species_card%} -{%block lvl3_breadcrumbs%} -<li {%if activelink=="platforms"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.platforms.index')}}"> - Sequencing Platforms</a> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('species.platforms.list_platforms', + species_id=species['SpeciesId'])}}"> + Platforms + </a> </li> -{%block lvl4_breadcrumbs%}{%endblock%} +{%endblock%} + + +{%block sidebarcontents%} +{{display_sui_species_card(species)}} {%endblock%} diff --git a/uploader/templates/platforms/create-platform.html b/uploader/templates/platforms/create-platform.html index 0866d5e..3a62472 100644 --- a/uploader/templates/platforms/create-platform.html +++ b/uploader/templates/platforms/create-platform.html @@ -1,19 +1,15 @@ {%extends "platforms/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%from "species/macro-display-species-card.html" import display_species_card%} {%block title%}Platforms — Create Platforms{%endblock%} -{%block pagetitle%}Platforms — Create Platforms{%endblock%} - -{%block lvl3_breadcrumbs%} -<li {%if activelink=="create-platform"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.platforms.create_platform', - species_id=species.SpeciesId)}}">create platform</a> + species_id=species['SpeciesId'])}}"> + Create + </a> </li> {%endblock%} @@ -118,7 +114,3 @@ </form> </div> {%endblock%} - -{%block sidebarcontents%} -{{display_species_card(species)}} -{%endblock%} diff --git a/uploader/templates/platforms/list-platforms.html b/uploader/templates/platforms/list-platforms.html index a6bcfdc..db14745 100644 --- a/uploader/templates/platforms/list-platforms.html +++ b/uploader/templates/platforms/list-platforms.html @@ -1,6 +1,5 @@ {%extends "platforms/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%from "species/macro-display-species-card.html" import display_species_card%} {%block title%}Platforms — List Platforms{%endblock%} @@ -87,7 +86,3 @@ {%endif%} </div> {%endblock%} - -{%block sidebarcontents%} -{{display_species_card(species)}} -{%endblock%} diff --git a/uploader/templates/platforms/sui-base.html b/uploader/templates/platforms/sui-base.html deleted file mode 100644 index 136b956..0000000 --- a/uploader/templates/platforms/sui-base.html +++ /dev/null @@ -1,17 +0,0 @@ -{%extends "species/sui-base.html"%} -{%from "species/macro-display-species-card.html" import display_sui_species_card%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('species.platforms.list_platforms', - species_id=species['SpeciesId'])}}"> - Platforms - </a> -</li> -{%endblock%} - - -{%block sidebarcontents%} -{{display_sui_species_card(species)}} -{%endblock%} diff --git a/uploader/templates/platforms/sui-create-platform.html b/uploader/templates/platforms/sui-create-platform.html deleted file mode 100644 index 25977a7..0000000 --- a/uploader/templates/platforms/sui-create-platform.html +++ /dev/null @@ -1,116 +0,0 @@ -{%extends "platforms/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Platforms — Create Platforms{%endblock%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('species.platforms.create_platform', - species_id=species['SpeciesId'])}}"> - Create - </a> -</li> -{%endblock%} - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <h2>Create New Platform</h2> - - <p>You can create a new genetic sequencing platform below.</p> -</div> - -<div class="row"> - <form id="frm-create-platform" - method="POST" - action="{{url_for('species.platforms.create_platform', - species_id=species.SpeciesId)}}"> - - <div class="form-group"> - <label for="txt-geo-platform" class="form-label">GEO Platform</label> - <input type="text" - id="txt-geo-platform" - name="geo-platform" - required="required" - class="form-control" /> - <small class="form-text text-muted"> - <p>This is the platform's - <a href="https://www.ncbi.nlm.nih.gov/geo/browse/?view=platforms&tax={{species.TaxonomyId}}" - title="Platforms for '{{species.FullName}}' on NCBI"> - accession value on NCBI</a>. If you do not know the value, click the - link and search on NCBI for species '{{species.FullName}}'.</p></small> - </div> - - <div class="form-group"> - <label for="txt-platform-name" class="form-label">Platform Name</label> - <input type="text" - id="txt-platform-name" - name="platform-name" - required="required" - class="form-control" /> - <small class="form-text text-muted"> - <p>This is name of the genetic sequencing platform.</p></small> - </div> - - <div class="form-group"> - <label for="txt-platform-shortname" class="form-label"> - Platform Short Name</label> - <input type="text" - id="txt-platform-shortname" - name="platform-shortname" - required="required" - class="form-control" /> - <small class="form-text text-muted"> - <p>Use the following conventions for this field: - <ol> - <li>Start with a 4-letter vendor code, e.g. "Affy" for "Affymetrix", "Illu" for "Illumina", etc.</li> - <li>Append an underscore to the 4-letter vendor code</li> - <li>Use the name of the array given by the vendor, e.g. U74AV2, MOE430A, etc.</li> - </ol> - </p> - </small> - </div> - - <div class="form-group"> - <label for="txt-platform-title" class="form-label">Platform Title</label> - <input type="text" - id="txt-platform-title" - name="platform-title" - required="required" - class="form-control" /> - <small class="form-text text-muted"> - <p>The full platform title. Sometimes, this is the same as the Platform - Name above.</p></small> - </div> - - <div class="form-group"> - <label for="txt-go-tree-value" class="form-label">GO Tree Value</label> - <input type="text" - id="txt-go-tree-value" - name="go-tree-value" - class="form-control" /> - <small class="form-text text-muted"> - <p>This is a Chip identification value useful for analysis with the - <strong> - <a href="https://www.geneweaver.org/" - title="Go to the GeneWeaver site." - target="_blank">GeneWeaver</a></strong> - and - <strong> - <a href="https://www.webgestalt.org/" - title="Go to the WEB-based GEne SeT AnaLysis Toolkit site." - target="_blank">WebGestalt</a></strong> - tools.<br /> - This can be left blank for custom platforms.</p></small> - </div> - - <div class="form-group"> - <input type="submit" - value="create new platform" - class="btn btn-primary" /> - </div> - </form> -</div> -{%endblock%} diff --git a/uploader/templates/platforms/sui-list-platforms.html b/uploader/templates/platforms/sui-list-platforms.html deleted file mode 100644 index f4c17e8..0000000 --- a/uploader/templates/platforms/sui-list-platforms.html +++ /dev/null @@ -1,88 +0,0 @@ -{%extends "platforms/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Platforms — List Platforms{%endblock%} - -{%block pagetitle%}Platforms — List Platforms{%endblock%} - - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <p>View the list of the genetic sequencing platforms that are currently - supported by GeneNetwork.</p> - <p>If you cannot find the platform you wish to use, you can add it by clicking - the "New Platform" button below.</p> - <p><a href="{{url_for('species.platforms.create_platform', - species_id=species.SpeciesId)}}" - title="Create a new genetic sequencing platform for species {{species.FullName}}" - class="btn btn-primary">Create Platform</a></p> -</div> - -<div class="row"> - <h2>Supported Platforms</h2> - {%if platforms is defined and platforms | length > 0%} - <p>There are {{total_platforms}} platforms supported by GeneNetwork</p> - - <div class="row"> - <div class="col-md-2" style="text-align: start;"> - {%if start_from > 0%} - <a href="{{url_for('species.platforms.list_platforms', - species_id=species.SpeciesId, - start_from=start_from-count, - count=count)}}"> - <span class="glyphicon glyphicon-backward"></span> - Previous - </a> - {%endif%} - </div> - <div class="col-md-8" style="text-align: center;"> - Displaying platforms {{start_from+1}} to {{start_from+count if start_from+count < total_platforms else total_platforms}} of - {{total_platforms}} - </div> - <div class="col-md-2" style="text-align: end;"> - {%if start_from + count < total_platforms%} - <a href="{{url_for('species.platforms.list_platforms', - species_id=species.SpeciesId, - start_from=start_from+count, - count=count)}}"> - Next - <span class="glyphicon glyphicon-forward"></span> - </a> - {%endif%} - </div> - </div> - - <table class="table"> - <thead> - <tr> - <th></th> - <th>Platform Name</th> - <th><a href="https://www.ncbi.nlm.nih.gov/geo/browse/?view=platforms&tax={{species.TaxonomyId}}" - title="Gene Expression Omnibus: Platforms section" - target="_blank">GEO Platform</a></th> - <th>Title</th> - </tr> - </thead> - - <tbody> - {%for platform in platforms%} - <tr> - <td>{{platform.sequence_number}}</td> - <td>{{platform.GeneChipName}}</td> - <td><a href="https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc={{platform.GeoPlatform}}" - title="View platform on the Gene Expression Omnibus" - target="_blank">{{platform.GeoPlatform}}</a></td> - <td>{{platform.Title}}</td> - </tr> - {%endfor%} - </tbody> - </table> - {%else%} - <p class="text-warning"> - <span class="glyphicon glyphicon-exclamation-sign"></span> - There are no platforms supported at this time!</p> - {%endif%} -</div> -{%endblock%} diff --git a/uploader/templates/populations/base.html b/uploader/templates/populations/base.html index 9db8083..24cacc2 100644 --- a/uploader/templates/populations/base.html +++ b/uploader/templates/populations/base.html @@ -1,18 +1,20 @@ {%extends "species/base.html"%} +{%from "populations/macro-display-population-card.html" import display_sui_population_card%} -{%block lvl2_breadcrumbs%} -<li {%if activelink=="populations"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - {%if population is mapping%} +{%block breadcrumbs%} +{{super()}} +{%if population%} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.view_population', - species_id=species.SpeciesId, - population_id=population.Id)}}">{{population.Name}}</a> - {%else%} - <a href="{{url_for('species.populations.index')}}">Populations</a> - {%endif%} + species_id=species['SpeciesId'], + population_id=population['Id'])}}"> + {{population["Name"]}} + </a> </li> -{%block lvl3_breadcrumbs%}{%endblock%} +{%endif%} +{%endblock%} + + +{%block sidebarcontents%} +{{display_sui_population_card(species, population)}} {%endblock%} diff --git a/uploader/templates/populations/create-population.html b/uploader/templates/populations/create-population.html index 007b6bf..d5359f5 100644 --- a/uploader/templates/populations/create-population.html +++ b/uploader/templates/populations/create-population.html @@ -1,20 +1,16 @@ {%extends "populations/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%from "species/macro-select-species.html" import select_species_form%} -{%from "species/macro-display-species-card.html" import display_species_card%} {%block title%}Create Population{%endblock%} {%block pagetitle%}Create Population{%endblock%} -{%block lvl3_breadcrumbs%} -<li {%if activelink=="create-population"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.create_population', - species_id=species.SpeciesId)}}">create population</a> + species_id=species['SpeciesId'])}}"> + create population</a> </li> {%endblock%} @@ -263,7 +259,3 @@ </form> </div> {%endblock%} - -{%block sidebarcontents%} -{{display_species_card(species)}} -{%endblock%} diff --git a/uploader/templates/populations/macro-display-population-card.html b/uploader/templates/populations/macro-display-population-card.html index 6b5f1e0..f3040ea 100644 --- a/uploader/templates/populations/macro-display-population-card.html +++ b/uploader/templates/populations/macro-display-population-card.html @@ -43,7 +43,7 @@ {%macro display_sui_population_card(species, population)%} {{display_sui_species_card(species)}} - +{%if population%} <div class="row"> <table class="table"> <caption>Current population</caption> @@ -75,4 +75,5 @@ </tbody> </table> </div> +{%endif%} {%endmacro%} diff --git a/uploader/templates/populations/sui-base.html b/uploader/templates/populations/sui-base.html deleted file mode 100644 index 0ca5c59..0000000 --- a/uploader/templates/populations/sui-base.html +++ /dev/null @@ -1,12 +0,0 @@ -{%extends "species/sui-base.html"%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('species.populations.view_population', - species_id=species['SpeciesId'], - population_id=population['Id'])}}"> - {{population["Name"]}} - </a> -</li> -{%endblock%} diff --git a/uploader/templates/populations/sui-view-population.html b/uploader/templates/populations/sui-view-population.html deleted file mode 100644 index a35eac6..0000000 --- a/uploader/templates/populations/sui-view-population.html +++ /dev/null @@ -1,127 +0,0 @@ -{%extends "populations/sui-base.html"%} -{%from "macro-step-indicator.html" import step_indicator%} -{%from "populations/macro-display-population-card.html" import display_sui_population_card%} - -{%block contents%} -<div class="row"> - <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2> -</div> - -<div class="row"> - <ul class="nav nav-tabs" id="population-actions"> - <li class="nav-item presentation"> - <button class="nav-link" - id="samples-tab" - data-bs-toggle="tab" - data-bs-target="#samples-content" - type="button" - role="tab" - aria-controls="samples-content" - aria-selected="true">Samples</button></li> - <li class="nav-item presentation"> - <button class="nav-link active" - id="phenotypes-tab" - data-bs-toggle="tab" - data-bs-target="#phenotypes-content" - type="button" - role="tab" - aria-controls="phenotypes-content" - aria-selected="false">Phenotypes</button></li> - {%if view_under_construction%} - <li class="nav-item presentation"> - <button class="nav-link" - id="genotypes-tab" - data-bs-toggle="tab" - data-bs-target="#genotypes-content" - type="button" - role="tab" - aria-controls="genotypes-content" - aria-selected="false">Genotypes</button></li> - <li class="nav-item presentation"> - <button class="nav-link" - id="expression-data-tab" - data-bs-toggle="tab" - data-bs-target="#expression-data-content" - type="button" - role="tab" - aria-controls="expression-data-content" - aria-selected="false">Expression-Data</button></li> - {%endif%} - </ul> -</div> - -<div class="row"> - <div class="tab-content" id="populations-tabs-content"> - <div class="tab-pane fade" - id="samples-content" - role="tabpanel" - aria-labelledby="samples-content-tab"> - <p>Think of a <strong>"sample"</strong> as say a single case or individual - in the experiment. It could even be a single strain (where applicable). - </p> - <p>This is a convenience feature for when you want to upload phenotypes to - the system, but do not have the genotypes data ready yet.</p> - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="View and upload samples for population '{{population['Name']}}'" - class="btn btn-primary">Manage Samples</a> - </div> - - <div class="tab-pane fade show active" - id="phenotypes-content" - role="tabpanel" - aria-labelledby="phenotypes-content-tab"> - - <div class="row" style="margin-top: 1em;"> - <h3> Phenotypes in Population "{{population.FullName}} ({{population.Name}})"</h3> - - <p>To view existing phenotype traits, or upload new ones, click the button below:</p> - - <div class="row"> - <div class="col"> - <a href="{{url_for( - 'species.populations.phenotypes.list_datasets', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="View and upload phenotype traits" - class="btn btn-primary">Phenotypes</a> - </div> - </div> - </div> - </div> - - <div class="tab-pane fade" - id="genotypes-content" - role="tabpanel" - aria-labelledby="genotypes-content-tab"> - <p>This allows you to upload the data that concerns your genotypes.</p> - <p>Any samples/individuals/cases/strains that do not already exist in the - system will be added. This does not delete any existing data.</p> - <a href="{{url_for('species.populations.genotypes.list_genotypes', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="Upload genotype information for the '{{population.FullName}}' population of the '{{species.FullName}}' species." - class="btn btn-primary">upload genotypes</a> - </div> - <div class="tab-pane fade" id="expression-data-content" role="tabpanel" aria-labelledby="expression-data-content-tab"> - <p>Upload expression data (mRNA data) for this population.</p> - <a href="#" title="" class="btn btn-primary">upload genotypes</a> - </div> - </div> -</div> -{%endblock%} - -{%block sidebarcontents%} -<div class="row"> - <p>Each tab presents a feature that's available at the population level. - Select the tab that allows you to continue with your task.</p> -</div> -{{display_sui_population_card(species, population)}} -{%endblock%} - - - - -{%block javascript%} -{%endblock%} diff --git a/uploader/templates/populations/view-population.html b/uploader/templates/populations/view-population.html index 3b9661b..ac89bc7 100644 --- a/uploader/templates/populations/view-population.html +++ b/uploader/templates/populations/view-population.html @@ -1,104 +1,127 @@ {%extends "populations/base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "species/macro-select-species.html" import select_species_form%} -{%from "species/macro-display-species-card.html" import display_species_card%} - -{%block title%}Populations{%endblock%} - -{%block pagetitle%}Populations{%endblock%} - -{%block lvl3_breadcrumbs%} -<li {%if activelink=="view-population"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.view_population', - species_id=species.SpeciesId, - population_id=population.InbredSetId)}}">view</a> -</li> -{%endblock%} - +{%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>Population Details</h2> - - {{flash_all_messages()}} - - <dl> - <dt>Name</dt> - <dd>{{population.Name}}</dd> - - <dt>FullName</dt> - <dd>{{population.FullName}}</dd> - - <dt>Code</dt> - <dd>{{population.InbredSetCode}}</dd> - - <dt>Genetic Type</dt> - <dd>{{population.GeneticType}}</dd> - - <dt>Family</dt> - <dd>{{population.Family}}</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> + <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2> </div> <div class="row"> - … maybe provide a way to organise populations in the same family here … + <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"> - <h3>Actions</h3> - - <p> - Click any of the following links to use this population in performing the - subsequent operations. - </p> - - <nav class="nav"> - <ul> - <li> - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="Manage samples: Add new or delete existing."> - manage samples</a> - </li> - <li> - <a href="{{url_for('species.populations.genotypes.list_genotypes', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="Manage genotypes for {{species.FullName}}">Manage Genotypes</a> - </li> - <li> - <a href="{{url_for('species.populations.phenotypes.list_datasets', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="Manage phenotype data.">manage phenotype data</a> - </li> - <li> - <a href="#" title="Manage expression data" - class="not-implemented">manage expression data</a> - </li> - <li> - <a href="#" title="Manage individual data" - class="not-implemented">manage individual data</a> - </li> - <li> - <a href="#" title="Manage RNA-Seq data" - class="not-implemented">manage RNA-Seq data</a> - </li> - </ul> - </nav> + <div class="tab-content" id="populations-tabs-content"> + <div class="tab-pane fade" + id="samples-content" + role="tabpanel" + aria-labelledby="samples-content-tab"> + <p>Think of a <strong>"sample"</strong> as say a single case or individual + in the experiment. It could even be a single strain (where applicable). + </p> + <p>This is a convenience feature for when you want to upload phenotypes to + the system, but do not have the genotypes data ready yet.</p> + <a href="{{url_for('species.populations.samples.list_samples', + species_id=species.SpeciesId, + population_id=population.Id)}}" + title="View and upload samples for population '{{population['Name']}}'" + class="btn btn-primary">Manage Samples</a> + </div> + + <div class="tab-pane fade show active" + id="phenotypes-content" + role="tabpanel" + aria-labelledby="phenotypes-content-tab"> + + <div class="row" style="margin-top: 1em;"> + <h3> Phenotypes in Population "{{population.FullName}} ({{population.Name}})"</h3> + + <p>To view existing phenotype traits, or upload new ones, click the button below:</p> + + <div class="row"> + <div class="col"> + <a href="{{url_for( + 'species.populations.phenotypes.list_datasets', + species_id=species.SpeciesId, + population_id=population.Id)}}" + title="View and upload phenotype traits" + class="btn btn-primary">Phenotypes</a> + </div> + </div> + </div> + </div> + + <div class="tab-pane fade" + id="genotypes-content" + role="tabpanel" + aria-labelledby="genotypes-content-tab"> + <p>This allows you to upload the data that concerns your genotypes.</p> + <p>Any samples/individuals/cases/strains that do not already exist in the + system will be added. This does not delete any existing data.</p> + <a href="{{url_for('species.populations.genotypes.list_genotypes', + species_id=species.SpeciesId, + population_id=population.Id)}}" + title="Upload genotype information for the '{{population.FullName}}' population of the '{{species.FullName}}' species." + class="btn btn-primary">upload genotypes</a> + </div> + <div class="tab-pane fade" id="expression-data-content" role="tabpanel" aria-labelledby="expression-data-content-tab"> + <p>Upload expression data (mRNA data) for this population.</p> + <a href="#" title="" class="btn btn-primary">upload genotypes</a> + </div> + </div> </div> {%endblock%} {%block sidebarcontents%} -{{display_species_card(species)}} +<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> +{{super()}} +{%endblock%} + + + + +{%block javascript%} {%endblock%} diff --git a/uploader/templates/publications/base.html b/uploader/templates/publications/base.html index db80bfa..de0a350 100644 --- a/uploader/templates/publications/base.html +++ b/uploader/templates/publications/base.html @@ -1,12 +1,9 @@ {%extends "base.html"%} -{%block lvl1_breadcrumbs%} -<li {%if activelink=="publications"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('publications.index')}}">Publications</a> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.index')}}" + title="Manage publications">Publications</a> </li> -{%block lvl2_breadcrumbs%}{%endblock%} {%endblock%} diff --git a/uploader/templates/publications/create-publication.html b/uploader/templates/publications/create-publication.html index 3f828a9..fb0127d 100644 --- a/uploader/templates/publications/create-publication.html +++ b/uploader/templates/publications/create-publication.html @@ -3,7 +3,13 @@ {%block title%}View Publication{%endblock%} -{%block pagetitle%}View Publication{%endblock%} +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.create_publication', **get_args)}}" + title="Manage publications">create publication</a> +</li> +{%endblock%} {%block contents%} @@ -12,7 +18,7 @@ <div class="row"> <form id="frm-create-publication" method="POST" - action="{{url_for('publications.create_publication', **request.args)}}" + action="{{url_for('publications.create_publication', **get_args)}}" class="form-horizontal"> <div class="row mb-3"> @@ -152,11 +158,14 @@ </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="Add" /> - <input type="reset" class="btn btn-danger" /> + <div class="row"> + <div class="col"> + <input type="submit" + class="btn btn-primary" + value="create publication" /> + </div> + <div class="col"> + <input type="reset" class="btn btn-danger" value="reset form" /> </div> </div> diff --git a/uploader/templates/publications/delete-publication.html b/uploader/templates/publications/delete-publication.html index 0ac93ec..a9c8c7c 100644 --- a/uploader/templates/publications/delete-publication.html +++ b/uploader/templates/publications/delete-publication.html @@ -1,9 +1,16 @@ {%extends "publications/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%block title%}View Publication{%endblock%} +{%block title%}Delete Publication{%endblock%} -{%block pagetitle%}View Publication{%endblock%} +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.delete_publication', + publication_id=publication.Id)}}" + title="Manage publications">delete publication</a> +</li> +{%endblock%} {%block contents%} diff --git a/uploader/templates/publications/edit-publication.html b/uploader/templates/publications/edit-publication.html index 97fa134..314a78c 100644 --- a/uploader/templates/publications/edit-publication.html +++ b/uploader/templates/publications/edit-publication.html @@ -1,9 +1,16 @@ {%extends "publications/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%block title%}View Publication{%endblock%} - -{%block pagetitle%}View Publication{%endblock%} +{%block title%}Edit Publication{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.edit_publication', + publication_id=publication.Id)}}" + title="Edit the publication's details">edit publication</a> +</li> +{%endblock%} {%block contents%} diff --git a/uploader/templates/publications/index.html b/uploader/templates/publications/index.html index 369812b..54d3fc0 100644 --- a/uploader/templates/publications/index.html +++ b/uploader/templates/publications/index.html @@ -3,16 +3,22 @@ {%block title%}Publications{%endblock%} -{%block pagetitle%}Publications{%endblock%} - {%block contents%} {{flash_all_messages()}} <div class="row" style="padding-bottom: 1em;"> - <a href="{{url_for('publications.create_publication')}}" - class="btn btn-primary"> - add new publication</a> + <div class="col"> + <a href="{{url_for('publications.create_publication')}}" + class="btn btn-primary" + title="Create a new publication."> + add new publication</a> + </div> +</div> + +<div class="row"> + <p>Click on title to view more details and to edit details for that + publication.</p> </div> <div class="row"> @@ -33,6 +39,8 @@ {%block javascript%} +<script type="text/javascript" src="/static/js/urls.js"></script> + <script type="text/javascript"> $(function() { var publicationsDataTable = buildDataTable( @@ -43,24 +51,25 @@ { searchable: true, data: (pub) => { - if(pub.PubMed_ID) { - return `<a href="https://pubmed.ncbi.nlm.nih.gov/` + - `${pub.PubMed_ID}/" target="_blank" ` + - `title="Link to publication on NCBI.">` + - `${pub.PubMed_ID}</a>`; - } - return ""; + if(pub.PubMed_ID) { + return `<a href="https://pubmed.ncbi.nlm.nih.gov/` + + `${pub.PubMed_ID}/" target="_blank" ` + + `title="Link to publication on NCBI.">` + + `${pub.PubMed_ID}</a>`; + } + return ""; } }, { searchable: true, data: (pub) => { - var title = "⸻"; - if(pub.Title) { - title = pub.Title - } - return `<a href="/publications/view/${pub.Id}" ` + - `target="_blank" ` + + var title = "⸻"; + if(pub.Title) { + title = pub.Title + } + url=buildURLFromCurrentURL( + `/publications/view/${pub.Id}`); + return `<a href="${url}" target="_blank" ` + `title="Link to view publication details">` + `${title}</a>`; } @@ -68,12 +77,12 @@ { searchable: true, data: (pub) => { - authors = pub.Authors.split(",").map( - (item) => {return item.trim();}); - if(authors.length > 1) { - return authors[0] + ", et. al."; - } - return authors[0]; + authors = pub.Authors.split(",").map( + (item) => {return item.trim();}); + if(authors.length > 1) { + return authors[0] + ", et. al."; + } + return authors[0]; } } ], diff --git a/uploader/templates/publications/sui-base.html b/uploader/templates/publications/sui-base.html deleted file mode 100644 index 64e41ef..0000000 --- a/uploader/templates/publications/sui-base.html +++ /dev/null @@ -1,9 +0,0 @@ -{%extends "sui-base.html"%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('publications.index')}}" - title="Manage publications">Publications</a> -</li> -{%endblock%} diff --git a/uploader/templates/publications/sui-create-publication.html b/uploader/templates/publications/sui-create-publication.html deleted file mode 100644 index 81edca6..0000000 --- a/uploader/templates/publications/sui-create-publication.html +++ /dev/null @@ -1,200 +0,0 @@ -{%extends "publications/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}View Publication{%endblock%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('publications.create_publication', **get_args)}}" - title="Manage publications">create publication</a> -</li> -{%endblock%} - - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <form id="frm-create-publication" - method="POST" - action="{{url_for('publications.create_publication', **get_args)}}" - class="form-horizontal"> - - <div class="row mb-3"> - <label for="txt-pubmed-id" class="col-sm-2 col-form-label"> - PubMed ID</label> - <div class="col-sm-10"> - <div class="input-group"> - <input type="text" - id="txt-pubmed-id" - name="pubmed-id" - class="form-control"/> - <div class="input-group-text"> - <button class="btn btn-outline-primary" - id="btn-search-pubmed-id">search</button> - </div> - </div> - <span id="search-pubmed-id-error" - class="form-text text-muted text-danger visually-hidden"> - </span> - <span class="form-text text-muted">This is the publication's ID on - <a href="https://pubmed.ncbi.nlm.nih.gov/" - title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a> - </span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-title" class="col-sm-2 col-form-label"> - Title</label> - <div class="col-sm-10"> - <input type="text" - id="txt-publication-title" - name="publication-title" - class="form-control" /> - <span class="form-text text-muted">Provide the publication's title here.</span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-authors" class="col-sm-2 col-form-label"> - Authors</label> - <div class="col-sm-10"> - <input type="text" - id="txt-publication-authors" - name="publication-authors" - required="required" - class="form-control" /> - <span class="form-text text-muted"> - A publication <strong>MUST</strong> have an author. You <em>must</em> - provide a value for the authors field. - </span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-journal" class="col-sm-2 col-form-label"> - Journal</label> - <div class="col-sm-10"> - <input type="text" - id="txt-publication-journal" - name="publication-journal" - class="form-control" /> - <span class="form-text text-muted">Provide the name journal where the - publication was done, here.</span> - </div> - </div> - - <div class="row mb-3"> - <label for="select-publication-month" - class="col-sm-2 col-form-label"> - Month</label> - <div class="col-sm-4"> - <select class="form-control" - id="select-publication-month" - name="publication-month"> - <option value="">Select a month</option> - <option value="january">January</option> - <option value="february">February</option> - <option value="march">March</option> - <option value="april">April</option> - <option value="may">May</option> - <option value="june">June</option> - <option value="july">July</option> - <option value="august">August</option> - <option value="september">September</option> - <option value="october">October</option> - <option value="november">November</option> - <option value="december">December</option> - </select> - <span class="form-text text-muted">Month of publication</span> - </div> - - <label for="txt-publication-year" - class="col-sm-2 col-form-label"> - Year</label> - <div class="col-sm-4"> - <input type="number" - id="txt-publication-year" - name="publication-year" - class="form-control" - min="1960" /> - <span class="form-text text-muted">Year of publication</span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-volume" - class="col-sm-2 col-form-label"> - Volume</label> - <div class="col-sm-4"> - <input type="text" - id="txt-publication-volume" - name="publication-volume" - class="form-control"> - <span class="form-text text-muted">Journal volume</span> - </div> - - <label for="txt-publication-pages" - class="col-sm-2 col-form-label"> - Pages</label> - <div class="col-sm-4"> - <input type="text" - id="txt-publication-pages" - name="publication-pages" - class="form-control" /> - <span class="form-text text-muted">Journal pages for the publication</span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label> - <div class="col-sm-10"> - <textarea id="txt-publication-abstract" - name="publication-abstract" - class="form-control" - rows="7"></textarea> - </div> - </div> - - <div class="row"> - <div class="col"> - <input type="submit" - class="btn btn-primary" - value="create publication" /> - </div> - <div class="col"> - <input type="reset" class="btn btn-danger" value="reset form" /> - </div> - </div> - -</form> -</div> - -{%endblock%} - - -{%block javascript%} -<script type="text/javascript" src="/static/js/pubmed.js"></script> -<script type="text/javascript"> - $(function() { - $("#btn-search-pubmed-id").on("click", (event) => { - event.preventDefault(); - var search_button = event.target; - var pubmed_id = $("#txt-pubmed-id").val().trim(); - remove_class($("#txt-pubmed-id").parent(), "has-error"); - if(pubmed_id == "") { - add_class($("#txt-pubmed-id").parent(), "has-error"); - return false; - } - - search_button.disabled = true; - // Fetch publication details - fetch_publication_details(pubmed_id, - [() => {search_button.disabled = false;}]); - return false; - }); - }); -</script> -{%endblock%} diff --git a/uploader/templates/publications/sui-delete-publication.html b/uploader/templates/publications/sui-delete-publication.html deleted file mode 100644 index 436f2c1..0000000 --- a/uploader/templates/publications/sui-delete-publication.html +++ /dev/null @@ -1,95 +0,0 @@ -{%extends "publications/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Delete Publication{%endblock%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('publications.delete_publication', - publication_id=publication.Id)}}" - title="Manage publications">delete publication</a> -</li> -{%endblock%} - - -{%block contents%} -{{flash_all_messages()}} -<div class="row"> - <p>You are about to delete the publication with the following details:</p> -</div> - -<div class="row"> - <table class="table"> - <tr> - <th>Linked Phenotypes</th> - <td>{{linked_phenotypes | count}}</td> - </tr> - <tr> - <th>PubMed</th> - <td> - {%if publication.PubMed_ID%} - <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/" - target="_blank">{{publication.PubMed_ID}}</a> - {%else%} - — - {%endif%} - </td> - </tr> - <tr> - <th>Title</th> - <td>{{publication.Title or "—"}}</td> - </tr> - <tr> - <th>Authors</th> - <td>{{publication.Authors or "—"}}</td> - </tr> - <tr> - <th>Journal</th> - <td>{{publication.Journal or "—"}}</td> - </tr> - <tr> - <th>Published</th> - <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td> - </tr> - <tr> - <th>Volume</th> - <td>{{publication.Volume or "—"}}</td> - </tr> - <tr> - <th>Pages</th> - <td>{{publication.Pages or "—"}}</td> - </tr> - <tr> - <th>Abstract</th> - <td> - {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%} - <p>{{line}}</p> - {%endfor%} - </td> - </tr> - </table> -</div> - -<div class="row"> - <p>If you are sure that is what you want, click the button below to delete the - publication</p> - <p class="form-text text-small"> - <small>You will not be able to recover the data if you click - delete below.</small></p> - - <form action="{{url_for('publications.delete_publication', publication_id=publication_id)}}" - method="POST"> - <div class="form-group"> - <input type="submit" value="delete" class="btn btn-danger" /> - </div> - </form> -</div> -{%endblock%} - - -{%block javascript%} -<script type="text/javascript"> - $(function() {}); -</script> -{%endblock%} diff --git a/uploader/templates/publications/sui-edit-publication.html b/uploader/templates/publications/sui-edit-publication.html deleted file mode 100644 index 847b020..0000000 --- a/uploader/templates/publications/sui-edit-publication.html +++ /dev/null @@ -1,203 +0,0 @@ -{%extends "publications/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Edit Publication{%endblock%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('publications.edit_publication', - publication_id=publication.Id)}}" - title="Edit the publication's details">edit publication</a> -</li> -{%endblock%} - - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <form id="frm-create-publication" - method="POST" - action="{{url_for('publications.edit_publication', - publication_id=publication_id, - next=request.args.get('next', ''))}}" - class="form-horizontal"> - - <div class="row mb-3"> - <label for="txt-pubmed-id" class="col-sm-2 col-form-label"> - PubMed ID</label> - <div class="col-sm-10"> - <div class="input-group"> - <input type="text" - id="txt-pubmed-id" - name="pubmed-id" - value="{{publication.PubMed_ID or ''}}" - class="form-control" /> - <div class="input-group-text"> - <button class="btn btn-outline-primary" - id="btn-search-pubmed-id">search</button> - </div> - </div> - <span id="search-pubmed-id-error" - class="form-text text-muted text-danger visually-hidden"> - </span> - <span class="form-text text-muted">This is the publication's ID on - <a href="https://pubmed.ncbi.nlm.nih.gov/" - title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a> - </span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-title" class="col-sm-2 col-form-label"> - Title</label> - <div class="col-sm-10"> - <input type="text" - id="txt-publication-title" - name="publication-title" - value="{{publication.Title}}" - class="form-control" /> - <span class="form-text text-muted">Provide the publication's title here.</span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-authors" class="col-sm-2 col-form-label"> - Authors</label> - <div class="col-sm-10"> - <input type="text" - id="txt-publication-authors" - name="publication-authors" - value="{{publication.Authors}}" - required="required" - class="form-control" /> - <span class="form-text text-muted"> - A publication <strong>MUST</strong> have an author. You <em>must</em> - provide a value for the authors field. - </span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-journal" class="col-sm-2 col-form-label"> - Journal</label> - <div class="col-sm-10"> - <input type="text" - id="txt-publication-journal" - name="publication-journal" - value="{{publication.Journal}}" - class="form-control" /> - <span class="form-text text-muted">Provide the name journal where the - publication was done, here.</span> - </div> - </div> - - <div class="row mb-3"> - <label for="select-publication-month" - class="col-sm-2 col-form-label"> - Month</label> - <div class="col-sm-4"> - <select class="form-control" - id="select-publication-month" - name="publication-month"> - <option value="">Select a month</option> - {%for month in ("january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"):%} - <option value="{{month}}" - {%if publication.Month | lower == month %} - selected="selected" - {%endif%}> - {{month | title}} - </option> - {%endfor%} - </select> - <span class="form-text text-muted">Month of publication</span> - </div> - - <label for="txt-publication-year" - class="col-sm-2 col-form-label"> - Year</label> - <div class="col-sm-4"> - <input type="number" - id="txt-publication-year" - name="publication-year" - value="{{publication.Year}}" - class="form-control" - min="1960" /> - <span class="form-text text-muted">Year of publication</span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-publication-volume" - class="col-sm-2 col-form-label"> - Volume</label> - <div class="col-sm-4"> - <input type="text" - id="txt-publication-volume" - name="publication-volume" - value="{{publication.Volume}}" - class="form-control"> - <span class="form-text text-muted">Journal volume</span> - </div> - - <label for="txt-publication-pages" - class="col-sm-2 col-form-label"> - Pages</label> - <div class="col-sm-4"> - <input type="text" - id="txt-publication-pages" - name="publication-pages" - value="{{publication.Pages}}" - class="form-control" /> - <span class="form-text text-muted">Journal pages for the publication</span> - </div> - </div> - - <div class="row mb-3"> - <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label> - <div class="col-sm-10"> - <textarea id="txt-publication-abstract" - name="publication-abstract" - class="form-control" - rows="7">{{publication.Abstract or ""}}</textarea> - </div> - </div> - - <div class="row mb-3"> - <div class="col-sm-2"></div> - <div class="col-sm-8"> - <input type="submit" class="btn btn-primary" value="Save" /> - <input type="reset" class="btn btn-danger" /> - </div> - </div> - -</form> -</div> - -{%endblock%} - - -{%block javascript%} -<script type="text/javascript" src="/static/js/pubmed.js"></script> -<script type="text/javascript"> - $(function() { - $("#btn-search-pubmed-id").on("click", (event) => { - event.preventDefault(); - var search_button = event.target; - var pubmed_id = $("#txt-pubmed-id").val().trim(); - remove_class($("#txt-pubmed-id").parent(), "has-error"); - if(pubmed_id == "") { - add_class($("#txt-pubmed-id").parent(), "has-error"); - return false; - } - - search_button.disabled = true; - // Fetch publication details - fetch_publication_details(pubmed_id, - [() => {search_button.disabled = false;}]); - return false; - }); - }); -</script> -{%endblock%} diff --git a/uploader/templates/publications/sui-index.html b/uploader/templates/publications/sui-index.html deleted file mode 100644 index e405dd1..0000000 --- a/uploader/templates/publications/sui-index.html +++ /dev/null @@ -1,109 +0,0 @@ -{%extends "publications/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Publications{%endblock%} - - -{%block contents%} -{{flash_all_messages()}} - -<div class="row" style="padding-bottom: 1em;"> - <div class="col"> - <a href="{{url_for('publications.create_publication')}}" - class="btn btn-primary" - title="Create a new publication."> - add new publication</a> - </div> -</div> - -<div class="row"> - <p>Click on title to view more details and to edit details for that - publication.</p> -</div> - -<div class="row"> - <table id="tbl-list-publications" class="table compact stripe"> - <thead> - <tr> - <th>#</th> - <th>PubMed ID</th> - <th>Title</th> - <th>Authors</th> - </tr> - </thead> - - <tbody></tbody> - </table> -</div> -{%endblock%} - - -{%block javascript%} -<script type="text/javascript" src="/static/js/urls.js"></script> - -<script type="text/javascript"> - $(function() { - var publicationsDataTable = buildDataTable( - "#tbl-list-publications", - [], - [ - {data: "index"}, - { - searchable: true, - data: (pub) => { - if(pub.PubMed_ID) { - return `<a href="https://pubmed.ncbi.nlm.nih.gov/` + - `${pub.PubMed_ID}/" target="_blank" ` + - `title="Link to publication on NCBI.">` + - `${pub.PubMed_ID}</a>`; - } - return ""; - } - }, - { - searchable: true, - data: (pub) => { - var title = "⸻"; - if(pub.Title) { - title = pub.Title - } - url=buildURLFromCurrentURL( - `/publications/view/${pub.Id}`); - return `<a href="${url}" target="_blank" ` + - `title="Link to view publication details">` + - `${title}</a>`; - } - }, - { - searchable: true, - data: (pub) => { - authors = pub.Authors.split(",").map( - (item) => {return item.trim();}); - if(authors.length > 1) { - return authors[0] + ", et. al."; - } - return authors[0]; - } - } - ], - { - serverSide: true, - ajax: { - url: "/publications/list", - dataSrc: "publications" - }, - scrollY: 700, - scroller: true, - scrollCollapse: true, - paging: true, - deferRender: true, - layout: { - topStart: "info", - topEnd: "search", - bottomStart: "pageLength", - bottomEnd: false - } - }); - }); -</script> -{%endblock%} diff --git a/uploader/templates/publications/sui-view-publication.html b/uploader/templates/publications/sui-view-publication.html deleted file mode 100644 index 740fc37..0000000 --- a/uploader/templates/publications/sui-view-publication.html +++ /dev/null @@ -1,80 +0,0 @@ -{%extends "publications/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}View Publication{%endblock%} - - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <table class="table"> - <tr> - <th>Linked Phenotypes</th> - <td>{{linked_phenotypes | count}}</td> - </tr> - <tr> - <th>PubMed</th> - <td> - {%if publication.PubMed_ID%} - <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/" - target="_blank">{{publication.PubMed_ID}}</a> - {%else%} - — - {%endif%} - </td> - </tr> - <tr> - <th>Title</th> - <td>{{publication.Title or "—"}}</td> - </tr> - <tr> - <th>Authors</th> - <td>{{publication.Authors or "—"}}</td> - </tr> - <tr> - <th>Journal</th> - <td>{{publication.Journal or "—"}}</td> - </tr> - <tr> - <th>Published</th> - <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td> - </tr> - <tr> - <th>Volume</th> - <td>{{publication.Volume or "—"}}</td> - </tr> - <tr> - <th>Pages</th> - <td>{{publication.Pages or "—"}}</td> - </tr> - <tr> - <th>Abstract</th> - <td> - {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%} - <p>{{line}}</p> - {%endfor%} - </td> - </tr> - </table> -</div> - -<div class="row"> - <div> - <a href="{{url_for('publications.edit_publication', publication_id=publication.Id)}}" - title="Edit details for this publication." - class="btn btn-primary">Edit</a> - {%if linked_phenotypes | length == 0%} - <a href="{{url_for('publications.delete_publication', publication_id=publication.Id)}}" - class="btn btn-danger">delete</a> - {%endif%} - </div> -</div> -{%endblock%} - - -{%block javascript%} -<script type="text/javascript"> - $(function() {}); -</script> -{%endblock%} diff --git a/uploader/templates/publications/view-publication.html b/uploader/templates/publications/view-publication.html index 0bd7bc5..01ccf1e 100644 --- a/uploader/templates/publications/view-publication.html +++ b/uploader/templates/publications/view-publication.html @@ -3,8 +3,6 @@ {%block title%}View Publication{%endblock%} -{%block pagetitle%}View Publication{%endblock%} - {%block contents%} {{flash_all_messages()}} diff --git a/uploader/templates/samples/base.html b/uploader/templates/samples/base.html index 291782b..7fd5020 100644 --- a/uploader/templates/samples/base.html +++ b/uploader/templates/samples/base.html @@ -1,12 +1,25 @@ {%extends "populations/base.html"%} +{%from "populations/macro-display-population-card.html" import display_sui_population_card%} -{%block lvl3_breadcrumbs%} -<li {%if activelink=="samples"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.samples.index')}}">Samples</a> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('species.populations.samples.list_samples', + species_id=species['SpeciesId'], + population_id=population.Id)}}"> + Samples + </a> </li> -{%block lvl4_breadcrumbs%}{%endblock%} +{%endblock%} + +{%block contents%} +<div class="row"> + <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2> +</div> +{%endblock%} + + + +{%block sidebarcontents%} +{{display_sui_population_card(species, population)}} {%endblock%} diff --git a/uploader/templates/samples/list-samples.html b/uploader/templates/samples/list-samples.html index aed27c3..3aac984 100644 --- a/uploader/templates/samples/list-samples.html +++ b/uploader/templates/samples/list-samples.html @@ -1,53 +1,34 @@ {%extends "samples/base.html"%} {%from "flash_messages.html" import flash_all_messages%} {%from "populations/macro-select-population.html" import select_population_form%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block title%}Samples — List Samples{%endblock%} -{%block pagetitle%}Samples — List Samples{%endblock%} - -{%block lvl4_breadcrumbs%} -<li {%if activelink=="list-samples"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id)}}">List</a> -</li> -{%endblock%} - {%block contents%} -{{flash_all_messages()}} +{{super()}} <div class="row"> - <p> - You selected the population "{{population.FullName}}" from the - "{{species.FullName}}" species. - </p> + <h3 class="subheading">manage samples</h3> + {{flash_all_messages()}} </div> <div class="row"> - <p> + <div class="col"> <a href="{{url_for('species.populations.samples.upload_samples', species_id=species.SpeciesId, population_id=population.Id)}}" title="Add samples for population '{{population.FullName}}' from species '{{species.FullName}}'." - class="btn btn-primary"> - add samples - </a> - </p> + class="btn btn-primary">add new samples</a> + </div> </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. + Population "{{population.FullName}} ({{population.Name}})" already has + <strong>{{total_samples}}</strong> samples/individuals entered. You can + explore the list of samples in the table below. </p> </div> @@ -106,15 +87,6 @@ {%endfor%} </tbody> </table> - - <p> - <a href="#" - title="Delete samples from population '{{population.FullName}}' from species - '{{species.FullName}}'." - class="btn btn-danger not-implemented"> - delete all samples - </a> - </p> </div> {%else%} <div class="row"> @@ -124,7 +96,3 @@ {%endif%} {%endblock%} - -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/samples/sui-base.html b/uploader/templates/samples/sui-base.html deleted file mode 100644 index 8ec7505..0000000 --- a/uploader/templates/samples/sui-base.html +++ /dev/null @@ -1,25 +0,0 @@ -{%extends "populations/sui-base.html"%} -{%from "populations/macro-display-population-card.html" import display_sui_population_card%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species['SpeciesId'], - population_id=population.Id)}}"> - Samples - </a> -</li> -{%endblock%} - -{%block contents%} -<div class="row"> - <h2 class="heading">{{population.FullName}} ({{population.Name}})</h2> -</div> -{%endblock%} - - - -{%block sidebarcontents%} -{{display_sui_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/samples/sui-list-samples.html b/uploader/templates/samples/sui-list-samples.html deleted file mode 100644 index e9ed71a..0000000 --- a/uploader/templates/samples/sui-list-samples.html +++ /dev/null @@ -1,98 +0,0 @@ -{%extends "samples/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "populations/macro-select-population.html" import select_population_form%} - -{%block title%}Samples — List Samples{%endblock%} - -{%block contents%} -{{super()}} - -<div class="row"> - <h3 class="subheading">manage samples</h3> - {{flash_all_messages()}} -</div> - -<div class="row"> - <div class="col"> - <a href="{{url_for('species.populations.samples.upload_samples', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="Add samples for population '{{population.FullName}}' from species - '{{species.FullName}}'." - class="btn btn-primary">add new samples</a> - </div> -</div> - -{%if samples | length > 0%} -<div class="row"> - <p> - Population "{{population.FullName}} ({{population.Name}})" already has - <strong>{{total_samples}}</strong> samples/individuals entered. You can - explore the list of samples in the table below. - </p> -</div> - -<div class="row"> - <div class="col-md-2"> - {%if offset > 0:%} - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id, - from=offset-count, - count=count)}}"> - <span class="glyphicon glyphicon-backward"></span> - Previous - </a> - {%endif%} - </div> - - <div class="col-md-8" style="text-align: center;"> - Samples {{offset}} — {{offset+(count if offset + count < total_samples else total_samples - offset)}} / {{total_samples}} - </div> - - <div class="col-md-2"> - {%if offset + count < total_samples:%} - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id, - from=offset+count, - count=count)}}"> - Next - <span class="glyphicon glyphicon-forward"></span> - </a> - {%endif%} - </div> -</div> -<div class="row"> - <table class="table"> - <thead> - <tr> - <th></th> - <th>Name</th> - <th>Auxilliary Name</th> - <th>Symbol</th> - <th>Alias</th> - </tr> - </thead> - - <tbody> - {%for sample in samples%} - <tr> - <td>{{sample.sequence_number}}</td> - <td>{{sample.Name}}</td> - <td>{{sample.Name2}}</td> - <td>{{sample.Symbol or "-"}}</td> - <td>{{sample.Alias or "-"}}</td> - </tr> - {%endfor%} - </tbody> - </table> -</div> -{%else%} -<div class="row"> - <p>There are no samples entered for this population. Click the "Add Samples" - button above, to add some new samples.</p> -</div> -{%endif%} - -{%endblock%} diff --git a/uploader/templates/samples/sui-upload-failure.html b/uploader/templates/samples/sui-upload-failure.html deleted file mode 100644 index d950c50..0000000 --- a/uploader/templates/samples/sui-upload-failure.html +++ /dev/null @@ -1,32 +0,0 @@ -{%extends "sui-base.html"%} -{%from "cli-output.html" import cli_output%} - -{%block title%}Samples Upload Failure{%endblock%} - -{%block contents%} -<div class="row"> -<h2 class="heading">{{job.job_name[0:50]}}…</h2> - -<p>There was a failure attempting to upload the samples.</p> - -<p>Here is some information to help with debugging the issue. Provide this - information to the developer/maintainer.</p> - -<h3>Debugging Information</h3> -<ul> - <li><strong>job id</strong>: {{job.jobid}}</li> - <li><strong>status</strong>: {{job.status}}</li> - <li><strong>job type</strong>: {{job["job-type"]}}</li> -</ul> -</div> - -<div class="row"> -<h4>stdout</h4> -{{cli_output(job, "stdout")}} -</div> - -<div class="row"> -<h4>stderr</h4> -{{cli_output(job, "stderr")}} -</div> -{%endblock%} diff --git a/uploader/templates/samples/sui-upload-progress.html b/uploader/templates/samples/sui-upload-progress.html deleted file mode 100644 index 8412c46..0000000 --- a/uploader/templates/samples/sui-upload-progress.html +++ /dev/null @@ -1,26 +0,0 @@ -{%extends "samples/sui-base.html"%} -{%from "cli-output.html" import cli_output%} - -{%block extrameta%} -<meta http-equiv="refresh" content="5"> -{%endblock%} - -{%block title%}Job Status{%endblock%} - -{%block contents%} -<div class="row" style="overflow-x: clip;"> -<h2 class="heading">{{job.job_name[0:50]}}…</h2> - -<p> -<strong>status</strong>: -<span>{{job["status"]}} ({{job.get("message", "-")}})</span><br /> -</p> - -<p>saving to database...</p> -</div> - -<div class="row"> - {{cli_output(job, "stdout")}} -</div> - -{%endblock%} diff --git a/uploader/templates/samples/sui-upload-samples.html b/uploader/templates/samples/sui-upload-samples.html deleted file mode 100644 index 83c2061..0000000 --- a/uploader/templates/samples/sui-upload-samples.html +++ /dev/null @@ -1,153 +0,0 @@ -{%extends "samples/sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} - -{%block title%}Samples — Upload Samples{%endblock%} - -{%block breadcrumbs%} -{{super()}} -<li class="breadcrumb-item"> - <a href="{{url_for('species.populations.samples.upload_samples', - species_id=species['SpeciesId'], - population_id=population.Id)}}"> - Upload - </a> -</li> -{%endblock%} - -{%block contents%} -{{flash_all_messages()}} - -<div class="row"> - <form id="form-samples" - method="POST" - action="{{url_for('species.populations.samples.upload_samples', - species_id=species.SpeciesId, - population_id=population.InbredSetId)}}" - enctype="multipart/form-data"> - <legend class="heading">upload samples</legend> - - <input type="hidden" name="species_id" value="{{species.SpeciesId}}" /> - <input type="hidden" name="population_id" value="{{population.Id}}" /> - - <div class="form-group"> - <label for="file-samples" class="form-label">select file</label> - <input type="file" name="samples_file" id="file-samples" - accept="text/csv, text/tab-separated-values, text/plain" - class="form-control" /> - <small class="form-text text-muted"> - See the <a href="#docs-samples-upload">documentation below</a> for - details on expected file format.</small> - </div> - - <div class="form-group"> - <label for="select-separator" class="form-label">field separator</label> - <select id="select-separator" - name="separator" - required="required" - class="form-control"> - <option value="">Select separator for your file: (default is comma)</option> - <option value="	">TAB</option> - <option value=" ">Space</option> - <option value=",">Comma</option> - <option value=";">Semicolon</option> - <option value="other">Other</option> - </select> - <input id="txt-separator" - type="text" - name="other_separator" - class="form-control" /> - <small class="form-text text-muted"> - If you select '<strong>Other</strong>' for the field separator value, - enter the character that separates the fields in your CSV file in the form - field below. - </small> - </div> - - <div class="form-group form-check"> - <input id="chk-heading" - type="checkbox" - name="first_line_heading" - class="form-check-input" /> - <label for="chk-heading" class="form-check-label"> - first line is a heading?</label> - <small class="form-text text-muted"> - Select this if the first line in your file contains headings for the - columns. - </small> - </div> - - <div class="form-group"> - <label for="txt-delimiter" class="form-label">field delimiter</label> - <input id="txt-delimiter" - type="text" - name="field_delimiter" - maxlength="1" - class="form-control" /> - <small class="form-text text-muted"> - If there is a character delimiting the string texts within particular - fields in your CSV, provide the character here. This can be left blank if - no such delimiters exist in your file. - </small> - </div> - - <button type="submit" - class="btn btn-primary">upload samples file</button> - </form> -</div> - -<div class="row"> - <h3>Preview File Content</h3> - - <table id="tbl:samples-preview" class="table"> - <caption class="heading">preview content</caption> - - <thead> - <tr> - <th>Name</th> - <th>Name2</th> - <th>Symbol</th> - <th>Alias</th> - </tr> - </thead> - - <tbody> - <tr id="default-row"> - <td colspan="4"> - Please make some selections in the form above to preview the data.</td> - </tr> - </tbody> - </table> -</div> - - - -<div class="row" id="docs-samples-upload"> - <h3 class="subheading">File Format</h3> - <p> - Upload a <strong>character-separated value (CSV)</strong> file that contains - details about your samples. The CSV file should have the following fields: - <dl> - <dt>Name</dt> - <dd>The primary name/identifier for the sample/individual.</dd> - - <dt>Name2</dt> - <dd>A secondary name for the sample. This can simply be the same as - <strong>Name</strong> above. This field <strong>MUST</strong> contain a - value.</dd> - - <dt>Symbol</dt> - <dd>A symbol for the sample. This can be a strain name, e.g. 'BXD60' for - species that have strains. This field can be left empty for species like - Humans that do not have strains..</dd> - - <dt>Alias</dt> - <dd>An alias for the sample. Can be an empty field, or take on the same - value as that of the Symbol.</dd> - </dl> - </p> -</div> -{%endblock%} - -{%block javascript%} -<script src="/static/js/upload_samples.js" type="text/javascript"></script> -{%endblock%} diff --git a/uploader/templates/samples/sui-upload-success.html b/uploader/templates/samples/sui-upload-success.html deleted file mode 100644 index 054bde8..0000000 --- a/uploader/templates/samples/sui-upload-success.html +++ /dev/null @@ -1,31 +0,0 @@ -{%extends "samples/sui-base.html"%} -{%from "cli-output.html" import cli_output%} - -{%block title%}Job Status{%endblock%} - -{%block contents%} - -<div class="row" style="overflow-x: clip;"> - <h2 class="heading">{{job.job_name[0:50]}}…</h2> - - <p> - <strong>status</strong>: - <span>{{job["status"]}} ({{job.get("message", "-")}})</span><br /> - </p> - - <p>Successfully uploaded the samples.</p> - <p> - <a href="{{url_for('species.populations.samples.list_samples', - species_id=species.SpeciesId, - population_id=population.Id)}}" - title="View population samples"> - View samples - </a> - </p> -</div> - -<div class="row"> - {{cli_output(job, "stdout")}} -</div> - -{%endblock%} diff --git a/uploader/templates/samples/upload-failure.html b/uploader/templates/samples/upload-failure.html index 2cf8053..75192ec 100644 --- a/uploader/templates/samples/upload-failure.html +++ b/uploader/templates/samples/upload-failure.html @@ -1,6 +1,5 @@ {%extends "base.html"%} {%from "cli-output.html" import cli_output%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block title%}Samples Upload Failure{%endblock%} @@ -31,7 +30,3 @@ {{cli_output(job, "stderr")}} </div> {%endblock%} - -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/samples/upload-progress.html b/uploader/templates/samples/upload-progress.html index 677d457..38f931b 100644 --- a/uploader/templates/samples/upload-progress.html +++ b/uploader/templates/samples/upload-progress.html @@ -1,6 +1,5 @@ {%extends "samples/base.html"%} {%from "cli-output.html" import cli_output%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block extrameta%} <meta http-equiv="refresh" content="5"> @@ -25,7 +24,3 @@ </div> {%endblock%} - -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/samples/upload-samples.html b/uploader/templates/samples/upload-samples.html index 4aa2f7f..1f665a3 100644 --- a/uploader/templates/samples/upload-samples.html +++ b/uploader/templates/samples/upload-samples.html @@ -1,21 +1,16 @@ {%extends "samples/base.html"%} {%from "flash_messages.html" import flash_all_messages%} -{%from "populations/macro-select-population.html" import select_population_form%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block title%}Samples — Upload Samples{%endblock%} -{%block pagetitle%}Samples — Upload Samples{%endblock%} - -{%block lvl4_breadcrumbs%} -<li {%if activelink=="uploade-samples"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> <a href="{{url_for('species.populations.samples.upload_samples', - species_id=species.SpeciesId, - population_id=population.Id)}}">List</a> + species_id=species['SpeciesId'], + population_id=population.Id)}}"> + Upload + </a> </li> {%endblock%} @@ -23,35 +18,6 @@ {{flash_all_messages()}} <div class="row"> - <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> - <dd>The primary name/identifier for the sample/individual.</dd> - - <dt>Name2</dt> - <dd>A secondary name for the sample. This can simply be the same as - <strong>Name</strong> above. This field <strong>MUST</strong> contain a - value.</dd> - - <dt>Symbol</dt> - <dd>A symbol for the sample. This can be a strain name, e.g. 'BXD60' for - species that have strains. This field can be left empty for species like - Humans that do not have strains..</dd> - - <dt>Alias</dt> - <dd>An alias for the sample. Can be an empty field, or take on the same - value as that of the Symbol.</dd> - </dl> - </p> -</div> - -<div class="row"> <form id="form-samples" method="POST" action="{{url_for('species.populations.samples.upload_samples', @@ -68,6 +34,9 @@ <input type="file" name="samples_file" id="file-samples" accept="text/csv, text/tab-separated-values, text/plain" class="form-control" /> + <small class="form-text text-muted"> + See the <a href="#docs-samples-upload">documentation below</a> for + details on expected file format.</small> </div> <div class="form-group"> @@ -149,10 +118,34 @@ </tbody> </table> </div> -{%endblock%} -{%block sidebarcontents%} -{{display_population_card(species, population)}} + + +<div class="row" id="docs-samples-upload"> + <h3 class="subheading">File Format</h3> + <p> + Upload a <strong>character-separated value (CSV)</strong> file that contains + details about your samples. The CSV file should have the following fields: + <dl> + <dt>Name</dt> + <dd>The primary name/identifier for the sample/individual.</dd> + + <dt>Name2</dt> + <dd>A secondary name for the sample. This can simply be the same as + <strong>Name</strong> above. This field <strong>MUST</strong> contain a + value.</dd> + + <dt>Symbol</dt> + <dd>A symbol for the sample. This can be a strain name, e.g. 'BXD60' for + species that have strains. This field can be left empty for species like + Humans that do not have strains..</dd> + + <dt>Alias</dt> + <dd>An alias for the sample. Can be an empty field, or take on the same + value as that of the Symbol.</dd> + </dl> + </p> +</div> {%endblock%} {%block javascript%} diff --git a/uploader/templates/samples/upload-success.html b/uploader/templates/samples/upload-success.html index 881d466..d6318e9 100644 --- a/uploader/templates/samples/upload-success.html +++ b/uploader/templates/samples/upload-success.html @@ -1,6 +1,5 @@ {%extends "samples/base.html"%} {%from "cli-output.html" import cli_output%} -{%from "populations/macro-display-population-card.html" import display_population_card%} {%block title%}Job Status{%endblock%} @@ -30,7 +29,3 @@ </div> {%endblock%} - -{%block sidebarcontents%} -{{display_population_card(species, population)}} -{%endblock%} diff --git a/uploader/templates/species/base.html b/uploader/templates/species/base.html index f64f72b..a7c1a8f 100644 --- a/uploader/templates/species/base.html +++ b/uploader/templates/species/base.html @@ -1,17 +1,10 @@ {%extends "base.html"%} -{%block lvl1_breadcrumbs%} -<li {%if activelink=="species"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - {%if species is mapping%} - <a href="{{url_for('species.view_species', species_id=species.SpeciesId)}}"> - {{species.Name}}</a> - {%else%} - <a href="{{url_for('species.list_species')}}">Species</a> - {%endif%} +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('species.view_species', species_id=species['SpeciesId'])}}"> + {{species["Name"]|title}} + </a> </li> -{%block lvl2_breadcrumbs%}{%endblock%} {%endblock%} diff --git a/uploader/templates/species/sui-base.html b/uploader/templates/species/sui-base.html deleted file mode 100644 index f7b4fef..0000000 --- a/uploader/templates/species/sui-base.html +++ /dev/null @@ -1,10 +0,0 @@ -{%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 deleted file mode 100644 index 4b6402e..0000000 --- a/uploader/templates/species/sui-view-species.html +++ /dev/null @@ -1,127 +0,0 @@ -{%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/species/view-species.html b/uploader/templates/species/view-species.html index 2d02f7e..81608fc 100644 --- a/uploader/templates/species/view-species.html +++ b/uploader/templates/species/view-species.html @@ -1,90 +1,127 @@ {%extends "species/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%} -{%block pagetitle%}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.populations.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 lvl2_breadcrumbs%} -<li {%if activelink=="view-species"%} - class="breadcrumb-item active" - {%else%} - class="breadcrumb-item" - {%endif%}> - <a href="{{url_for('species.view_species', species_id=species.SpeciesId)}}">View</a> -</li> -{%endblock%} {%block contents%} -{{flash_all_messages()}} <div class="row"> - <h2>Details on species {{species.FullName}}</h2> + <h2 class="heading">{{species.FullName}} ({{species.Name}})</h2> +</div> - <dl> - <dt>Common Name</dt> - <dd>{{species.SpeciesName}}</dd> +<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> - <dt>Scientific Name</dt> - <dd>{{species.FullName}}</dd> +<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> - <dt>Taxonomy ID</dt> - <dd>{{species.TaxonomyId}}</dd> - </dl> + <form method="GET" + action="{{url_for('species.view_species', species_id=species.SpeciesId)}}" + class="form-horizontal"> + {{add_http_feature_flags()}} + {{add_form_buttons()}} - <h3>Actions</h3> + {%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> - <p> - You can proceed to perform any of the following actions for species - {{species.FullName}} - </p> + <tbody></tbody> + </table> + </div> - <ol> - <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="{{url_for('species.platforms.list_platforms', - species_id=species.SpeciesId)}}" - title="Create/Edit sequencing platforms for {{species.FullName}}"> - Manage sequencing platforms</a> - </li> - </ol> + {%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="card"> - <div class="card-body"> - <h5 class="card-title">Species Extras</h5> - <div class="card-text"> - <p>Some extra internal-use details (mostly for UI concerns on GeneNetwork)</p> - <p> - <small> - If you do not understand what the following are about, simply ignore them - — - They have no bearing whatsoever on your data, or its analysis. - </small> - </p> - <dl> - <dt>Family</dt> - <dd>{{species.Family}}</dd> +<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%} - <dt>FamilyOrderId</dt> - <dd>{{species.FamilyOrderId}}</dd> - <dt>OrderId</dt> - <dd>{{species.OrderId}}</dd> - </dl> - </div> - <a href="{{url_for('species.edit_species_extra', - species_id=species.SpeciesId)}}" - class="card-link" - title="Edit the species' internal-use details.">Edit</a> - </div> -</div> +{%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 deleted file mode 100644 index 719a646..0000000 --- a/uploader/templates/sui-base.html +++ /dev/null @@ -1,103 +0,0 @@ -<!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 deleted file mode 100644 index b93bf40..0000000 --- a/uploader/templates/sui-index.html +++ /dev/null @@ -1,161 +0,0 @@ -{%extends "sui-base.html"%} -{%from "flash_messages.html" import flash_all_messages%} -{%from "macro-forms.html" import add_http_feature_flags%} -{%from "macro-step-indicator.html" import step_indicator%} - -{%block title%}Home{%endblock%} - -{%block pagetitle%}Home{%endblock%} - -{%block extra_breadcrumbs%}{%endblock%} - -{%block contents%} - -{%macro add_form_buttons()%} -<div class="row form-buttons"> - <div class="col"> - <input type="submit" - class="btn btn-primary" - value="use selected species" /> - </div> - <div class="col"> - <a href="{{url_for('species.create_species', return_to='base.index')}}" - class="btn btn-outline-primary" - title="Create a new species.">Create a new Species</a> - </div> -</div> -{%endmacro%} - -<div class="row">{{flash_all_messages()}}</div> - -{%if user_logged_in()%} - -<div class="row"> - <ul class="nav nav-tabs" id="index-actions"> - <li class="nav-item presentation"> - <button class="nav-link active" - id="upload-data-tab" - data-bs-toggle="tab" - data-bs-target="#upload-data-content" - type="button" - role="tab" - aria-controls="upload-data-content" - aria-selected="false">Upload Data</button></li> - <li class="nav-item presentation"> - <button class="nav-link" - id="publications-tab" - data-bs-toggle="tab" - data-bs-target="#publications-content" - type="button" - role="tab" - aria-controls="publications-content" - aria-selected="true">Publications</button></li> - </ul> -</div> - -<div class="row"> - <div class="tab-content" id="upload-data-tabs-content"> - <div class="tab-pane fade show active" - id="upload-data-content" - role="tabpanel" - aria-labelledby="upload-data-content-tab"> - <h2 class="heading">Species</h2> - - <p>Select the species you want to work with.</p> - - <form method="GET" action="{{url_for('base.index')}}" class="form-horizontal"> - {{add_http_feature_flags()}} - - {{add_form_buttons()}} - - {%if species | length != 0%} - <div style="margin-top:1em;"> - <table id="tbl-select-species" class="table compact stripe" - data-species-list='{{species | tojson}}'> - <thead> - <tr> - <th></th> - <th>Species Name</th> - </tr> - </thead> - - <tbody></tbody> - </table> - </div> - - {%else%} - - <label class="control-label" for="rdo-cant-find-species"> - <input id="rdo-cant-find-species" type="radio" name="species_id" - value="CREATE-SPECIES" /> - There are no species to select from. Create the first one.</label> - - <div class="col-sm-offset-10 col-sm-2"> - <input type="submit" - class="btn btn-primary col-sm-offset-1" - value="continue" /> - </div> - - {%endif%} - - {{add_form_buttons()}} - - </form> - </div> - - <div class="tab-pane fade" - id="publications-content" - role="tabpanel" - aria-labelledby="publications-content-tab"> - <p>View, edit and delete existing publications, and add new - publications by clicking on the button below.</p> - - <a href="{{url_for('publications.index')}}" - title="Manage publications." - class="btn btn-primary">manage publications</a> - </div> - </div> -</div> - - {%else%} - - <div class="row"> - <p>The Genenetwork Uploader (<em>gn-uploader</em>) enables upload of new data - into the Genenetwork System. It provides Quality Control over data, and - guidance in case you data does not meet the standards for acceptance.</p> - <p> - <a href="{{authserver_authorise_uri()}}" - title="Sign in to the system" - class="btn btn-primary">Sign in</a> - to get started.</p> - </div> - {%endif%} - - {%endblock%} - - - - {%block sidebarcontents%} - <div class="row"> - <p>The data in Genenetwork is related to one species or another. Use the form - provided to select from existing species, or click on the - "Create a New Species" button if you cannot find the species you want to - work with.</p> - </div> - <div class="row"> - <form id="frm-quick-navigation"> - <legend>Quick Navigation</legend> - <div class="form-group"> - <label for="fqn-species-id">Species</label> - <select name="species_id"> - <option value="">Select species</option> - </select> - </div> - </form> - </div> - {%endblock%} - - - {%block javascript%} - <script type="text/javascript" src="/static/js/species.js"></script> - {%endblock%} diff --git a/uploader/templates/sui-login.html b/uploader/templates/sui-login.html deleted file mode 120000 index 733bbd3..0000000 --- a/uploader/templates/sui-login.html +++ /dev/null @@ -1 +0,0 @@ -sui-index.html \ No newline at end of file diff --git a/uploader/ui.py b/uploader/ui.py index 1994056..41791c7 100644 --- a/uploader/ui.py +++ b/uploader/ui.py @@ -1,5 +1,5 @@ """Utilities to handle the UI""" -from flask import render_template as flask_render_template +from uploader.flask_extensions import render_template as flask_render_template def make_template_renderer(default): """Render template for species.""" |
