about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2024-10-07 13:32:01 -0500
committerFrederick Muriuki Muriithi2024-10-07 13:42:22 -0500
commit4c0186d281ff77b28fa1abe1f84da0e8cb72dea1 (patch)
tree1900f94939deaf3ef49b2122a576bb45e632c231
parent40ae605d358440212a2617d1ec0dddb5f75df5bb (diff)
downloadgn-uploader-4c0186d281ff77b28fa1abe1f84da0e8cb72dea1.tar.gz
Create new phenotype dataset (PublishFreeze).
Provide the UI and code to create a new phenotype dataset.
-rw-r--r--uploader/input_validation.py21
-rw-r--r--uploader/phenotypes/models.py28
-rw-r--r--uploader/phenotypes/views.py54
-rw-r--r--uploader/static/css/styles.css6
-rw-r--r--uploader/templates/phenotypes/create-dataset.html106
-rw-r--r--uploader/templates/phenotypes/list-datasets.html6
6 files changed, 219 insertions, 2 deletions
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(
+    "<int:species_id>/populations/<int:population_id>/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%}
+<li {%if activelink=="create-dataset"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.populations.phenotypes.create_dataset',
+           species_id=species.SpeciesId,
+           population_id=population.Id)}}">Create Datasets</a>
+</li>
+{%endblock%}
+
+{%block contents%}
+{{flash_all_messages()}}
+
+<div class="row">
+  <p>Create a new phenotype dataset.</p>
+</div>
+
+<div class="row">
+  <form id="frm-create-pheno-dataset"
+        action="{{url_for('species.populations.phenotypes.create_dataset',
+                species_id=species.SpeciesId,
+                population_id=population.Id)}}"
+        method="POST">
+
+    <div class="form-group">
+      <label class="form-label" for="txt-dataset-name">Name</label>
+      {%if errors["dataset-name"] is defined%}
+      <small class="form-text text-muted danger">
+        <p>{{errors["dataset-name"]}}</p></small>
+      {%endif%}
+      <input type="text"
+             name="dataset-name"
+             id="txt-dataset-name"
+             value="{{original_formdata.get('dataset-name') or (population.InbredSetCode + 'Publish')}}"
+             {%if errors["dataset-name"] is defined%}
+             class="form-control danger"
+             {%else%}
+             class="form-control"
+             {%endif%}
+             required="required" />
+      <small class="form-text text-muted">
+        <p>A short representative name for the dataset.</p>
+        <p>Recommended: Use the population code and append "Publish" at the end.
+          <br />This field will only accept names composed of
+          letters ('A-Za-z'), numbers (0-9), hyphens and underscores.</p>
+      </small>
+    </div>
+
+    <div class="form-group">
+      <label class="form-label" for="txt-dataset-fullname">Full Name</label>
+      {%if errors["dataset-fullname"] is defined%}
+      <small class="form-text text-muted danger">
+        <p>{{errors["dataset-fullname"]}}</p></small>
+      {%endif%}
+      <input id="txt-dataset-fullname"
+             name="dataset-fullname"
+             type="text"
+             value="{{original_formdata.get('dataset-fullname', '')}}"
+             {%if errors["dataset-fullname"] is defined%}
+             class="form-control danger"
+             {%else%}
+             class="form-control"
+             {%endif%}
+             required="required" />
+      <small class="form-text text-muted">
+        <p>A longer, descriptive name for the dataset &mdash; useful for humans.
+      </p></small>
+    </div>
+
+    <div class="form-group">
+      <label class="form-label" for="txt-dataset-shortname">Short Name</label>
+      <input id="txt-dataset-shortname"
+             name="dataset-shortname"
+             type="text"
+             class="form-control"
+             value="{{original_formdata.get('dataset-shortname') or (population.InbredSetCode + ' Publish')}}" />
+      <small class="form-text text-muted">
+        <p>An optional, short name for the dataset. <br />
+          If this is not provided, it will default to the value provided for the
+          <strong>Name</strong> field above.</p></small>
+    </div>
+
+    <div class="form-group">
+      <input type="submit"
+             class="btn btn-primary"
+             value="create phenotype dataset"  />
+    </div>
+
+  </form>
+</div>
+{%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 @@
   <p class="text-warning">
     <span class="glyphicon glyphicon-exclamation-sign"></span>
     There is no dataset for this population!</p>
-  <p><a href="#"
-        class="not-implemented btn btn-primary"
+  <p><a href="{{url_for('species.populations.phenotypes.create_dataset',
+              species_id=species.SpeciesId,
+              population_id=population.Id)}}"
+        class="btn btn-primary"
         title="Create a new phenotype dataset.">create dataset</a></p>
   {%endif%}
 </div>