From 493f8fbe747650a4fbac2e0b153ad0074b4f91e4 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Wed, 6 Dec 2023 13:00:53 +0300 Subject: Feature: Upload Samples/Cases Implements the code enabling the upload of the samples/cases to the database. --- qc_app/__init__.py | 2 + qc_app/db_utils.py | 7 +- qc_app/entry.py | 8 +- qc_app/samples.py | 108 ++++++++++++++++++++++++ qc_app/templates/flash_messages.html | 25 ++++++ qc_app/templates/index.html | 35 +++++++- qc_app/templates/samples/select-population.html | 105 +++++++++++++++++++++++ qc_app/templates/samples/upload-samples.html | 107 +++++++++++++++++++++++ 8 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 qc_app/samples.py create mode 100644 qc_app/templates/flash_messages.html create mode 100644 qc_app/templates/samples/select-population.html create mode 100644 qc_app/templates/samples/upload-samples.html diff --git a/qc_app/__init__.py b/qc_app/__init__.py index 4810d45..f2001ab 100644 --- a/qc_app/__init__.py +++ b/qc_app/__init__.py @@ -6,6 +6,7 @@ from flask import Flask from .entry import entrybp from .parse import parsebp +from .samples import samples from .dbinsert import dbinsertbp from .errors import register_error_handlers @@ -30,6 +31,7 @@ def create_app(instance_dir): app.register_blueprint(entrybp, url_prefix="/") app.register_blueprint(parsebp, url_prefix="/parse") app.register_blueprint(dbinsertbp, url_prefix="/dbinsert") + app.register_blueprint(samples, url_prefix="/samples") register_error_handlers(app) return app diff --git a/qc_app/db_utils.py b/qc_app/db_utils.py index a04c5e1..75b6b73 100644 --- a/qc_app/db_utils.py +++ b/qc_app/db_utils.py @@ -2,7 +2,7 @@ import logging import traceback import contextlib -from typing import Tuple, Optional, Iterator +from typing import Any, Tuple, Optional, Iterator, Callable from urllib.parse import urlparse import MySQLdb as mdb @@ -32,3 +32,8 @@ def database_connection(db_url: Optional[str] = None) -> Iterator[mdb.Connection connection.rollback() finally: connection.close() + +def with_db_connection(func: Callable[[mdb.Connection], Any]) -> Any: + """Call `func` with a MySQDdb database connection.""" + with database_connection(app.config["SQL_URI"]) as conn: + return func(conn) diff --git a/qc_app/entry.py b/qc_app/entry.py index abea5ed..bf78037 100644 --- a/qc_app/entry.py +++ b/qc_app/entry.py @@ -14,6 +14,8 @@ from flask import ( render_template, current_app as app) +from .dbinsert import species + entrybp = Blueprint("entry", __name__) def errors(rqst) -> Tuple[str, ...]: @@ -76,14 +78,14 @@ def zip_file_errors(filepath, upload_dir) -> Tuple[str, ...]: def upload_file(): """Enables uploading the files""" if request.method == "GET": - return render_template("index.html") + return render_template("index.html", species = species()) upload_dir = app.config["UPLOAD_FOLDER"] request_errors = errors(request) if request_errors: for error in request_errors: flash(error, "alert-error") - return render_template("index.html"), 400 + return render_template("index.html", species = species()), 400 filename = secure_filename(request.files["qc_text_file"].filename) if not os.path.exists(upload_dir): @@ -96,7 +98,7 @@ def upload_file(): if zip_errors: for error in zip_errors: flash(error, "alert-error") - return render_template("index.html"), 400 + return render_template("index.html", species = species()), 400 return redirect(url_for( "parse.parse", filename=filename, diff --git a/qc_app/samples.py b/qc_app/samples.py new file mode 100644 index 0000000..cc745ca --- /dev/null +++ b/qc_app/samples.py @@ -0,0 +1,108 @@ +"""Code regarding samples""" +import MySQLdb as mdb +from MySQLdb.cursors import DictCursor +from flask import ( + flash, request, url_for, redirect, Blueprint, render_template, current_app as app) + +from .db_utils import with_db_connection +from .dbinsert import species_by_id, groups_by_species + +samples = Blueprint("samples", __name__) + +@samples.route("/upload/species", methods=["POST"]) +def select_species(): + """Select the species.""" + index_page = redirect(url_for("entry.upload_file")) + species_id = request.form.get("species_id") + if bool(species_id): + species_id = int(species_id) + species = species_by_id(species_id) + if bool(species): + return render_template( + "samples/select-population.html", + species=species, + populations=groups_by_species(species_id)) + flash("Invalid species selected!", "alert-error") + flash("You need to select a species", "alert-error") + return index_page + +def save_population(conn: mdb.Connection, population_details: dict) -> int: + """Save the population details to the db.""" + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute("SELECT MAX(Id) AS last_id FROM InbredSet") + new_id = cursor.fetchone()["last_id"] + 1 + cursor.execute( + "INSERT INTO InbredSet(" + "Id, InbredSetId, InbredSetName, Name, SpeciesId, FullName, " + "MenuOrderId, Description" + ") " + "VALUES (" + "%(Id)s, %(InbredSetId)s, %(InbredSetName)s, %(Name)s, " + "%(SpeciesId)s, %(FullName)s, %(MenuOrderId)s, %(Description)s" + ")", + { + "Id": new_id, + "InbredSetId": new_id, + "MenuOrderId": 0, + **population_details + }) + return new_id + +def population_by_id(conn: mdb.Connection, population_id: int) -> dict: + """Get the grouping/population by id.""" + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute("SELECT * FROM InbredSet WHERE InbredSetId=%s", + (population_id,)) + return cursor.fetchone() + +@samples.route("/upload/create-population", methods=["POST"]) +def create_population(): + """Create new grouping/population.""" + species_page = redirect(url_for("samples.select_species"), code=307) + species = species_by_id(request.form.get("species_id")) + pop_name = request.form.get("inbredset_name").strip() + pop_fullname = request.form.get("inbredset_fullname").strip() + + if not bool(species): + flash("Invalid species!", "alert-error error-create-population") + return species_page + if (not bool(pop_name)) or (not bool(pop_fullname)): + flash("You *MUST* provide a grouping/population name", + "alert-error error-create-population") + return species_page + + pop_id = with_db_connection(lambda conn: save_population(conn, { + "SpeciesId": species["SpeciesId"], + "Name": pop_name, + "InbredSetName": pop_fullname, + "FullName": pop_fullname, + "Family": request.form.get("inbredset_family") or None, + "Description": request.form.get("description") or None + })) + flash("Grouping/Population created successfully.", "alert-success") + return render_template( + "samples/upload-samples.html", + species=species, + population=with_db_connection( + lambda conn: population_by_id(conn, pop_id))) + +@samples.route("/upload/select-population", methods=["POST"]) +def select_population(): + """Select from existing groupings/populations.""" + species_page = redirect(url_for("samples.select_species"), code=307) + species = species_by_id(request.form.get("species_id")) + pop_id = int(request.form.get("inbredset_id")) + population = with_db_connection(lambda conn: population_by_id(conn, pop_id)) + + if not bool(species): + flash("Invalid species!", "alert-error error-select-population") + return species_page + + if not bool(population): + flash("Invalid grouping/population!", + "alert-error error-select-population") + return species_page + + return render_template("samples/upload-samples.html", + species=species, + population=population) diff --git a/qc_app/templates/flash_messages.html b/qc_app/templates/flash_messages.html new file mode 100644 index 0000000..b7af178 --- /dev/null +++ b/qc_app/templates/flash_messages.html @@ -0,0 +1,25 @@ +{%macro flash_all_messages()%} +{%with messages = get_flashed_messages(with_categories=true)%} +{%if messages:%} +
This application assumes that you are familiar with the basics of data @@ -26,7 +28,7 @@
+ {%endblock%} diff --git a/qc_app/templates/samples/select-population.html b/qc_app/templates/samples/select-population.html new file mode 100644 index 0000000..24decb4 --- /dev/null +++ b/qc_app/templates/samples/select-population.html @@ -0,0 +1,105 @@ +{%extends "base.html"%} +{%from "flash_messages.html" import flash_messages%} + +{%block title%}Select Grouping/Population{%endblock%} + +{%block contents%} +We organise the samples/cases/strains in a hierarchichal form, starting + with species at the very top. Under species, we have a + grouping in terms of the relevant population + (e.g. Inbred populations, cell tissue, etc.)
+OR
+ + + +{%endblock%} + + +{%block javascript%} +{%endblock%} diff --git a/qc_app/templates/samples/upload-samples.html b/qc_app/templates/samples/upload-samples.html new file mode 100644 index 0000000..b19e38c --- /dev/null +++ b/qc_app/templates/samples/upload-samples.html @@ -0,0 +1,107 @@ +{%extends "base.html"%} +{%from "flash_messages.html" import flash_messages%} + +{%block title%}Upload Samples{%endblock%} + +{%block css%} + +{%endblock%} + +{%block contents%} +You can now upload a character-separated value (CSV) file that contains + details about your samples. The CSV file should have the following fields: +