about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--uploader/__init__.py2
-rw-r--r--uploader/dbinsert.py1
-rw-r--r--uploader/expression_data/index.py2
-rw-r--r--uploader/expression_data/rqtl2.py1
-rw-r--r--uploader/expression_data/samples.py1
-rw-r--r--uploader/species/__init__.py2
-rw-r--r--uploader/species/models.py (renamed from uploader/db/species.py)4
-rw-r--r--uploader/species/views.py91
-rw-r--r--uploader/templates/base.html10
-rw-r--r--uploader/templates/species/base.html11
-rw-r--r--uploader/templates/species/create-species.html116
-rw-r--r--uploader/templates/species/list-species.html68
12 files changed, 305 insertions, 4 deletions
diff --git a/uploader/__init__.py b/uploader/__init__.py
index 941765d..347f170 100644
--- a/uploader/__init__.py
+++ b/uploader/__init__.py
@@ -10,6 +10,7 @@ from flask_session import Session
 from uploader.oauth2.client import user_logged_in, authserver_authorise_uri
 
 from .base_routes import base
+from .species import speciesbp
 from .dbinsert import dbinsertbp
 from .oauth2.views import oauth2
 from .expression_data import exprdatabp
@@ -81,6 +82,7 @@ def create_app():
     # setup blueprints
     app.register_blueprint(base, url_prefix="/")
     app.register_blueprint(oauth2, url_prefix="/oauth2")
+    app.register_blueprint(speciesbp, url_prefix="/species")
     app.register_blueprint(dbinsertbp, url_prefix="/dbinsert")
     app.register_blueprint(exprdatabp, url_prefix="/expression-data")
 
diff --git a/uploader/dbinsert.py b/uploader/dbinsert.py
index 559dc5e..328ccc8 100644
--- a/uploader/dbinsert.py
+++ b/uploader/dbinsert.py
@@ -14,6 +14,7 @@ from flask import (
 from uploader.authorisation import require_login
 from uploader.db_utils import with_db_connection, database_connection
 from uploader.db import species, species_by_id, populations_by_species
+from uploader.species.models import species_by_id, all_species as species
 
 from . import jobs
 
diff --git a/uploader/expression_data/index.py b/uploader/expression_data/index.py
index 799989e..db23136 100644
--- a/uploader/expression_data/index.py
+++ b/uploader/expression_data/index.py
@@ -14,7 +14,7 @@ from flask import (
     render_template,
     current_app as app)
 
-from uploader.db import species
+from uploader.species.models import all_species as species
 from uploader.authorisation import require_login
 from uploader.db_utils import with_db_connection
 
diff --git a/uploader/expression_data/rqtl2.py b/uploader/expression_data/rqtl2.py
index 48df66c..975fc1f 100644
--- a/uploader/expression_data/rqtl2.py
+++ b/uploader/expression_data/rqtl2.py
@@ -41,6 +41,7 @@ from uploader.db import (
     save_population,
     populations_by_species,
     population_by_species_and_id,)
+from uploader.species.models import species_by_id
 from uploader.db.datasets import (
     geno_dataset_by_id,
     geno_datasets_by_species_and_population,
diff --git a/uploader/expression_data/samples.py b/uploader/expression_data/samples.py
index 95b9b73..0715d14 100644
--- a/uploader/expression_data/samples.py
+++ b/uploader/expression_data/samples.py
@@ -34,6 +34,7 @@ from uploader.db import (
     population_by_id,
     populations_by_species,
     species as fetch_species)
+from uploader.species.models import species_by_id, all_species as fetch_species
 
 samples = Blueprint("samples", __name__)
 
diff --git a/uploader/species/__init__.py b/uploader/species/__init__.py
new file mode 100644
index 0000000..83f2165
--- /dev/null
+++ b/uploader/species/__init__.py
@@ -0,0 +1,2 @@
+"""Package to handle creation and management of species."""
+from .views import speciesbp
diff --git a/uploader/db/species.py b/uploader/species/models.py
index 653e59b..98337a3 100644
--- a/uploader/db/species.py
+++ b/uploader/species/models.py
@@ -2,12 +2,12 @@
 import MySQLdb as mdb
 from MySQLdb.cursors import DictCursor
 
-def species(conn: mdb.Connection) -> tuple:
+def all_species(conn: mdb.Connection) -> tuple:
     "Retrieve the species from the database."
     with conn.cursor(cursorclass=DictCursor) as cursor:
         cursor.execute(
             "SELECT SpeciesId, SpeciesName, LOWER(Name) AS Name, MenuName, "
-            "FullName FROM Species")
+            "FullName, TaxonomyId FROM Species")
         return tuple(cursor.fetchall())
 
     return tuple()
diff --git a/uploader/species/views.py b/uploader/species/views.py
new file mode 100644
index 0000000..af66de3
--- /dev/null
+++ b/uploader/species/views.py
@@ -0,0 +1,91 @@
+"""Endpoints handling species."""
+from flask import (flash,
+                   request,
+                   url_for,
+                   redirect,
+                   Blueprint,
+                   current_app as app,
+                   render_template as flask_render_template)
+
+from uploader.db_utils import database_connection
+
+from .models import all_species, save_species, species_by_id
+
+
+speciesbp = Blueprint("species", __name__)
+
+
+def render_template(template, **kwargs):
+    """Render template for species."""
+    return flask_render_template(template, **kwargs, activelink="species")
+
+
+@speciesbp.route("/", methods=["GET"])
+def list_species():
+    """List and display all the species in the database."""
+    with database_connection(app.config["SQL_URI"]) as conn:
+        return render_template("species/list-species.html",
+                               allspecies=all_species(conn))
+
+@speciesbp.route("/<int:species_id>", methods=["GET"])
+def view_species(species_id: int):
+    """View details of a particular species and menus to act upon it."""
+    with database_connection(app.config["SQL_URI"]) as conn:
+        return species_by_id(conn, species_id)
+
+@speciesbp.route("/create", methods=["GET", "POST"])
+def create_species():
+    """Create a new species."""
+    # We can use uniprot's API to fetch the details with something like
+    # https://rest.uniprot.org/taxonomy/<taxonID> e.g.
+    # https://rest.uniprot.org/taxonomy/6239
+    if request.method == "GET":
+        return render_template("species/create-species.html")
+
+    with (database_connection(app.config["SQL_URI"]) as conn,
+          conn.cursor() as cursor):
+        error = False
+        taxon_id = request.form.get("taxon_id", "").strip() or None
+
+        common_name = request.form.get("common_name", "").strip()
+        if not bool(common_name):
+            flash("The common species name MUST be provided.", "alert-danger")
+            error = True
+
+        scientific_name = request.form.get("scientific_name", "").strip()
+        if not bool(scientific_name):
+            flash("The species' scientific name MUST be provided.",
+                  "alert-danger")
+            error = True
+
+        parts = tuple(name.strip() for name in scientific_name.split(" "))
+        if len(parts) != 2 or not all(bool(name) for name in parts):
+            flash("The scientific name you provided is invalid.", "alert-danger")
+            error = True
+
+        cursor.execute(
+            "SELECT * FROM Species WHERE FullName=%s", (scientific_name,))
+        res = cursor.fetchone()
+        if bool(res):
+            flash("A species already exists with the provided scientific name.",
+                  "alert-danger")
+            error = True
+
+        if bool(taxon_id):
+            cursor.execute(
+                "SELECT * FROM Species WHERE TaxonomyId=%s", (taxon_id,))
+            res = cursor.fetchone()
+            if bool(res):
+                flash("A species already exists with the provided scientific name.",
+                      "alert-danger")
+                error = True
+
+        if error:
+            return redirect(url_for("species.create_species",
+                                    common_name=common_name,
+                                    scientific_name=scientific_name,
+                                    taxon_id=taxon_id))
+
+        species = save_species(conn, common_name, scientific_name, taxon_id)
+        flash("Species saved successfully!", "alert-success")
+        return redirect(url_for("species.view_species", species_id=species["species_id"]))
diff --git a/uploader/templates/base.html b/uploader/templates/base.html
index 58227f4..7431767 100644
--- a/uploader/templates/base.html
+++ b/uploader/templates/base.html
@@ -44,6 +44,7 @@
     <aside id="nav-sidebar" class="container-fluid">
       <ul class="nav flex-column">
         <li><a href="/" >Home</a></li>
+        <li><a href="{{url_for('species.list_species')}}" >Species</a></li>
         <li><a href="{{url_for('expression-data.index.index')}}" >Expression Data</a></li>
       </ul>
     </aside>
@@ -54,7 +55,14 @@
         <h1>GN Uploader: {%block pagetitle%}{%endblock%}</h1>
         <nav>
           <ol class="breadcrumb">
-            {%block breadcrumb%}{%endblock%}
+            <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 breadcrumbs%}{%endblock%}
           </ol>
         </nav>
       </div>
diff --git a/uploader/templates/species/base.html b/uploader/templates/species/base.html
new file mode 100644
index 0000000..b77cc8b
--- /dev/null
+++ b/uploader/templates/species/base.html
@@ -0,0 +1,11 @@
+{%extends "base.html"%}
+
+{%block breadcrumbs%}
+<li {%if activelink=="species"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.list_species')}}">Species</a>
+</li>
+{%endblock%}
diff --git a/uploader/templates/species/create-species.html b/uploader/templates/species/create-species.html
new file mode 100644
index 0000000..b96e2d3
--- /dev/null
+++ b/uploader/templates/species/create-species.html
@@ -0,0 +1,116 @@
+{%extends "species/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}Create Species{%endblock%}
+
+{%block pagetitle%}Create Species{%endblock%}
+
+{%block breadcrumbs%}
+<li {%if activelink=="create-species"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.create_species')}}">Species</a>
+</li>
+{%endblock%}
+
+{%block contents%}
+<div class="row">
+  <form id="frm-create-species"
+        method="POST"
+        action="{{url_for('species.create_species')}}">
+    <legend>Create Species</legend>
+
+    {{flash_all_messages()}}
+
+    <div class="form-group">
+      <label for="txt-taxonomy-id" class="form-label">
+        Taxonomy ID</label>
+      <div class="input-group">
+        <input id="txt-taxonomy-id"
+               name="species_taxonomy_id"
+               type="text"
+               class="form-control" />
+        <span class="input-group-btn">
+          <button id="btn-search-taxonid" class="btn btn-info">Search</button>
+        </span>
+      </div>
+      <small class="form-text text-small text-muted">Provide the taxonomy ID for
+        your species that can be used to link to external sites like NCBI. Enter
+        the taxonomy ID and click "Search" to auto-fill the form with data.
+        <br />
+        While it is recommended to provide a value for this field, doing so is
+        optional.
+      </small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-species-name" class="form-label">Common Name</label>
+      <input id="txt-species-name"
+             name="common_name"
+             type="text"
+             class="form-control"
+             required="required" />
+      <small class="form-text text-muted">Provide the common, possibly
+        non-scientific name for the species here, e.g. Human, Mouse, etc.</small>
+    </div>
+
+    <div class="form-group">
+      <label for="txt-species-scientific" class="form-label">
+        Scientific Name</label>
+      <input id="txt-species-scientific"
+             name="scientific_name"
+             type="text"
+             class="form-control"
+             required="required" />
+      <small class="form-text text-muted">Provide the scientific name for the
+        species you are creating, e.g. Homo sapiens, Mus musculus, etc.</small>
+    </div>
+
+    <div class="form-group">
+      <input type="submit"
+             value="create new species"
+             class="btn btn-primary" />
+    </div>
+
+  </form>
+</div>
+{%endblock%}
+
+{%block javascript%}
+<script>
+  var lastTaxonId = null;
+
+  var fetch_taxonomy = (taxonId) => {
+      var uri = (
+          "https://rest.uniprot.org/taxonomy/" + encodeURIComponent(taxonId));
+      $.get(
+          uri,
+          {},
+          (data, textStatus, jqXHR) => {
+              if(textStatus == "success") {
+                  lastTaxonId = taxonId;
+                  $("#txt-species-scientific").val(data.scientificName);
+                  $("#txt-species-name").val(data.commonName);
+                  return false;
+              }
+              msg = (
+                  "Request to '${uri}' failed with message '${textStatus}'. "
+                  + "Please try again later, or fill the details manually.");
+              alert(msg);
+              console.error(msg, data, textStatus);
+              return false;
+          },
+          "json");
+  };
+
+  $("#btn-search-taxonid").on("click", (event) => {
+      event.preventDefault();
+      taxonId = $("#txt-taxonomy-id").val();
+      if((taxonId !== "") && (taxonId !== lastTaxonId)) {
+          fetch_taxonomy(taxonId);
+      }
+  });
+</script>
+{%endblock%}
diff --git a/uploader/templates/species/list-species.html b/uploader/templates/species/list-species.html
new file mode 100644
index 0000000..42094c1
--- /dev/null
+++ b/uploader/templates/species/list-species.html
@@ -0,0 +1,68 @@
+{%extends "species/base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+
+{%block title%}List Species{%endblock%}
+
+{%block pagetitle%}List Species{%endblock%}
+
+{%block contents%}
+<div class="row">
+  <p>
+    All data in GeneNetwork revolves around species. This is the core of the
+    system.</p>
+  <p>Here you can see a list of all the species available in GeneNetwork.
+    Click on the link besides each species to view greater detail on the species,
+    and access further operations that are possible for said species.</p>
+</div>
+
+<div class="row">
+  <p>If you cannot find the species you are looking for below, click the button
+    below to create it</p>
+  <p><a href="{{url_for('species.create_species')}}"
+        title="Add a new species to GeneNetwork"
+        class="btn btn-danger">Create Species</a></p>
+</div>
+
+<div class="row">
+  <table class="table">
+    <caption>Available Species</caption>
+    <thead>
+      <tr>
+        <th>Common Name</th>
+        <th>Scientific Name</th>
+        <th>TaxonId</th>
+        <th>Use</th>
+      </tr>
+    </thead>
+    <tbody>
+      {%for species in allspecies%}
+      <tr>
+        <td>{{species["SpeciesName"]}}</td>
+        <td>{{species["FullName"]}}</td>
+        <td>
+          <a href="https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id={{species['TaxonomyId']}}"
+             title="View species details on NCBI"
+             target="_blank">{{species["TaxonomyId"]}}</a>
+        </td>
+        <td>
+          <a href="{{url_for('species.view_species',
+                   species_id=species['SpeciesId'])}}"
+             title="">
+            {{species["SpeciesName"]}} ({{species["FullName"]}})
+          </a>
+        </td>
+      </tr>
+      {%else%}
+      <tr>
+        <td colspan="3">
+          <p class="text-danger">
+            <span class="glyphicon glyphicon-exclamation-mark"></span>
+            There were no species found!
+          </p>
+        </td>
+      </tr>
+      {%endfor%}
+    </tbody>
+  </table>
+</div>
+{%endblock%}