Skip to content

Commit

Permalink
Merge pull request msysbio#6 from AndrewRadev/flask-ui-5
Browse files Browse the repository at this point in the history
 Flask UI: Part 5, steps 1 and 2 of the upload process
  • Loading branch information
AndrewRadev authored Sep 24, 2024
2 parents 7693e1d + 1eb0c60 commit cdc937f
Show file tree
Hide file tree
Showing 42 changed files with 7,294 additions and 62 deletions.
3 changes: 3 additions & 0 deletions .projections.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"flask_app/forms/*.py": {
"type": "form"
},
"flask_app/models/*.py": {
"type": "model"
},
"flask_app/pages/*.py": {
"type": "page"
},
Expand Down
7 changes: 5 additions & 2 deletions _project.vim
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ silent TagsExclude .micromamba/* flask_app/static/build/* flask_app/static/js/ve

set tags+=micromamba.tags

let bacterial_growth_db_roots = ['.', 'flask_app']
let vim_bacterial_growth_roots = ['.', 'flask_app']

call bacterial_growth_db#Init()
call vim_bacterial_growth#Init()

nnoremap ,t :tabnew _project.vim<cr>
command! Eroutes :e flask_app/initialization/routes.py
command! Eschema :e src/sql_scripts/create_db.sql
12 changes: 12 additions & 0 deletions bin/server
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#! /bin/sh

python flask_app/main.py

# If there's a syntax error, the flask server will crash, let's rerun it on
# change:

inotifywait -m -e modify flask_app/** |
while read file_path file_event file_name; do
echo "DEBUG: $file_path $file_event $file_name"
python flask_app/main.py
done
2 changes: 1 addition & 1 deletion flask_app/forms/experiment_chart_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def generate_reads_figures(self, read_type, args):
var_name='Species',
value_name='STD'
)
error_y=std_df['STD']
error_y = std_df['STD']

figs.append(px.line(
melted_df,
Expand Down
17 changes: 17 additions & 0 deletions flask_app/forms/upload_step2_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from flask_wtf import FlaskForm
from wtforms import SelectField, SelectMultipleField, StringField, FormField, FieldList
from wtforms.validators import DataRequired, Optional


class NewStrainsForm(FlaskForm):
class Meta:
csrf = False

name = StringField('name', validators=[DataRequired()])
description = StringField('description', validators=[Optional()])
species = SelectField('species')


class UploadStep2Form(FlaskForm):
strains = SelectMultipleField('strains')
new_strains = FieldList(FormField(NewStrainsForm), min_entries=0)
30 changes: 30 additions & 0 deletions flask_app/forms/upload_step3_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from flask_wtf import FlaskForm
from wtforms import SelectField, SelectMultipleField, IntegerField
from wtforms.validators import DataRequired


class UploadStep3Form(FlaskForm):
vessel_type = SelectField('vessel_type', choices=[
('bottles', "Bottles"),
('agar_plates', "Agar plates"),
('well_plates', "Well plates"),
('mini_react', "mini bioreactors"),
], validators=DataRequired())

bottle_count = IntegerField('bottle_count')
plate_count = IntegerField('plate_count')
column_count = IntegerField('column_count')
row_count = IntegerField('row_count')

timepoint_count = IntegerField('timepoint_count', validators=[DataRequired()])

technique_type = SelectField('technique_type', choices=[
('od', "Optical Density"),
('plates', "Plate Counts"),
('plates_ps', "Plate Counts (per species)"),
('fc', "Flow Cytometry"),
('fc_ps', "Flow Cytometry (per species)"),
('rna', "16S rRNA-seq"),
], validators=DataRequired())

technique_type = SelectMultipleField('metabolites')
5 changes: 5 additions & 0 deletions flask_app/initialization/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ def init_assets(app):

assets.register('app_js', flask_assets.Bundle(
'js/vendor/jquery-3.7.1.js',
'js/vendor/select2-4.0.13.js',
'js/util.js',
'js/main.js',
'js/search.js',
'js/dashboard.js',
'js/upload.js',
filters='rjsmin',
output='build/app.js'
))
Expand All @@ -20,12 +22,15 @@ def init_assets(app):
))

assets.register('app_css', flask_assets.Bundle(
'css/vendor/select2-4.0.13.css',
'css/select2-custom.css',
'css/reset.css',
'css/utils.css',
'css/main.css',
'css/sidebar.css',
'css/search.css',
'css/dashboard.css',
'css/upload.css',
filters='cssmin',
output='build/app.css'
))
Expand Down
9 changes: 7 additions & 2 deletions flask_app/initialization/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ def init_routes(app):
app.add_url_rule("/dashboard", view_func=dashboard_pages.dashboard_index_page)
app.add_url_rule("/dashboard/chart", view_func=dashboard_pages.dashboard_chart_fragment)

app.add_url_rule("/upload", view_func=upload_pages.upload_index_page)
app.add_url_rule("/upload/1", view_func=upload_pages.upload_step1_page, methods=["GET", "POST"])
app.add_url_rule("/upload/2", view_func=upload_pages.upload_step2_page, methods=["GET", "POST"])
app.add_url_rule("/upload/3", view_func=upload_pages.upload_step3_page, methods=["GET", "POST"])
app.add_url_rule("/upload/4", view_func=upload_pages.upload_step4_page, methods=["GET", "POST"])

app.add_url_rule("/study/<string:studyId>", view_func=study_pages.study_show_page)
app.add_url_rule("/study/<string:studyId>.zip", view_func=study_pages.study_download_page)

app.add_url_rule("/strain/<int:id>", view_func=strain_pages.strain_show_page)
app.add_url_rule("/strain/<int:id>", view_func=strain_pages.strain_show_page)
app.add_url_rule("/strains/completion", view_func=strain_pages.taxa_completion_json)

app.add_url_rule("/metabolite/<string:cheb_id>", view_func=metabolite_pages.metabolite_show_page)

app.add_url_rule(
Expand Down
116 changes: 116 additions & 0 deletions flask_app/models/submission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from uuid import uuid4

import sqlalchemy as sql


class Submission:
def __init__(self, data, current_step=None, db_conn=None):
self.step = current_step or 1
self.db_conn = db_conn

# Step 1:
self.type = data.get('type', None)
self.project_uuid = data.get('project_uuid', None)
self.study_uuid = data.get('study_uuid', None)

self.project = data.get('project', {'name': None, 'description': None})

# Step 2:
self.strains = data.get('strains', [])
self.new_strains = data.get('new_strains', [])

def update_project(self, data):
self.type = data['submission_type']

if self.type == 'new_project':
self.project_uuid = uuid4()
self.study_uuid = uuid4()

self.project = {
'name': data['project_name'],
'description': data['project_description'],
}
elif self.type == 'new_study':
self.project = _get_project_data(self.db_conn, data['project_uuid'])
if self.project:
self.project_uuid = data['project_uuid']

self.study_uuid = uuid4()
elif self.type == 'update_study':
self.project = _get_project_data(self.db_conn, data['project_uuid'])
if self.project:
self.project_uuid = data['project_uuid']

self.study_uuid = data['study_uuid']
else:
raise KeyError("Unknown self type: {}".format(self.submission_type))

def update_strains(self, data):
self.strains = data['strains']
self.new_strains = data['new_strains']

def update_study_design(self, data):
pass

def fetch_strains(self):
if len(self.strains) == 0:
return []

# TODO (2024-09-22) Figure out a sensible way to bind ids

query = f"""
SELECT
tax_id AS id,
tax_names AS name
FROM Taxa
WHERE tax_id IN ({','.join(self.strains)})
"""
result = self.db_conn.execute(sql.text(query)).all()

return [(entry.id, entry.name) for entry in result]

def fetch_new_strains(self):
if len(self.new_strains) == 0:
return []

new_strains = sorted(self.new_strains, key=lambda s: int(s['species']))
species_ids = [s['species'] for s in new_strains]

query = f"""
SELECT tax_names
FROM Taxa
WHERE tax_id IN ({','.join(species_ids)})
ORDER BY tax_id
"""
species_names = self.db_conn.execute(sql.text(query)).scalars()

for strain, name in zip(new_strains, species_names):
strain['species_name'] = name

return new_strains

def _asdict(self):
return {
'type': self.type,
'project_uuid': self.project_uuid,
'study_uuid': self.study_uuid,
'project': self.project,
'strains': self.strains,
'new_strains': self.new_strains,
}


def _get_project_data(db_conn, uuid):
query = """
SELECT
projectName AS name,
projectDescription AS description
FROM Project
WHERE projectUniqueID = :uuid
"""
result = db_conn.execute(sql.text(query), {'uuid': uuid}).one_or_none()

if result is None:
return None
else:
return result._asdict()
43 changes: 42 additions & 1 deletion flask_app/pages/strains.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from flask import render_template
import json

from flask import render_template, request
import sqlalchemy as sql

from flask_app.db import get_connection
Expand All @@ -13,3 +15,42 @@ def strain_show_page(id):
study = conn.execute(sql.text(query), {'studyId': strain['studyId']}).one()._asdict()

return render_template("pages/strains/show.html", strain=strain, study=study)


def taxa_completion_json():
term = request.args.get('term', '').lower()
page = int(request.args.get('page', '1'))
per_page = 10

# TODO (2024-09-21) Extract to model and test

with get_connection() as conn:
query = """
SELECT
tax_id AS id,
tax_names AS text
FROM Taxa
WHERE LOWER(tax_names) LIKE :term
ORDER BY tax_names ASC
LIMIT :per_page
OFFSET :offset
"""
results = conn.execute(sql.text(query), {
'term': f'%{term}%',
'per_page': per_page,
'offset': (page - 1) * per_page,
}).all()
results = [row._asdict() for row in results]

count_query = """
SELECT COUNT(*)
FROM Taxa
WHERE LOWER(tax_names) LIKE LOWER(:term)
"""
total_count = conn.execute(sql.text(count_query), {'term': f'%{term}%'}).scalar()
has_more = (page * per_page < total_count)

return json.dumps({
'results': results,
'pagination': {'more': has_more},
})
72 changes: 69 additions & 3 deletions flask_app/pages/upload.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
from flask import render_template
from flask import render_template, session, request, redirect, url_for

from flask_app.db import get_connection
from flask_app.models.submission import Submission
from flask_app.forms.upload_step2_form import UploadStep2Form
from flask_app.forms.upload_step3_form import UploadStep3Form

def upload_index_page():
return render_template("pages/upload.html")

def upload_step1_page():
with get_connection() as conn:
submission = Submission(session.get('submission', {}), current_step=1, db_conn=conn)
error = None

if request.method == 'POST':
submission.update_project(request.form)

if submission.project:
session['submission'] = submission._asdict()
return redirect(url_for('upload_step2_page'))
else:
error = "No project found with this UUID"

return render_template(
"pages/upload/index.html",
submission=submission,
error=error
)


def upload_step2_page():
with get_connection() as conn:
submission = Submission(session.get('submission', {}), current_step=2, db_conn=conn)
form = UploadStep2Form(request.form)

if request.method == 'POST':
submission.update_strains(form.data)
session['submission'] = submission._asdict()

return redirect(url_for('upload_step3_page'))

return render_template(
"pages/upload/index.html",
submission=submission,
)


def upload_step3_page():
with get_connection() as conn:
submission = Submission(session.get('submission', {}), current_step=3, db_conn=conn)
form = UploadStep3Form(request.form)

if request.method == 'POST':
submission.update_study_design(form.data)
session['submission'] = submission._asdict()

return redirect(url_for('upload_step3_page'))

return render_template(
"pages/upload/index.html",
submission=submission,
)


def upload_step4_page():
with get_connection() as conn:
submission = Submission(session.get('submission', {}), current_step=4, db_conn=conn)

return render_template(
"pages/upload/index.html",
submission=submission,
)
2 changes: 1 addition & 1 deletion flask_app/static/css/dashboard.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.dashboard-page {
details.experiment-container {
border: 1px solid var(--border-color);
border-radius: 8px;
border-radius: var(--border-radius);
padding: 10px;
margin-top: 20px;
margin-bottom: 10px;
Expand Down
Loading

0 comments on commit cdc937f

Please sign in to comment.