diff options
Diffstat (limited to 'uploader')
23 files changed, 893 insertions, 169 deletions
diff --git a/uploader/__init__.py b/uploader/__init__.py index 7425b38..0ba1f81 100644 --- a/uploader/__init__.py +++ b/uploader/__init__.py @@ -11,7 +11,7 @@ from cachelib import FileSystemCache from gn_libs import jobs as gnlibs_jobs -from flask_session import Session +from flask_session import Session# type: ignore[attr-defined] from uploader.oauth2.client import user_logged_in, authserver_authorise_uri @@ -103,7 +103,7 @@ def create_app(config: Optional[dict] = None): ### END: Application configuration app.config["SESSION_CACHELIB"] = FileSystemCache( - cache_dir=Path(app.config["SESSION_FILESYSTEM_CACHE_PATH"]).absolute(), + cache_dir=str(Path(app.config["SESSION_FILESYSTEM_CACHE_PATH"]).absolute()), threshold=int(app.config["SESSION_FILESYSTEM_CACHE_THRESHOLD"]), default_timeout=int(app.config["SESSION_FILESYSTEM_CACHE_TIMEOUT"])) diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py index 4aded1d..4e1cd13 100644 --- a/uploader/background_jobs.py +++ b/uploader/background_jobs.py @@ -4,9 +4,9 @@ import importlib from typing import Callable from functools import partial +from werkzeug.wrappers.response import Response from flask import ( redirect, - Response, Blueprint, render_template, current_app as app) @@ -48,7 +48,7 @@ def register_handlers( return job_type -def register_job_handlers(job: str): +def register_job_handlers(job: dict): """Related to register handlers above.""" def __load_handler__(absolute_function_path): _parts = absolute_function_path.split(".") @@ -79,8 +79,12 @@ def handler(job: dict, handler_type: str) -> HandlerType: ).get(handler_type) if bool(_handler): return _handler(job) - return render_template(sui_template("background-jobs/default-success-page.html"), - job=job) + + def __default_success_handler__(_job): + return render_template( + sui_template("background-jobs/default-success-page.html"), job=_job) + + return __default_success_handler__ error_handler = partial(handler, handler_type="error") diff --git a/uploader/default_settings.py b/uploader/default_settings.py index bb3a967..52cdad5 100644 --- a/uploader/default_settings.py +++ b/uploader/default_settings.py @@ -32,4 +32,4 @@ JWKS_DELETION_AGE_DAYS = 14 # Days (from creation) to keep a JWK around before d ## --- Feature flags --- -FEATURE_FLAGS_HTTP = [] +FEATURE_FLAGS_HTTP: list[str] = [] diff --git a/uploader/jobs.py b/uploader/jobs.py index 5968c03..b2de54b 100644 --- a/uploader/jobs.py +++ b/uploader/jobs.py @@ -147,8 +147,8 @@ def job_errors( return take( ( json.loads(error) - for key in rconn.keys(f"{prefix}:{str(job_id)}:*:errors:*") - for error in rconn.lrange(key, 0, -1)), + for key in rconn.keys(f"{prefix}:{str(job_id)}:*:errors:*")# type: ignore[union-attr] + for error in rconn.lrange(key, 0, -1)),# type: ignore[union-attr] count) @@ -160,8 +160,8 @@ def job_files_metadata( """Get the metadata for specific job file.""" return { key.split(":")[-1]: { - **rconn.hgetall(key), + **rconn.hgetall(key),# type: ignore[dict-item] "filetype": key.split(":")[-3] } - for key in rconn.keys(f"{prefix}:{str(job_id)}:*:metadata*") + for key in rconn.keys(f"{prefix}:{str(job_id)}:*:metadata*")# type: ignore[union-attr] } diff --git a/uploader/oauth2/client.py b/uploader/oauth2/client.py index b94a044..4e81afd 100644 --- a/uploader/oauth2/client.py +++ b/uploader/oauth2/client.py @@ -157,7 +157,10 @@ def fetch_user_details() -> Either: "user_id": uuid.UUID(usrdets["user_id"]), "name": usrdets["name"], "email": usrdets["email"], - "token": session.user_token()})) + "token": session.user_token(), + "logged_in": session.user_token().either( + lambda _e: False, lambda _t: True) + })) return udets return Right(suser) diff --git a/uploader/phenotypes/misc.py b/uploader/phenotypes/misc.py index cbe3b7f..1924c07 100644 --- a/uploader/phenotypes/misc.py +++ b/uploader/phenotypes/misc.py @@ -8,7 +8,7 @@ def phenotypes_data_differences( filedata: tuple[dict, ...], dbdata: tuple[dict, ...] ) -> tuple[dict, ...]: """Compute differences between file data and db data""" - diff = tuple() + diff: tuple[dict, ...] = tuple() for filerow, dbrow in zip( sorted(filedata, key=lambda item: (item["phenotype_id"], item["xref_id"])), sorted(dbdata, key=lambda item: (item["PhenotypeId"], item["xref_id"]))): diff --git a/uploader/phenotypes/models.py b/uploader/phenotypes/models.py index e962e62..a22497c 100644 --- a/uploader/phenotypes/models.py +++ b/uploader/phenotypes/models.py @@ -255,9 +255,9 @@ def phenotypes_vector_data(# pylint: disable=[too-many-arguments, too-many-posit xref_ids: tuple[int, ...] = tuple(), offset: int = 0, limit: Optional[int] = None -) -> dict[tuple[int, int, int]: dict[str, Union[int,float]]]: +) -> dict[tuple[int, int, int], dict[str, Union[int,float]]]: """Retrieve the vector data values for traits in the database.""" - _params = (species_id, population_id) + _params: tuple[int, ...] = (species_id, population_id) _query = ("SELECT " "Species.Id AS SpeciesId, iset.Id AS InbredSetId, " "pxr.Id AS xref_id, pdata.*, Strain.Id AS StrainId, " @@ -328,35 +328,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) @@ -381,7 +352,7 @@ def create_new_phenotypes(# pylint: disable=[too-many-locals] phenotypes: Iterable[dict] ) -> tuple[dict, ...]: """Add entirely new phenotypes to the database. WARNING: Not thread-safe.""" - _phenos = tuple() + _phenos: tuple[dict, ...] = tuple() with conn.cursor(cursorclass=DictCursor) as cursor: def make_next_id(idcol, table): cursor.execute(f"SELECT MAX({idcol}) AS last_id FROM {table}") @@ -430,9 +401,10 @@ def create_new_phenotypes(# pylint: disable=[too-many-locals] if len(batch) == 0: break - params, abbrevs = reduce(__build_params_and_prepubabbrevs__, - batch, - (tuple(), tuple())) + params, abbrevs = reduce(#type: ignore[var-annotated] + __build_params_and_prepubabbrevs__, + batch, + (tuple(), tuple())) # Check for uniqueness for all "Pre_publication_description" values abbrevs_paramsstr = ", ".join(["%s"] * len(abbrevs)) _query = ("SELECT PublishXRef.PhenotypeId, Phenotype.* " diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py index 9df7d81..42f2e34 100644 --- a/uploader/phenotypes/views.py +++ b/uploader/phenotypes/views.py @@ -805,7 +805,7 @@ def update_phenotype_data(conn, data: dict): } }) - values, serrs, counts = tuple( + values, serrs, counts = tuple(# type: ignore[var-annotated] tuple({ "data_id": row[0].split("::")[0], "strain_id": row[0].split("::")[1], diff --git a/uploader/population/views.py b/uploader/population/views.py index a6e2358..caee55b 100644 --- a/uploader/population/views.py +++ b/uploader/population/views.py @@ -157,7 +157,7 @@ def create_population(species_id: int): "FullName": population_fullname, "InbredSetCode": request.form.get("population_code") or None, "Description": request.form.get("population_description") or None, - "Family": request.form.get("population_family").strip() or None, + "Family": request.form.get("population_family", "").strip() or None, "MappingMethodId": request.form.get("population_mapping_method_id"), "GeneticType": request.form.get("population_genetic_type") or None }) diff --git a/uploader/publications/datatables.py b/uploader/publications/datatables.py index e07fafd..8b3d4a0 100644 --- a/uploader/publications/datatables.py +++ b/uploader/publications/datatables.py @@ -13,7 +13,7 @@ def fetch_publications( search: Optional[str] = None, offset: int = 0, limit: int = -1 -) -> tuple[dict, int, int, int]: +) -> tuple[tuple[dict, ...], int, int, int]: """Fetch publications from the database.""" _query = "SELECT * FROM Publication" _count_query = "SELECT COUNT(*) FROM Publication" diff --git a/uploader/publications/misc.py b/uploader/publications/misc.py index fca6f71..f0ff9c7 100644 --- a/uploader/publications/misc.py +++ b/uploader/publications/misc.py @@ -4,10 +4,10 @@ def publications_differences( filedata: tuple[dict, ...], dbdata: tuple[dict, ...], - pubmedid2pubidmap: tuple[dict, ...] + pubmedid2pubidmap: dict[int, int] ) -> tuple[dict, ...]: """Compute the differences between file data and db data""" - diff = tuple() + diff: tuple[dict, ...] = tuple() for filerow, dbrow in zip( sorted(filedata, key=lambda item: ( item["phenotype_id"], item["xref_id"])), diff --git a/uploader/publications/pubmed.py b/uploader/publications/pubmed.py index 2531c4a..15bf701 100644 --- a/uploader/publications/pubmed.py +++ b/uploader/publications/pubmed.py @@ -1,5 +1,6 @@ """Module to interact with NCBI's PubMed""" import logging +from typing import Optional import requests from lxml import etree @@ -40,7 +41,7 @@ def __pages__(pagination: etree.Element) -> str: )) if start is not None else "" -def __abstract__(article: etree.Element) -> str: +def __abstract__(article: etree.Element) -> Optional[str]: abstract = article.find("Abstract/AbstractText") return abstract.text if abstract is not None else None diff --git a/uploader/publications/views.py b/uploader/publications/views.py index 4ec832f..11732db 100644 --- a/uploader/publications/views.py +++ b/uploader/publications/views.py @@ -10,6 +10,8 @@ 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 @@ -30,7 +32,7 @@ pubbp = Blueprint("publications", __name__) @require_login def index(): """Index page for publications.""" - return render_template("publications/index.html") + return render_template(sui_template("publications/index.html")) @pubbp.route("/list", methods=["GET"]) @@ -72,7 +74,7 @@ def view_publication(publication_id: int): return redirect(url_for('publications.index')) return render_template( - "publications/view-publication.html", + sui_template("publications/view-publication.html"), publication=publication, linked_phenotypes=tuple(fetch_publication_phenotypes( conn, publication_id))) @@ -82,13 +84,21 @@ def view_publication(publication_id: int): @require_login def create_publication(): """Create a new publication.""" + _get_args = { + key: request.args[key] + for key in ("species_id", "population_id", "dataset_id", "return_to") + if bool(request.args.get(key)) + } + if request.method == "GET": - return render_template("publications/create-publication.html") + return render_template( + sui_template("publications/create-publication.html"), + get_args=_get_args) form = request.form authors = form.get("publication-authors").encode("utf8") if authors is None or authors == "": flash("The publication's author(s) MUST be provided!", "alert alert-danger") - return redirect(url_for("publications.create", **request.args)) + return redirect(url_for("publications.create")) with database_connection(app.config["SQL_URI"]) as conn: publications = create_new_publications(conn, ({ @@ -106,7 +116,7 @@ def create_publication(): return redirect(url_for( request.args.get("return_to") or "publications.view_publication", publication_id=publications[0]["publication_id"], - **request.args)) + **_get_args)) flash("Publication creation failed!", "alert alert-danger") app.logger.debug("Failed to create the new publication.", exc_info=True) @@ -120,7 +130,7 @@ def edit_publication(publication_id: int): with database_connection(app.config["SQL_URI"]) as conn: if request.method == "GET": return render_template( - "publications/edit-publication.html", + sui_template("publications/edit-publication.html"), publication=fetch_publication_by_id(conn, publication_id), linked_phenotypes=tuple(fetch_publication_phenotypes( conn, publication_id)), @@ -130,14 +140,14 @@ def edit_publication(publication_id: int): _pub = update_publications(conn, ({ "publication_id": publication_id, "pubmed_id": form.get("pubmed-id") or None, - "abstract": form.get("publication-abstract").encode("utf8") or None, - "authors": form.get("publication-authors").encode("utf8"), - "title": form.get("publication-title").encode("utf8") or None, - "journal": form.get("publication-journal").encode("utf8") or None, - "volume": form.get("publication-volume").encode("utf8") or None, - "pages": form.get("publication-pages").encode("utf8") or None, + "abstract": (form.get("publication-abstract") or "").encode("utf8") or None, + "authors": (form.get("publication-authors") or "").encode("utf8"), + "title": (form.get("publication-title") or "").encode("utf8") or None, + "journal": (form.get("publication-journal") or "").encode("utf8") or None, + "volume": (form.get("publication-volume") or "").encode("utf8") or None, + "pages": (form.get("publication-pages") or "").encode("utf8") or None, "month": (form.get("publication-month") or "").encode("utf8").capitalize() or None, - "year": form.get("publication-year").encode("utf8") or None + "year": (form.get("publication-year") or "").encode("utf8") or None },)) if not _pub: @@ -171,15 +181,16 @@ def delete_publication(publication_id: int): flash("Cannot delete publication with linked phenotypes!", "alert-warning") return redirect(url_for( - "publications.view_publication", publication_id=publication_id)) + sui_template("publications.view_publication"), + publication_id=publication_id)) if request.method == "GET": return render_template( - "publications/delete-publication.html", + sui_template("publications/delete-publication.html"), publication=publication, linked_phenotypes=linked_phenotypes, publication_id=publication_id) delete_publications(conn, (publication,)) flash("Deleted the publication successfully.", "alert-success") - return render_template("publications/delete-publication-success.html") + return redirect(url_for("publications.index")) diff --git a/uploader/route_utils.py b/uploader/route_utils.py index 4449475..426d7eb 100644 --- a/uploader/route_utils.py +++ b/uploader/route_utils.py @@ -56,22 +56,24 @@ def generic_select_population( def redirect_to_next(default: dict): """Redirect to the next uri if specified, else redirect to default.""" assert "uri" in default, "You must provide at least the 'uri' value." - try: - next_page = base64_decode_to_dict(request.args.get("next")) - _uri = next_page["uri"] - next_page.pop("uri") - return redirect(url_for(_uri, **next_page)) - except (TypeError, JSONDecodeError) as _err: - logger.debug("We could not decode the next value '%s'", - next_page, - exc_info=True) + _next = request.args.get("next") or "" + if bool(_next): + try: + next_page = base64_decode_to_dict(_next) + _uri = next_page["uri"] + next_page.pop("uri") + return redirect(url_for(_uri, **next_page)) + except (TypeError, JSONDecodeError) as _err: + logger.debug("We could not decode the next value '%s'", + next_page, + exc_info=True) return redirect(url_for( default["uri"], **{key:value for key,value in default.items() if key != "uri"})) -def build_next_argument(uri: str, **kwargs) -> str: +def build_next_argument(uri: str, **kwargs) -> bytes: """Build the `next` URI argument from provided details.""" dumps_keywords = ( "skipkeys", "ensure_ascii", "check_circular", "allow_nan", "cls", diff --git a/uploader/static/css/theme.css b/uploader/static/css/theme.css index 99b7af3..bdac745 100644 --- a/uploader/static/css/theme.css +++ b/uploader/static/css/theme.css @@ -83,3 +83,8 @@ table.dataTable tbody tr.selected td { padding-bottom: 0.2em; border-bottom: solid gray 1px; } + + +.breadcrumb-item { + text-transform: Capitalize; +} diff --git a/uploader/templates/phenotypes/sui-view-dataset.html b/uploader/templates/phenotypes/sui-view-dataset.html index 6a26004..f858c4e 100644 --- a/uploader/templates/phenotypes/sui-view-dataset.html +++ b/uploader/templates/phenotypes/sui-view-dataset.html @@ -53,14 +53,6 @@ title="Add a bunch of phenotypes" class="btn btn-primary">Add phenotypes</a> </div> - - <div class="col"> - <a href="#" - title="List all existing publications for this population." - class="btn btn-primary not-implemented">view publications</a> - <!-- Maybe, actually filter publications by population? --> - <!-- Provide other features for publications on loaded page. --> - </div> <div class="col"> <form id="frm-recompute-phenotype-means" diff --git a/uploader/templates/publications/sui-base.html b/uploader/templates/publications/sui-base.html new file mode 100644 index 0000000..64e41ef --- /dev/null +++ b/uploader/templates/publications/sui-base.html @@ -0,0 +1,9 @@ +{%extends "sui-base.html"%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.index')}}" + title="Manage publications">Publications</a> +</li> +{%endblock%} diff --git a/uploader/templates/publications/sui-create-publication.html b/uploader/templates/publications/sui-create-publication.html new file mode 100644 index 0000000..81edca6 --- /dev/null +++ b/uploader/templates/publications/sui-create-publication.html @@ -0,0 +1,200 @@ +{%extends "publications/sui-base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}View Publication{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.create_publication', **get_args)}}" + title="Manage publications">create publication</a> +</li> +{%endblock%} + + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <form id="frm-create-publication" + method="POST" + action="{{url_for('publications.create_publication', **get_args)}}" + class="form-horizontal"> + + <div class="row mb-3"> + <label for="txt-pubmed-id" class="col-sm-2 col-form-label"> + PubMed ID</label> + <div class="col-sm-10"> + <div class="input-group"> + <input type="text" + id="txt-pubmed-id" + name="pubmed-id" + class="form-control"/> + <div class="input-group-text"> + <button class="btn btn-outline-primary" + id="btn-search-pubmed-id">search</button> + </div> + </div> + <span id="search-pubmed-id-error" + class="form-text text-muted text-danger visually-hidden"> + </span> + <span class="form-text text-muted">This is the publication's ID on + <a href="https://pubmed.ncbi.nlm.nih.gov/" + title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a> + </span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-title" class="col-sm-2 col-form-label"> + Title</label> + <div class="col-sm-10"> + <input type="text" + id="txt-publication-title" + name="publication-title" + class="form-control" /> + <span class="form-text text-muted">Provide the publication's title here.</span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-authors" class="col-sm-2 col-form-label"> + Authors</label> + <div class="col-sm-10"> + <input type="text" + id="txt-publication-authors" + name="publication-authors" + required="required" + class="form-control" /> + <span class="form-text text-muted"> + A publication <strong>MUST</strong> have an author. You <em>must</em> + provide a value for the authors field. + </span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-journal" class="col-sm-2 col-form-label"> + Journal</label> + <div class="col-sm-10"> + <input type="text" + id="txt-publication-journal" + name="publication-journal" + class="form-control" /> + <span class="form-text text-muted">Provide the name journal where the + publication was done, here.</span> + </div> + </div> + + <div class="row mb-3"> + <label for="select-publication-month" + class="col-sm-2 col-form-label"> + Month</label> + <div class="col-sm-4"> + <select class="form-control" + id="select-publication-month" + name="publication-month"> + <option value="">Select a month</option> + <option value="january">January</option> + <option value="february">February</option> + <option value="march">March</option> + <option value="april">April</option> + <option value="may">May</option> + <option value="june">June</option> + <option value="july">July</option> + <option value="august">August</option> + <option value="september">September</option> + <option value="october">October</option> + <option value="november">November</option> + <option value="december">December</option> + </select> + <span class="form-text text-muted">Month of publication</span> + </div> + + <label for="txt-publication-year" + class="col-sm-2 col-form-label"> + Year</label> + <div class="col-sm-4"> + <input type="number" + id="txt-publication-year" + name="publication-year" + class="form-control" + min="1960" /> + <span class="form-text text-muted">Year of publication</span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-volume" + class="col-sm-2 col-form-label"> + Volume</label> + <div class="col-sm-4"> + <input type="text" + id="txt-publication-volume" + name="publication-volume" + class="form-control"> + <span class="form-text text-muted">Journal volume</span> + </div> + + <label for="txt-publication-pages" + class="col-sm-2 col-form-label"> + Pages</label> + <div class="col-sm-4"> + <input type="text" + id="txt-publication-pages" + name="publication-pages" + class="form-control" /> + <span class="form-text text-muted">Journal pages for the publication</span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label> + <div class="col-sm-10"> + <textarea id="txt-publication-abstract" + name="publication-abstract" + class="form-control" + rows="7"></textarea> + </div> + </div> + + <div class="row"> + <div class="col"> + <input type="submit" + class="btn btn-primary" + value="create publication" /> + </div> + <div class="col"> + <input type="reset" class="btn btn-danger" value="reset form" /> + </div> + </div> + +</form> +</div> + +{%endblock%} + + +{%block javascript%} +<script type="text/javascript" src="/static/js/pubmed.js"></script> +<script type="text/javascript"> + $(function() { + $("#btn-search-pubmed-id").on("click", (event) => { + event.preventDefault(); + var search_button = event.target; + var pubmed_id = $("#txt-pubmed-id").val().trim(); + remove_class($("#txt-pubmed-id").parent(), "has-error"); + if(pubmed_id == "") { + add_class($("#txt-pubmed-id").parent(), "has-error"); + return false; + } + + search_button.disabled = true; + // Fetch publication details + fetch_publication_details(pubmed_id, + [() => {search_button.disabled = false;}]); + return false; + }); + }); +</script> +{%endblock%} diff --git a/uploader/templates/publications/sui-delete-publication.html b/uploader/templates/publications/sui-delete-publication.html new file mode 100644 index 0000000..436f2c1 --- /dev/null +++ b/uploader/templates/publications/sui-delete-publication.html @@ -0,0 +1,95 @@ +{%extends "publications/sui-base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Delete Publication{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.delete_publication', + publication_id=publication.Id)}}" + title="Manage publications">delete publication</a> +</li> +{%endblock%} + + +{%block contents%} +{{flash_all_messages()}} +<div class="row"> + <p>You are about to delete the publication with the following details:</p> +</div> + +<div class="row"> + <table class="table"> + <tr> + <th>Linked Phenotypes</th> + <td>{{linked_phenotypes | count}}</td> + </tr> + <tr> + <th>PubMed</th> + <td> + {%if publication.PubMed_ID%} + <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/" + target="_blank">{{publication.PubMed_ID}}</a> + {%else%} + — + {%endif%} + </td> + </tr> + <tr> + <th>Title</th> + <td>{{publication.Title or "—"}}</td> + </tr> + <tr> + <th>Authors</th> + <td>{{publication.Authors or "—"}}</td> + </tr> + <tr> + <th>Journal</th> + <td>{{publication.Journal or "—"}}</td> + </tr> + <tr> + <th>Published</th> + <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td> + </tr> + <tr> + <th>Volume</th> + <td>{{publication.Volume or "—"}}</td> + </tr> + <tr> + <th>Pages</th> + <td>{{publication.Pages or "—"}}</td> + </tr> + <tr> + <th>Abstract</th> + <td> + {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%} + <p>{{line}}</p> + {%endfor%} + </td> + </tr> + </table> +</div> + +<div class="row"> + <p>If you are sure that is what you want, click the button below to delete the + publication</p> + <p class="form-text text-small"> + <small>You will not be able to recover the data if you click + delete below.</small></p> + + <form action="{{url_for('publications.delete_publication', publication_id=publication_id)}}" + method="POST"> + <div class="form-group"> + <input type="submit" value="delete" class="btn btn-danger" /> + </div> + </form> +</div> +{%endblock%} + + +{%block javascript%} +<script type="text/javascript"> + $(function() {}); +</script> +{%endblock%} diff --git a/uploader/templates/publications/sui-edit-publication.html b/uploader/templates/publications/sui-edit-publication.html new file mode 100644 index 0000000..847b020 --- /dev/null +++ b/uploader/templates/publications/sui-edit-publication.html @@ -0,0 +1,203 @@ +{%extends "publications/sui-base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Edit Publication{%endblock%} + +{%block breadcrumbs%} +{{super()}} +<li class="breadcrumb-item"> + <a href="{{url_for('publications.edit_publication', + publication_id=publication.Id)}}" + title="Edit the publication's details">edit publication</a> +</li> +{%endblock%} + + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <form id="frm-create-publication" + method="POST" + action="{{url_for('publications.edit_publication', + publication_id=publication_id, + next=request.args.get('next', ''))}}" + class="form-horizontal"> + + <div class="row mb-3"> + <label for="txt-pubmed-id" class="col-sm-2 col-form-label"> + PubMed ID</label> + <div class="col-sm-10"> + <div class="input-group"> + <input type="text" + id="txt-pubmed-id" + name="pubmed-id" + value="{{publication.PubMed_ID or ''}}" + class="form-control" /> + <div class="input-group-text"> + <button class="btn btn-outline-primary" + id="btn-search-pubmed-id">search</button> + </div> + </div> + <span id="search-pubmed-id-error" + class="form-text text-muted text-danger visually-hidden"> + </span> + <span class="form-text text-muted">This is the publication's ID on + <a href="https://pubmed.ncbi.nlm.nih.gov/" + title="Link to NCBI's PubMed service">NCBI's Pubmed Service</a> + </span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-title" class="col-sm-2 col-form-label"> + Title</label> + <div class="col-sm-10"> + <input type="text" + id="txt-publication-title" + name="publication-title" + value="{{publication.Title}}" + class="form-control" /> + <span class="form-text text-muted">Provide the publication's title here.</span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-authors" class="col-sm-2 col-form-label"> + Authors</label> + <div class="col-sm-10"> + <input type="text" + id="txt-publication-authors" + name="publication-authors" + value="{{publication.Authors}}" + required="required" + class="form-control" /> + <span class="form-text text-muted"> + A publication <strong>MUST</strong> have an author. You <em>must</em> + provide a value for the authors field. + </span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-journal" class="col-sm-2 col-form-label"> + Journal</label> + <div class="col-sm-10"> + <input type="text" + id="txt-publication-journal" + name="publication-journal" + value="{{publication.Journal}}" + class="form-control" /> + <span class="form-text text-muted">Provide the name journal where the + publication was done, here.</span> + </div> + </div> + + <div class="row mb-3"> + <label for="select-publication-month" + class="col-sm-2 col-form-label"> + Month</label> + <div class="col-sm-4"> + <select class="form-control" + id="select-publication-month" + name="publication-month"> + <option value="">Select a month</option> + {%for month in ("january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"):%} + <option value="{{month}}" + {%if publication.Month | lower == month %} + selected="selected" + {%endif%}> + {{month | title}} + </option> + {%endfor%} + </select> + <span class="form-text text-muted">Month of publication</span> + </div> + + <label for="txt-publication-year" + class="col-sm-2 col-form-label"> + Year</label> + <div class="col-sm-4"> + <input type="number" + id="txt-publication-year" + name="publication-year" + value="{{publication.Year}}" + class="form-control" + min="1960" /> + <span class="form-text text-muted">Year of publication</span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-publication-volume" + class="col-sm-2 col-form-label"> + Volume</label> + <div class="col-sm-4"> + <input type="text" + id="txt-publication-volume" + name="publication-volume" + value="{{publication.Volume}}" + class="form-control"> + <span class="form-text text-muted">Journal volume</span> + </div> + + <label for="txt-publication-pages" + class="col-sm-2 col-form-label"> + Pages</label> + <div class="col-sm-4"> + <input type="text" + id="txt-publication-pages" + name="publication-pages" + value="{{publication.Pages}}" + class="form-control" /> + <span class="form-text text-muted">Journal pages for the publication</span> + </div> + </div> + + <div class="row mb-3"> + <label for="txt-abstract" class="col-sm-2 col-form-label">Abstract</label> + <div class="col-sm-10"> + <textarea id="txt-publication-abstract" + name="publication-abstract" + class="form-control" + rows="7">{{publication.Abstract or ""}}</textarea> + </div> + </div> + + <div class="row mb-3"> + <div class="col-sm-2"></div> + <div class="col-sm-8"> + <input type="submit" class="btn btn-primary" value="Save" /> + <input type="reset" class="btn btn-danger" /> + </div> + </div> + +</form> +</div> + +{%endblock%} + + +{%block javascript%} +<script type="text/javascript" src="/static/js/pubmed.js"></script> +<script type="text/javascript"> + $(function() { + $("#btn-search-pubmed-id").on("click", (event) => { + event.preventDefault(); + var search_button = event.target; + var pubmed_id = $("#txt-pubmed-id").val().trim(); + remove_class($("#txt-pubmed-id").parent(), "has-error"); + if(pubmed_id == "") { + add_class($("#txt-pubmed-id").parent(), "has-error"); + return false; + } + + search_button.disabled = true; + // Fetch publication details + fetch_publication_details(pubmed_id, + [() => {search_button.disabled = false;}]); + return false; + }); + }); +</script> +{%endblock%} diff --git a/uploader/templates/publications/sui-index.html b/uploader/templates/publications/sui-index.html new file mode 100644 index 0000000..e405dd1 --- /dev/null +++ b/uploader/templates/publications/sui-index.html @@ -0,0 +1,109 @@ +{%extends "publications/sui-base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Publications{%endblock%} + + +{%block contents%} +{{flash_all_messages()}} + +<div class="row" style="padding-bottom: 1em;"> + <div class="col"> + <a href="{{url_for('publications.create_publication')}}" + class="btn btn-primary" + title="Create a new publication."> + add new publication</a> + </div> +</div> + +<div class="row"> + <p>Click on title to view more details and to edit details for that + publication.</p> +</div> + +<div class="row"> + <table id="tbl-list-publications" class="table compact stripe"> + <thead> + <tr> + <th>#</th> + <th>PubMed ID</th> + <th>Title</th> + <th>Authors</th> + </tr> + </thead> + + <tbody></tbody> + </table> +</div> +{%endblock%} + + +{%block javascript%} +<script type="text/javascript" src="/static/js/urls.js"></script> + +<script type="text/javascript"> + $(function() { + var publicationsDataTable = buildDataTable( + "#tbl-list-publications", + [], + [ + {data: "index"}, + { + searchable: true, + data: (pub) => { + if(pub.PubMed_ID) { + return `<a href="https://pubmed.ncbi.nlm.nih.gov/` + + `${pub.PubMed_ID}/" target="_blank" ` + + `title="Link to publication on NCBI.">` + + `${pub.PubMed_ID}</a>`; + } + return ""; + } + }, + { + searchable: true, + data: (pub) => { + var title = "⸻"; + if(pub.Title) { + title = pub.Title + } + url=buildURLFromCurrentURL( + `/publications/view/${pub.Id}`); + return `<a href="${url}" target="_blank" ` + + `title="Link to view publication details">` + + `${title}</a>`; + } + }, + { + searchable: true, + data: (pub) => { + authors = pub.Authors.split(",").map( + (item) => {return item.trim();}); + if(authors.length > 1) { + return authors[0] + ", et. al."; + } + return authors[0]; + } + } + ], + { + serverSide: true, + ajax: { + url: "/publications/list", + dataSrc: "publications" + }, + scrollY: 700, + scroller: true, + scrollCollapse: true, + paging: true, + deferRender: true, + layout: { + topStart: "info", + topEnd: "search", + bottomStart: "pageLength", + bottomEnd: false + } + }); + }); +</script> +{%endblock%} diff --git a/uploader/templates/publications/sui-view-publication.html b/uploader/templates/publications/sui-view-publication.html new file mode 100644 index 0000000..740fc37 --- /dev/null +++ b/uploader/templates/publications/sui-view-publication.html @@ -0,0 +1,80 @@ +{%extends "publications/sui-base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}View Publication{%endblock%} + + +{%block contents%} +{{flash_all_messages()}} + +<div class="row"> + <table class="table"> + <tr> + <th>Linked Phenotypes</th> + <td>{{linked_phenotypes | count}}</td> + </tr> + <tr> + <th>PubMed</th> + <td> + {%if publication.PubMed_ID%} + <a href="https://pubmed.ncbi.nlm.nih.gov/{{publication.PubMed_ID}}/" + target="_blank">{{publication.PubMed_ID}}</a> + {%else%} + — + {%endif%} + </td> + </tr> + <tr> + <th>Title</th> + <td>{{publication.Title or "—"}}</td> + </tr> + <tr> + <th>Authors</th> + <td>{{publication.Authors or "—"}}</td> + </tr> + <tr> + <th>Journal</th> + <td>{{publication.Journal or "—"}}</td> + </tr> + <tr> + <th>Published</th> + <td>{{publication.Month or ""}} {{publication.Year or "—"}}</td> + </tr> + <tr> + <th>Volume</th> + <td>{{publication.Volume or "—"}}</td> + </tr> + <tr> + <th>Pages</th> + <td>{{publication.Pages or "—"}}</td> + </tr> + <tr> + <th>Abstract</th> + <td> + {%for line in (publication.Abstract or "—").replace("\r\n", "<br />").replace("\n", "<br />").split("<br />")%} + <p>{{line}}</p> + {%endfor%} + </td> + </tr> + </table> +</div> + +<div class="row"> + <div> + <a href="{{url_for('publications.edit_publication', publication_id=publication.Id)}}" + title="Edit details for this publication." + class="btn btn-primary">Edit</a> + {%if linked_phenotypes | length == 0%} + <a href="{{url_for('publications.delete_publication', publication_id=publication.Id)}}" + class="btn btn-danger">delete</a> + {%endif%} + </div> +</div> +{%endblock%} + + +{%block javascript%} +<script type="text/javascript"> + $(function() {}); +</script> +{%endblock%} diff --git a/uploader/templates/sui-index.html b/uploader/templates/sui-index.html index 888823f..b93bf40 100644 --- a/uploader/templates/sui-index.html +++ b/uploader/templates/sui-index.html @@ -13,16 +13,16 @@ {%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 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%} @@ -31,93 +31,131 @@ {%if user_logged_in()%} <div class="row"> - <div class="row"> - <h2 class="heading">Species</h2> - - <p>Select the species you want to work with.</p> - </div> + <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"> - <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> + <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> - {%else%} + <p>Select the species you want to work with.</p> - <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> + <form method="GET" action="{{url_for('base.index')}}" class="form-horizontal"> + {{add_http_feature_flags()}} - <div class="col-sm-offset-10 col-sm-2"> - <input type="submit" - class="btn btn-primary col-sm-offset-1" - value="continue" /> - </div> + {{add_form_buttons()}} - {%endif%} + {%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> - {{add_form_buttons()}} + <tbody></tbody> + </table> + </div> - </form> -</div> + {%else%} -{%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="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%} + <div class="col-sm-offset-10 col-sm-2"> + <input type="submit" + class="btn btn-primary col-sm-offset-1" + value="continue" /> + </div> -{%endblock%} + {%endif%} + {{add_form_buttons()}} + </form> + </div> -{%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 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> - </form> + </div> </div> -{%endblock%} + + {%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%} + {%block javascript%} + <script type="text/javascript" src="/static/js/species.js"></script> + {%endblock%} |
