From 4c0186d281ff77b28fa1abe1f84da0e8cb72dea1 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 7 Oct 2024 13:32:01 -0500 Subject: Create new phenotype dataset (PublishFreeze). Provide the UI and code to create a new phenotype dataset. --- uploader/input_validation.py | 21 +++++ uploader/phenotypes/models.py | 28 ++++++ uploader/phenotypes/views.py | 54 +++++++++++ uploader/static/css/styles.css | 6 ++ uploader/templates/phenotypes/create-dataset.html | 106 ++++++++++++++++++++++ uploader/templates/phenotypes/list-datasets.html | 6 +- 6 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 uploader/templates/phenotypes/create-dataset.html diff --git a/uploader/input_validation.py b/uploader/input_validation.py index 88ffd8c..b84fca6 100644 --- a/uploader/input_validation.py +++ b/uploader/input_validation.py @@ -1,6 +1,11 @@ """Input validation utilities""" +import re +import json +import base64 from typing import Any +from flask import request + def is_empty_string(value: str) -> bool: """Check whether as string is empty""" return (isinstance(value, str) and value.strip() == "") @@ -50,3 +55,19 @@ def is_valid_representative_name(repr_name: str) -> bool: """ pattern = re.compile(r"^[a-zA-Z]+[a-zA-Z0-9_-]*[a-zA-Z0-9]$") return bool(pattern.match(repr_name)) + + +def encode_errors(errors: tuple[tuple[str, str], ...], form) -> str: + """Encode form errors into base64 string.""" + return base64.b64encode( + json.dumps({ + "errors": dict(errors), + "original_formdata": dict(form) + }).encode("utf8")) + + +def decode_errors(errorstr) -> dict[str, dict]: + """Decode errors from base64 string""" + if not bool(errorstr): + return {"errors": {}, "original_formdata": {}} + return json.loads(base64.b64decode(errorstr.encode("utf8")).decode("utf8")) diff --git a/uploader/phenotypes/models.py b/uploader/phenotypes/models.py index be970ac..9324601 100644 --- a/uploader/phenotypes/models.py +++ b/uploader/phenotypes/models.py @@ -1,6 +1,7 @@ """Database and utility functions for phenotypes.""" from typing import Optional from functools import reduce +from datetime import datetime import MySQLdb as mdb from MySQLdb.cursors import Cursor, DictCursor @@ -202,3 +203,30 @@ def phenotypes_data(conn: mdb.Connection, cursor.execute(_query, (population_id, dataset_id)) debug_query(cursor) return tuple(dict(row) for row in cursor.fetchall()) + + +def save_new_dataset(cursor: Cursor, + population_id: int, + dataset_name: str, + dataset_fullname: str, + dataset_shortname: str) -> dict: + """Create a new phenotype dataset.""" + params = { + "population_id": population_id, + "dataset_name": dataset_name, + "dataset_fullname": dataset_fullname, + "dataset_shortname": dataset_shortname, + "created": datetime.now().date().isoformat(), + "public": 2, + "confidentiality": 0, + "users": None + } + cursor.execute( + "INSERT INTO PublishFreeze(Name, FullName, ShortName, CreateTime, " + "public, InbredSetId, confidentiality, AuthorisedUsers) " + "VALUES(%(dataset_name)s, %(dataset_fullname)s, %(dataset_shortname)s, " + "%(created)s, %(public)s, %(population_id)s, %(confidentiality)s, " + "%(users)s)", + params) + debug_query(cursor) + return {**params, "Id": cursor.lastrowid} diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py index 47fbd51..c7bc965 100644 --- a/uploader/phenotypes/views.py +++ b/uploader/phenotypes/views.py @@ -1,6 +1,7 @@ """Views handling ('classical') phenotypes.""" from functools import wraps +from MySQLdb.cursors import DictCursor from flask import (flash, request, url_for, @@ -18,10 +19,14 @@ from uploader.request_checks import with_species, with_population from uploader.datautils import safe_int, order_by_family, enumerate_sequence from uploader.population.models import (populations_by_species, population_by_species_and_id) +from uploader.input_validation import (encode_errors, + decode_errors, + is_valid_representative_name) from .models import (dataset_by_id, phenotype_by_id, phenotypes_count, + save_new_dataset, dataset_phenotypes, datasets_by_population) @@ -222,3 +227,52 @@ def view_phenotype(# pylint: disable=[unused-argument] make_either_error_handler( "There was an error fetching the roles and privileges."), lambda resp: resp) + + +@phenotypesbp.route( + "/populations//phenotypes/datasets/create", + methods=["GET", "POST"]) +@require_login +@with_population( + species_redirect_uri="species.populations.phenotypes.index", + redirect_uri="species.populations.phenotypes.select_population") +def create_dataset(species: dict, population: dict, **kwargs): + """Create a new phenotype dataset.""" + with (database_connection(app.config["SQL_URI"]) as conn, + conn.cursor(cursorclass=DictCursor) as cursor): + if request.method == "GET": + return render_template("phenotypes/create-dataset.html", + activelink="create-dataset", + species=species, + population=population, + **decode_errors( + request.args.get("error_values", ""))) + + form = request.form + _errors = tuple() + if not is_valid_representative_name( + (form.get("dataset-name") or "").strip()): + _errors = _errors + (("dataset-name", "Invalid dataset name."),) + + if not bool((form.get("dataset-fullname") or "").strip()): + _errors = _errors + (("dataset-fullname", + "You must provide a value for 'Full Name'."),) + + if bool(_errors) > 0: + return redirect(url_for( + "species.populations.phenotypes.create_dataset", + species_id=species["SpeciesId"], + population_id=population["Id"], + error_values=encode_errors(_errors, form))) + + dataset_shortname = ( + form["dataset-shortname"] or form["dataset-name"]).strip() + pheno_dataset = save_new_dataset( + cursor, + population["Id"], + form["dataset-name"].strip(), + form["dataset-fullname"].strip(), + dataset_shortname) + return redirect(url_for("species.populations.phenotypes.list_datasets", + species_id=species["SpeciesId"], + population_id=population["Id"])) diff --git a/uploader/static/css/styles.css b/uploader/static/css/styles.css index 574f53e..0e9a029 100644 --- a/uploader/static/css/styles.css +++ b/uploader/static/css/styles.css @@ -125,3 +125,9 @@ input[type="submit"], .btn { border-color: #AAAAAA; background-color: #EFEFEF; } + +.danger { + color: #A94442; + border-color: #DCA7A7; + background-color: #F2DEDE; +} diff --git a/uploader/templates/phenotypes/create-dataset.html b/uploader/templates/phenotypes/create-dataset.html new file mode 100644 index 0000000..93de92f --- /dev/null +++ b/uploader/templates/phenotypes/create-dataset.html @@ -0,0 +1,106 @@ +{%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%} + +{%block pagetitle%}Phenotypes{%endblock%} + +{%block lvl4_breadcrumbs%} + +{%endblock%} + +{%block contents%} +{{flash_all_messages()}} + +
+

Create a new phenotype dataset.

+
+ +
+
+ +
+ + {%if errors["dataset-name"] is defined%} + +

{{errors["dataset-name"]}}

+ {%endif%} + + +

A short representative name for the dataset.

+

Recommended: Use the population code and append "Publish" at the end. +
This field will only accept names composed of + letters ('A-Za-z'), numbers (0-9), hyphens and underscores.

+
+
+ +
+ + {%if errors["dataset-fullname"] is defined%} + +

{{errors["dataset-fullname"]}}

+ {%endif%} + + +

A longer, descriptive name for the dataset — useful for humans. +

+
+ +
+ + + +

An optional, short name for the dataset.
+ If this is not provided, it will default to the value provided for the + Name field above.

+
+ +
+ +
+ +
+
+{%endblock%} + +{%block sidebarcontents%} +{{display_population_card(species, population)}} +{%endblock%} diff --git a/uploader/templates/phenotypes/list-datasets.html b/uploader/templates/phenotypes/list-datasets.html index 360fd2c..2eaf43a 100644 --- a/uploader/templates/phenotypes/list-datasets.html +++ b/uploader/templates/phenotypes/list-datasets.html @@ -51,8 +51,10 @@

There is no dataset for this population!

-

create dataset

{%endif%} -- cgit v1.2.3