Increased robustness of form parsers.
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
|
## 202310.01
|
||||||
|
|
||||||
|
- Controls linker is now depreciated.
|
||||||
|
- Controls will now be directly added to their submissions instead of having to run linker.
|
||||||
|
- Submission details now has additional html functionality in plate map.
|
||||||
|
- Added Submission Category to fields.
|
||||||
|
- Increased robustness of form parsers by adding custom procedures for each.
|
||||||
|
|
||||||
## 202309.04
|
## 202309.04
|
||||||
|
|
||||||
|
- Updated KitAdder to add location info as well.
|
||||||
- Extraction kit can now be updated after import.
|
- Extraction kit can now be updated after import.
|
||||||
- Large scale refactoring to improve efficiency of database functions.
|
- Large scale refactoring to improve efficiency of database functions.
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -49,7 +49,16 @@ This is meant to import .xslx files created from the Design & Analysis Software
|
|||||||
|
|
||||||
## Adding new Kit:
|
## Adding new Kit:
|
||||||
|
|
||||||
1. Instructions to come.
|
1. Click "Add Kit" tab in the tab bar.
|
||||||
|
2. Select the Submission type from the drop down menu.
|
||||||
|
3. Fill in the kit name (required) and other fields (optional).
|
||||||
|
4. For each reagent type in the kit click the "Add Reagent Type" button.
|
||||||
|
5. Fill in the name of the reagent type. Alternatively select from already existing types in the drop down.
|
||||||
|
6. Fill in the reagent location in the excel submission sheet.
|
||||||
|
a. For example if the reagent name is in a sheet called "Reagent Info" in row 12, column 1, type "Reagent Info" in the "Excel Location Sheet Name" field.
|
||||||
|
b. Set 12 in the "Name Row" and 1 in the "Name Column".
|
||||||
|
c. Repeat 6b for the Lot and the Expiry row and columns.
|
||||||
|
7. Click the "Submit" button at the top.
|
||||||
|
|
||||||
## Linking Controls:
|
## Linking Controls:
|
||||||
|
|
||||||
|
|||||||
9
TODO.md
9
TODO.md
@@ -1,7 +1,12 @@
|
|||||||
|
- [ ] Get info for controls into their sample hitpicks.
|
||||||
|
- [x] Move submission-type specific parser functions into class methods in their respective models.
|
||||||
|
- [ ] Improve results reporting.
|
||||||
|
- Maybe make it a list until it gets to the reporter?
|
||||||
|
- [x] Increase robustness of form parsers by adding custom procedures for each.
|
||||||
- [x] Rerun Kit integrity if extraction kit changed in the form.
|
- [x] Rerun Kit integrity if extraction kit changed in the form.
|
||||||
- [x] Clean up db.functions.
|
- [x] Clean up db.functions.
|
||||||
- [ ] Make kits easier to add.
|
- [x] Make kits easier to add.
|
||||||
- [ ] Clean up & document code... again.
|
- [x] Clean up & document code... again.
|
||||||
- Including paring down the logging.debugs
|
- Including paring down the logging.debugs
|
||||||
- Also including reducing number of functions in db.functions
|
- Also including reducing number of functions in db.functions
|
||||||
- [x] Fix Tests... again.
|
- [x] Fix Tests... again.
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||||
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
|
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
|
||||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions_test.db
|
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|||||||
32
alembic/versions/b95478ffb4a3_adding_submission_category.py
Normal file
32
alembic/versions/b95478ffb4a3_adding_submission_category.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""adding submission category
|
||||||
|
|
||||||
|
Revision ID: b95478ffb4a3
|
||||||
|
Revises: 9a133efb3ffd
|
||||||
|
Create Date: 2023-10-03 14:00:09.663055
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b95478ffb4a3'
|
||||||
|
down_revision = '9a133efb3ffd'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('submission_category', sa.String(length=64), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('submission_category')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Version of the realpython-reader package
|
# Version of the realpython-reader package
|
||||||
__project__ = "submissions"
|
__project__ = "submissions"
|
||||||
__version__ = "202309.4b"
|
__version__ = "202310.1b"
|
||||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||||
__copyright__ = "2022-2023, Government of Canada"
|
__copyright__ = "2022-2023, Government of Canada"
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ def store_object(ctx:Settings, object) -> dict|None:
|
|||||||
dbs.merge(object)
|
dbs.merge(object)
|
||||||
try:
|
try:
|
||||||
dbs.commit()
|
dbs.commit()
|
||||||
except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e:
|
except (SQLIntegrityError, AlcIntegrityError) as e:
|
||||||
logger.debug(f"Hit an integrity error : {e}")
|
logger.debug(f"Hit an integrity error : {e}")
|
||||||
dbs.rollback()
|
dbs.rollback()
|
||||||
return {"message":f"This object {object} already exists, so we can't add it.", "status":"Critical"}
|
return {"message":f"This object {object} already exists, so we can't add it.\n{e}", "status":"Critical"}
|
||||||
except (SQLOperationalError, AlcOperationalError):
|
except (SQLOperationalError, AlcOperationalError):
|
||||||
logger.error(f"Hit an operational error: {e}")
|
logger.error(f"Hit an operational error: {e}")
|
||||||
dbs.rollback()
|
dbs.rollback()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from datetime import date, timedelta
|
|||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from sqlalchemy.exc import IntegrityError, SAWarning
|
from sqlalchemy.exc import IntegrityError, SAWarning
|
||||||
|
from . import store_object
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ def construct_samples(ctx:Settings, instance:models.BasicSubmission, samples:Lis
|
|||||||
models.BasicSubmission: Updated submission object.
|
models.BasicSubmission: Updated submission object.
|
||||||
"""
|
"""
|
||||||
for sample in samples:
|
for sample in samples:
|
||||||
sample_instance = lookup_samples(ctx=ctx, submitter_id=sample['sample'].submitter_id)
|
sample_instance = lookup_samples(ctx=ctx, submitter_id=str(sample['sample'].submitter_id))
|
||||||
if sample_instance == None:
|
if sample_instance == None:
|
||||||
sample_instance = sample['sample']
|
sample_instance = sample['sample']
|
||||||
else:
|
else:
|
||||||
@@ -174,7 +175,7 @@ def construct_samples(ctx:Settings, instance:models.BasicSubmission, samples:Lis
|
|||||||
try:
|
try:
|
||||||
assoc = getattr(models, f"{sample_query}Association")
|
assoc = getattr(models, f"{sample_query}Association")
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.error(f"Couldn't get type specific association. Getting generic.")
|
logger.error(f"Couldn't get type specific association using {sample_instance.sample_type.replace('Sample', '').strip()}. Getting generic.")
|
||||||
assoc = models.SubmissionSampleAssociation
|
assoc = models.SubmissionSampleAssociation
|
||||||
assoc = assoc(submission=instance, sample=sample_instance, row=sample['row'], column=sample['column'])
|
assoc = assoc(submission=instance, sample=sample_instance, row=sample['row'], column=sample['column'])
|
||||||
instance.submission_sample_associations.append(assoc)
|
instance.submission_sample_associations.append(assoc)
|
||||||
@@ -189,7 +190,7 @@ def construct_samples(ctx:Settings, instance:models.BasicSubmission, samples:Lis
|
|||||||
continue
|
continue
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def construct_kit_from_yaml(ctx:Settings, exp:dict) -> dict:
|
def construct_kit_from_yaml(ctx:Settings, kit_dict:dict) -> dict:
|
||||||
"""
|
"""
|
||||||
Create and store a new kit in the database based on a .yml file
|
Create and store a new kit in the database based on a .yml file
|
||||||
TODO: split into create and store functions
|
TODO: split into create and store functions
|
||||||
@@ -206,36 +207,33 @@ def construct_kit_from_yaml(ctx:Settings, exp:dict) -> dict:
|
|||||||
if not check_is_power_user(ctx=ctx):
|
if not check_is_power_user(ctx=ctx):
|
||||||
logger.debug(f"{getuser()} does not have permission to add kits.")
|
logger.debug(f"{getuser()} does not have permission to add kits.")
|
||||||
return {'code':1, 'message':"This user does not have permission to add kits.", "status":"warning"}
|
return {'code':1, 'message':"This user does not have permission to add kits.", "status":"warning"}
|
||||||
# iterate through keys in dict
|
submission_type = lookup_submission_type(ctx=ctx, name=kit_dict['used_for'])
|
||||||
for type in exp:
|
logger.debug(f"Looked up submission type: {kit_dict['used_for']} and got {submission_type}")
|
||||||
# A submission type may use multiple kits.
|
kit = models.KitType(name=kit_dict["kit_name"])
|
||||||
for kt in exp[type]['kits']:
|
kt_st_assoc = models.SubmissionTypeKitTypeAssociation(kit_type=kit, submission_type=submission_type)
|
||||||
logger.debug(f"Looking up submission type: {type}")
|
for k,v in kit_dict.items():
|
||||||
# submission_type = lookup_submissiontype_by_name(ctx=ctx, type_name=type)
|
if k not in ["reagent_types", "kit_name", "used_for"]:
|
||||||
submission_type = lookup_submission_type(ctx=ctx, name=type)
|
kt_st_assoc.set_attrib(k, v)
|
||||||
logger.debug(f"Looked up submission type: {submission_type}")
|
kit.kit_submissiontype_associations.append(kt_st_assoc)
|
||||||
kit = models.KitType(name=kt)
|
# A kit contains multiple reagent types.
|
||||||
kt_st_assoc = models.SubmissionTypeKitTypeAssociation(kit_type=kit, submission_type=submission_type)
|
for r in kit_dict['reagent_types']:
|
||||||
kt_st_assoc.constant_cost = exp[type]["kits"][kt]["constant_cost"]
|
# check if reagent type already exists.
|
||||||
kt_st_assoc.mutable_cost_column = exp[type]["kits"][kt]["mutable_cost_column"]
|
logger.debug(f"Constructing reagent type: {r}")
|
||||||
kt_st_assoc.mutable_cost_sample = exp[type]["kits"][kt]["mutable_cost_sample"]
|
rtname = massage_common_reagents(r['rtname'])
|
||||||
kit.kit_submissiontype_associations.append(kt_st_assoc)
|
# look_up = ctx.database_session.query(models.ReagentType).filter(models.ReagentType.name==rtname).first()
|
||||||
# A kit contains multiple reagent types.
|
look_up = lookup_reagent_types(name=rtname)
|
||||||
for r in exp[type]['kits'][kt]['reagenttypes']:
|
if look_up == None:
|
||||||
# check if reagent type already exists.
|
rt = models.ReagentType(name=rtname.strip(), eol_ext=timedelta(30*r['eol']))
|
||||||
r = massage_common_reagents(r)
|
else:
|
||||||
look_up = ctx.database_session.query(models.ReagentType).filter(models.ReagentType.name==r).first()
|
rt = look_up
|
||||||
if look_up == None:
|
uses = {kit_dict['used_for']:{k:v for k,v in r.items() if k not in ['eol']}}
|
||||||
rt = models.ReagentType(name=r.strip(), eol_ext=timedelta(30*exp[type]['kits'][kt]['reagenttypes'][r]['eol_ext']), last_used="")
|
assoc = models.KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=rt, uses=uses)
|
||||||
else:
|
# ctx.database_session.add(rt)
|
||||||
rt = look_up
|
store_object(ctx=ctx, object=rt)
|
||||||
assoc = models.KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=rt, uses={})
|
kit.kit_reagenttype_associations.append(assoc)
|
||||||
ctx.database_session.add(rt)
|
logger.debug(f"Kit construction reagent type: {rt.__dict__}")
|
||||||
kit.kit_reagenttype_associations.append(assoc)
|
logger.debug(f"Kit construction kit: {kit.__dict__}")
|
||||||
logger.debug(f"Kit construction reagent type: {rt.__dict__}")
|
store_object(ctx=ctx, object=kit)
|
||||||
logger.debug(f"Kit construction kit: {kit.__dict__}")
|
|
||||||
ctx.database_session.add(kit)
|
|
||||||
ctx.database_session.commit()
|
|
||||||
return {'code':0, 'message':'Kit has been added', 'status': 'information'}
|
return {'code':0, 'message':'Kit has been added', 'status': 'information'}
|
||||||
|
|
||||||
def construct_org_from_yaml(ctx:Settings, org:dict) -> dict:
|
def construct_org_from_yaml(ctx:Settings, org:dict) -> dict:
|
||||||
|
|||||||
@@ -209,7 +209,11 @@ def lookup_submissions(ctx:Settings,
|
|||||||
match rsl_number:
|
match rsl_number:
|
||||||
case str():
|
case str():
|
||||||
logger.debug(f"Looking up BasicSubmission with rsl number: {rsl_number}")
|
logger.debug(f"Looking up BasicSubmission with rsl number: {rsl_number}")
|
||||||
rsl_number = RSLNamer(ctx=ctx, instr=rsl_number).parsed_name
|
try:
|
||||||
|
rsl_number = RSLNamer(ctx=ctx, instr=rsl_number).parsed_name
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.error(f"No parsed name found, returning None.")
|
||||||
|
return None
|
||||||
# query = query.filter(models.BasicSubmission.rsl_plate_num==rsl_number)
|
# query = query.filter(models.BasicSubmission.rsl_plate_num==rsl_number)
|
||||||
query = query.filter(model.rsl_plate_num==rsl_number)
|
query = query.filter(model.rsl_plate_num==rsl_number)
|
||||||
limit = 1
|
limit = 1
|
||||||
@@ -306,6 +310,7 @@ def lookup_controls(ctx:Settings,
|
|||||||
control_type:models.ControlType|str|None=None,
|
control_type:models.ControlType|str|None=None,
|
||||||
start_date:date|str|int|None=None,
|
start_date:date|str|int|None=None,
|
||||||
end_date:date|str|int|None=None,
|
end_date:date|str|int|None=None,
|
||||||
|
control_name:str|None=None,
|
||||||
limit:int=0
|
limit:int=0
|
||||||
) -> models.Control|List[models.Control]:
|
) -> models.Control|List[models.Control]:
|
||||||
query = setup_lookup(ctx=ctx, locals=locals()).query(models.Control)
|
query = setup_lookup(ctx=ctx, locals=locals()).query(models.Control)
|
||||||
@@ -343,6 +348,12 @@ def lookup_controls(ctx:Settings,
|
|||||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||||
logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||||
query = query.filter(models.Control.submitted_date.between(start_date, end_date))
|
query = query.filter(models.Control.submitted_date.between(start_date, end_date))
|
||||||
|
match control_name:
|
||||||
|
case str():
|
||||||
|
query = query.filter(models.Control.name.startswith(control_name))
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
return query_return(query=query, limit=limit)
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
def lookup_control_types(ctx:Settings, limit:int=0) -> models.ControlType|List[models.ControlType]:
|
def lookup_control_types(ctx:Settings, limit:int=0) -> models.ControlType|List[models.ControlType]:
|
||||||
|
|||||||
@@ -236,3 +236,24 @@ def update_subsampassoc_with_pcr(ctx:Settings, submission:models.BasicSubmission
|
|||||||
result = store_object(ctx=ctx, object=assoc)
|
result = store_object(ctx=ctx, object=assoc)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_polymorphic_subclass(base:object, polymorphic_identity:str|None=None):
|
||||||
|
"""
|
||||||
|
Retrieves any subclasses of given base class whose polymorphic identity matches the string input.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base (object): Base (parent) class
|
||||||
|
polymorphic_identity (str | None): Name of subclass of interest. (Defaults to None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_: Subclass, or parent class on
|
||||||
|
"""
|
||||||
|
if polymorphic_identity == None:
|
||||||
|
return base
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return [item for item in base.__subclasses__() if item.__mapper_args__['polymorphic_identity']==polymorphic_identity][0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not get polymorph {polymorphic_identity} of {base} due to {e}")
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
Contains all models for sqlalchemy
|
Contains all models for sqlalchemy
|
||||||
'''
|
'''
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlalchemy.orm import declarative_base
|
from sqlalchemy.orm import declarative_base, DeclarativeMeta
|
||||||
import logging
|
import logging
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
Base = declarative_base()
|
Base: DeclarativeMeta = declarative_base()
|
||||||
metadata = Base.metadata
|
metadata = Base.metadata
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|||||||
@@ -332,4 +332,7 @@ class SubmissionTypeKitTypeAssociation(Base):
|
|||||||
self.constant_cost = 0.00
|
self.constant_cost = 0.00
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name})"
|
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name})"
|
||||||
|
|
||||||
|
def set_attrib(self, name, value):
|
||||||
|
self.__setattr__(name, value)
|
||||||
@@ -13,6 +13,9 @@ from sqlalchemy.ext.associationproxy import association_proxy
|
|||||||
import uuid
|
import uuid
|
||||||
from pandas import Timestamp
|
from pandas import Timestamp
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
|
import re
|
||||||
|
import pandas as pd
|
||||||
|
from tools import row_map
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ class BasicSubmission(Base):
|
|||||||
run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation.
|
run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation.
|
||||||
uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database.
|
uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database.
|
||||||
comment = Column(JSON)
|
comment = Column(JSON)
|
||||||
|
submission_category = Column(String(64))
|
||||||
|
|
||||||
submission_sample_associations = relationship(
|
submission_sample_associations = relationship(
|
||||||
"SubmissionSampleAssociation",
|
"SubmissionSampleAssociation",
|
||||||
@@ -83,7 +87,7 @@ class BasicSubmission(Base):
|
|||||||
dict: dictionary used in submissions summary and details
|
dict: dictionary used in submissions summary and details
|
||||||
"""
|
"""
|
||||||
# get lab from nested organization object
|
# get lab from nested organization object
|
||||||
logger.debug(f"Converting {self.rsl_plate_num} to dict...")
|
# logger.debug(f"Converting {self.rsl_plate_num} to dict...")
|
||||||
try:
|
try:
|
||||||
sub_lab = self.submitting_lab.name
|
sub_lab = self.submitting_lab.name
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -125,6 +129,7 @@ class BasicSubmission(Base):
|
|||||||
"id": self.id,
|
"id": self.id,
|
||||||
"Plate Number": self.rsl_plate_num,
|
"Plate Number": self.rsl_plate_num,
|
||||||
"Submission Type": self.submission_type_name,
|
"Submission Type": self.submission_type_name,
|
||||||
|
"Submission Category": self.submission_category,
|
||||||
"Submitter Plate Number": self.submitter_plate_num,
|
"Submitter Plate Number": self.submitter_plate_num,
|
||||||
"Submitted Date": self.submitted_date.strftime("%Y-%m-%d"),
|
"Submitted Date": self.submitted_date.strftime("%Y-%m-%d"),
|
||||||
"Submitting Lab": sub_lab,
|
"Submitting Lab": sub_lab,
|
||||||
@@ -232,6 +237,34 @@ class BasicSubmission(Base):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
return output_list
|
return output_list
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||||
|
"""
|
||||||
|
Update submission dictionary with type specific information
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated sample dictionary
|
||||||
|
"""
|
||||||
|
logger.debug(f"Calling {cls.__name__} info parser.")
|
||||||
|
return input_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_samples(cls, input_dict:dict) -> dict:
|
||||||
|
"""
|
||||||
|
Update sample dictionary with type specific information
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated sample dictionary
|
||||||
|
"""
|
||||||
|
logger.debug(f"Called {cls.__name__} sample parser")
|
||||||
|
return input_dict
|
||||||
|
|
||||||
# Below are the custom submission types
|
# Below are the custom submission types
|
||||||
|
|
||||||
@@ -252,7 +285,7 @@ class BacterialCulture(BasicSubmission):
|
|||||||
output = super().to_dict(full_data=full_data)
|
output = super().to_dict(full_data=full_data)
|
||||||
if full_data:
|
if full_data:
|
||||||
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
||||||
return output
|
return output
|
||||||
|
|
||||||
class Wastewater(BasicSubmission):
|
class Wastewater(BasicSubmission):
|
||||||
"""
|
"""
|
||||||
@@ -278,6 +311,23 @@ class Wastewater(BasicSubmission):
|
|||||||
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
|
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||||
|
"""
|
||||||
|
Update submission dictionary with type specific information. Extends parent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated sample dictionary
|
||||||
|
"""
|
||||||
|
input_dict = super().parse_info(input_dict)
|
||||||
|
if xl != None:
|
||||||
|
input_dict['csv'] = xl.parse("Copy to import file")
|
||||||
|
return input_dict
|
||||||
|
|
||||||
|
|
||||||
class WastewaterArtic(BasicSubmission):
|
class WastewaterArtic(BasicSubmission):
|
||||||
"""
|
"""
|
||||||
derivative submission type for artic wastewater
|
derivative submission type for artic wastewater
|
||||||
@@ -303,6 +353,25 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Calculation error: {e}")
|
logger.error(f"Calculation error: {e}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_samples(cls, input_dict: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Update sample dictionary with type specific information. Extends parent.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated sample dictionary
|
||||||
|
"""
|
||||||
|
input_dict = super().parse_samples(input_dict)
|
||||||
|
input_dict['sample_type'] = "Wastewater Sample"
|
||||||
|
# Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
|
||||||
|
# at the end, this has to be done here. No moving to sqlalchemy object :(
|
||||||
|
input_dict['submitter_id'] = re.sub(r"\s\(.+\)$", "", str(input_dict['submitter_id'])).strip()
|
||||||
|
return input_dict
|
||||||
|
|
||||||
|
|
||||||
class BasicSample(Base):
|
class BasicSample(Base):
|
||||||
"""
|
"""
|
||||||
Base of basic sample which polymorphs into BCSample and WWSample
|
Base of basic sample which polymorphs into BCSample and WWSample
|
||||||
@@ -364,26 +433,31 @@ class BasicSample(Base):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: 'well' and sample submitter_id as 'name'
|
dict: 'well' and sample submitter_id as 'name'
|
||||||
"""
|
"""
|
||||||
row_map = {1:"A", 2:"B", 3:"C", 4:"D", 5:"E", 6:"F", 7:"G", 8:"H"}
|
|
||||||
self.assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||||
sample = {}
|
sample = {}
|
||||||
try:
|
try:
|
||||||
sample['well'] = f"{row_map[self.assoc.row]}{self.assoc.column}"
|
sample['well'] = f"{row_map[assoc.row]}{assoc.column}"
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.error(f"Unable to find row {self.assoc.row} in row_map.")
|
logger.error(f"Unable to find row {assoc.row} in row_map.")
|
||||||
sample['well'] = None
|
sample['well'] = None
|
||||||
sample['name'] = self.submitter_id
|
sample['name'] = self.submitter_id
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
def to_hitpick(self, submission_rsl:str|None=None) -> dict|None:
|
def to_hitpick(self, submission_rsl:str|None=None) -> dict|None:
|
||||||
"""
|
"""
|
||||||
Outputs a dictionary of locations
|
Outputs a dictionary usable for html plate maps.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: dictionary of sample id, row and column in elution plate
|
dict: dictionary of sample id, row and column in elution plate
|
||||||
"""
|
"""
|
||||||
# Since there is no PCR, negliable result is necessary.
|
# Since there is no PCR, negliable result is necessary.
|
||||||
return dict(name=self.submitter_id, positive=False)
|
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||||
|
tooltip_text = f"""
|
||||||
|
Sample name: {self.submitter_id}<br>
|
||||||
|
Well: {row_map[assoc.row]}{assoc.column}
|
||||||
|
"""
|
||||||
|
return dict(name=self.submitter_id, positive=False, tooltip=tooltip_text)
|
||||||
|
|
||||||
class WastewaterSample(BasicSample):
|
class WastewaterSample(BasicSample):
|
||||||
"""
|
"""
|
||||||
@@ -445,42 +519,24 @@ class WastewaterSample(BasicSample):
|
|||||||
value = self.submitter_id
|
value = self.submitter_id
|
||||||
super().set_attribute(name, value)
|
super().set_attribute(name, value)
|
||||||
|
|
||||||
|
|
||||||
def to_sub_dict(self, submission_rsl:str) -> dict:
|
|
||||||
"""
|
|
||||||
Gui friendly dictionary. Extends parent method.
|
|
||||||
This version will include PCR status.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
submission_rsl (str): RSL plate number (passed down from the submission.to_dict() functino)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Alphanumeric well id and sample name
|
|
||||||
"""
|
|
||||||
# Get the relevant submission association for this sample
|
|
||||||
sample = super().to_sub_dict(submission_rsl=submission_rsl)
|
|
||||||
# check if PCR data exists.
|
|
||||||
try:
|
|
||||||
check = self.assoc.ct_n1 != None and self.assoc.ct_n2 != None
|
|
||||||
except AttributeError as e:
|
|
||||||
check = False
|
|
||||||
if check:
|
|
||||||
sample['name'] = f"{self.submitter_id}\n\t- ct N1: {'{:.2f}'.format(self.assoc.ct_n1)} ({self.assoc.n1_status})\n\t- ct N2: {'{:.2f}'.format(self.assoc.ct_n2)} ({self.assoc.n2_status})"
|
|
||||||
return sample
|
|
||||||
|
|
||||||
def to_hitpick(self, submission_rsl:str) -> dict|None:
|
def to_hitpick(self, submission_rsl:str) -> dict|None:
|
||||||
"""
|
"""
|
||||||
Outputs a dictionary of locations if sample is positive
|
Outputs a dictionary usable for html plate maps. Extends parent method.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: dictionary of sample id, row and column in elution plate
|
dict: dictionary of sample id, row and column in elution plate
|
||||||
"""
|
"""
|
||||||
sample = super().to_hitpick(submission_rsl=submission_rsl)
|
sample = super().to_hitpick(submission_rsl=submission_rsl)
|
||||||
|
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||||
# if either n1 or n2 is positive, include this sample
|
# if either n1 or n2 is positive, include this sample
|
||||||
try:
|
try:
|
||||||
sample['positive'] = any(["positive" in item for item in [self.assoc.n1_status, self.assoc.n2_status]])
|
sample['positive'] = any(["positive" in item for item in [assoc.n1_status, assoc.n2_status]])
|
||||||
except (TypeError, AttributeError) as e:
|
except (TypeError, AttributeError) as e:
|
||||||
logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
|
logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
|
||||||
|
try:
|
||||||
|
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(assoc.ct_n1)} ({assoc.n1_status})<br>- ct N2: {'{:.2f}'.format(assoc.ct_n2)} ({assoc.n2_status})"
|
||||||
|
except (TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
class BacterialCultureSample(BasicSample):
|
class BacterialCultureSample(BasicSample):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pprint
|
|||||||
from typing import List
|
from typing import List
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from backend.db import models, lookup_kit_types, lookup_submission_type, lookup_samples
|
from backend.db import models, lookup_kit_types, lookup_submission_type, lookup_samples, get_polymorphic_subclass
|
||||||
from backend.pydant import PydSubmission, PydReagent
|
from backend.pydant import PydSubmission, PydReagent
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
@@ -91,12 +91,11 @@ class SheetParser(object):
|
|||||||
Pulls basic information from the excel sheet
|
Pulls basic information from the excel sheet
|
||||||
"""
|
"""
|
||||||
info = InfoParser(ctx=self.ctx, xl=self.xl, submission_type=self.sub['submission_type']['value']).parse_info()
|
info = InfoParser(ctx=self.ctx, xl=self.xl, submission_type=self.sub['submission_type']['value']).parse_info()
|
||||||
parser_query = f"parse_{self.sub['submission_type']['value'].replace(' ', '_').lower()}"
|
# parser_query = f"parse_{self.sub['submission_type']['value'].replace(' ', '_').lower()}"
|
||||||
try:
|
# custom_parser = getattr(self, parser_query)
|
||||||
custom_parser = getattr(self, parser_query)
|
|
||||||
info = custom_parser(info)
|
# except AttributeError:
|
||||||
except AttributeError:
|
# logger.error(f"Couldn't find submission parser: {parser_query}")
|
||||||
logger.error(f"Couldn't find submission parser: {parser_query}")
|
|
||||||
for k,v in info.items():
|
for k,v in info.items():
|
||||||
match k:
|
match k:
|
||||||
case "sample":
|
case "sample":
|
||||||
@@ -120,41 +119,41 @@ class SheetParser(object):
|
|||||||
"""
|
"""
|
||||||
self.sample_result, self.sub['samples'] = SampleParser(ctx=self.ctx, xl=self.xl, submission_type=self.sub['submission_type']['value']).parse_samples()
|
self.sample_result, self.sub['samples'] = SampleParser(ctx=self.ctx, xl=self.xl, submission_type=self.sub['submission_type']['value']).parse_samples()
|
||||||
|
|
||||||
def parse_bacterial_culture(self, input_dict) -> dict:
|
# def parse_bacterial_culture(self, input_dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update submission dictionary with type specific information
|
# Update submission dictionary with type specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
def parse_wastewater(self, input_dict) -> dict:
|
# def parse_wastewater(self, input_dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update submission dictionary with type specific information
|
# Update submission dictionary with type specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
def parse_wastewater_artic(self, input_dict:dict) -> dict:
|
# def parse_wastewater_artic(self, input_dict:dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update submission dictionary with type specific information
|
# Update submission dictionary with type specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
|
|
||||||
def import_kit_validation_check(self):
|
def import_kit_validation_check(self):
|
||||||
@@ -206,6 +205,7 @@ class InfoParser(object):
|
|||||||
self.map = self.fetch_submission_info_map(submission_type=submission_type)
|
self.map = self.fetch_submission_info_map(submission_type=submission_type)
|
||||||
self.xl = xl
|
self.xl = xl
|
||||||
logger.debug(f"Info map for InfoParser: {pprint.pformat(self.map)}")
|
logger.debug(f"Info map for InfoParser: {pprint.pformat(self.map)}")
|
||||||
|
|
||||||
|
|
||||||
def fetch_submission_info_map(self, submission_type:str|dict) -> dict:
|
def fetch_submission_info_map(self, submission_type:str|dict) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -223,6 +223,8 @@ class InfoParser(object):
|
|||||||
# submission_type = lookup_submissiontype_by_name(ctx=self.ctx, type_name=submission_type['value'])
|
# submission_type = lookup_submissiontype_by_name(ctx=self.ctx, type_name=submission_type['value'])
|
||||||
submission_type = lookup_submission_type(ctx=self.ctx, name=submission_type['value'])
|
submission_type = lookup_submission_type(ctx=self.ctx, name=submission_type['value'])
|
||||||
info_map = submission_type.info_map
|
info_map = submission_type.info_map
|
||||||
|
# Get the parse_info method from the submission type specified
|
||||||
|
self.custom_parser = get_polymorphic_subclass(models.BasicSubmission, submission_type.name).parse_info
|
||||||
return info_map
|
return info_map
|
||||||
|
|
||||||
def parse_info(self) -> dict:
|
def parse_info(self) -> dict:
|
||||||
@@ -263,7 +265,13 @@ class InfoParser(object):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
dicto[item] = dict(value=convert_nans_to_nones(value), parsed=False)
|
dicto[item] = dict(value=convert_nans_to_nones(value), parsed=False)
|
||||||
return dicto
|
try:
|
||||||
|
check = dicto['submission_category'] not in ["", None]
|
||||||
|
except KeyError:
|
||||||
|
check = False
|
||||||
|
return self.custom_parser(input_dict=dicto, xl=self.xl)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ReagentParser(object):
|
class ReagentParser(object):
|
||||||
|
|
||||||
@@ -351,6 +359,7 @@ class SampleParser(object):
|
|||||||
submission_type = lookup_submission_type(ctx=self.ctx, name=submission_type)
|
submission_type = lookup_submission_type(ctx=self.ctx, name=submission_type)
|
||||||
logger.debug(f"info_map: {pprint.pformat(submission_type.info_map)}")
|
logger.debug(f"info_map: {pprint.pformat(submission_type.info_map)}")
|
||||||
sample_info_map = submission_type.info_map['samples']
|
sample_info_map = submission_type.info_map['samples']
|
||||||
|
self.custom_parser = get_polymorphic_subclass(models.BasicSubmission, submission_type.name).parse_samples
|
||||||
return sample_info_map
|
return sample_info_map
|
||||||
|
|
||||||
def construct_plate_map(self, plate_map_location:dict) -> pd.DataFrame:
|
def construct_plate_map(self, plate_map_location:dict) -> pd.DataFrame:
|
||||||
@@ -473,12 +482,12 @@ class SampleParser(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
translated_dict[k] = convert_nans_to_nones(v)
|
translated_dict[k] = convert_nans_to_nones(v)
|
||||||
translated_dict['sample_type'] = f"{self.submission_type} Sample"
|
translated_dict['sample_type'] = f"{self.submission_type} Sample"
|
||||||
parser_query = f"parse_{translated_dict['sample_type'].replace(' ', '_').lower()}"
|
# parser_query = f"parse_{translated_dict['sample_type'].replace(' ', '_').lower()}"
|
||||||
try:
|
# try:
|
||||||
custom_parser = getattr(self, parser_query)
|
# custom_parser = getattr(self, parser_query)
|
||||||
translated_dict = custom_parser(translated_dict)
|
translated_dict = self.custom_parser(translated_dict)
|
||||||
except AttributeError:
|
# except AttributeError:
|
||||||
logger.error(f"Couldn't get custom parser: {parser_query}")
|
# logger.error(f"Couldn't get custom parser: {parser_query}")
|
||||||
if generate:
|
if generate:
|
||||||
new_samples.append(self.generate_sample_object(translated_dict))
|
new_samples.append(self.generate_sample_object(translated_dict))
|
||||||
else:
|
else:
|
||||||
@@ -502,7 +511,7 @@ class SampleParser(object):
|
|||||||
logger.error(f"Could not find the model {query}. Using generic.")
|
logger.error(f"Could not find the model {query}. Using generic.")
|
||||||
database_obj = models.BasicSample
|
database_obj = models.BasicSample
|
||||||
logger.debug(f"Searching database for {input_dict['submitter_id']}...")
|
logger.debug(f"Searching database for {input_dict['submitter_id']}...")
|
||||||
instance = lookup_samples(ctx=self.ctx, submitter_id=input_dict['submitter_id'])
|
instance = lookup_samples(ctx=self.ctx, submitter_id=str(input_dict['submitter_id']))
|
||||||
if instance == None:
|
if instance == None:
|
||||||
logger.debug(f"Couldn't find sample {input_dict['submitter_id']}. Creating new sample.")
|
logger.debug(f"Couldn't find sample {input_dict['submitter_id']}. Creating new sample.")
|
||||||
instance = database_obj()
|
instance = database_obj()
|
||||||
@@ -516,63 +525,63 @@ class SampleParser(object):
|
|||||||
return dict(sample=instance, row=input_dict['row'], column=input_dict['column'])
|
return dict(sample=instance, row=input_dict['row'], column=input_dict['column'])
|
||||||
|
|
||||||
|
|
||||||
def parse_bacterial_culture_sample(self, input_dict:dict) -> dict:
|
# def parse_bacterial_culture_sample(self, input_dict:dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update sample dictionary with bacterial culture specific information
|
# Update sample dictionary with bacterial culture specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
logger.debug("Called bacterial culture sample parser")
|
# logger.debug("Called bacterial culture sample parser")
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
def parse_wastewater_sample(self, input_dict:dict) -> dict:
|
# def parse_wastewater_sample(self, input_dict:dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update sample dictionary with wastewater specific information
|
# Update sample dictionary with wastewater specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
logger.debug(f"Called wastewater sample parser")
|
# logger.debug(f"Called wastewater sample parser")
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
def parse_wastewater_artic_sample(self, input_dict:dict) -> dict:
|
# def parse_wastewater_artic_sample(self, input_dict:dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update sample dictionary with artic specific information
|
# Update sample dictionary with artic specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
logger.debug("Called wastewater artic sample parser")
|
# logger.debug("Called wastewater artic sample parser")
|
||||||
input_dict['sample_type'] = "Wastewater Sample"
|
# input_dict['sample_type'] = "Wastewater Sample"
|
||||||
# Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
|
# # Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
|
||||||
# at the end, this has to be done here. No moving to sqlalchemy object :(
|
# # at the end, this has to be done here. No moving to sqlalchemy object :(
|
||||||
input_dict['submitter_id'] = re.sub(r"\s\(.+\)$", "", str(input_dict['submitter_id'])).strip()
|
# input_dict['submitter_id'] = re.sub(r"\s\(.+\)$", "", str(input_dict['submitter_id'])).strip()
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
def parse_first_strand_sample(self, input_dict:dict) -> dict:
|
# def parse_first_strand_sample(self, input_dict:dict) -> dict:
|
||||||
"""
|
# """
|
||||||
Update sample dictionary with first strand specific information
|
# Update sample dictionary with first strand specific information
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
input_dict (dict): Input sample dictionary
|
# input_dict (dict): Input sample dictionary
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
dict: Updated sample dictionary
|
# dict: Updated sample dictionary
|
||||||
"""
|
# """
|
||||||
logger.debug("Called first strand sample parser")
|
# logger.debug("Called first strand sample parser")
|
||||||
input_dict['well'] = re.search(r"\s\((.*)\)$", input_dict['submitter_id']).groups()[0]
|
# input_dict['well'] = re.search(r"\s\((.*)\)$", input_dict['submitter_id']).groups()[0]
|
||||||
input_dict['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(input_dict['submitter_id'])).strip()
|
# input_dict['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(input_dict['submitter_id'])).strip()
|
||||||
return input_dict
|
# return input_dict
|
||||||
|
|
||||||
def grab_plates(self) -> List[str]:
|
def grab_plates(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
|||||||
|
|
||||||
env = jinja_template_loading()
|
env = jinja_template_loading()
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
|
||||||
|
|
||||||
def make_report_xlsx(records:list[dict]) -> Tuple[DataFrame, DataFrame]:
|
def make_report_xlsx(records:list[dict]) -> Tuple[DataFrame, DataFrame]:
|
||||||
"""
|
"""
|
||||||
create the dataframe for a report
|
create the dataframe for a report
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class PydSubmission(BaseModel, extra=Extra.allow):
|
|||||||
sample_count: dict|None
|
sample_count: dict|None
|
||||||
extraction_kit: dict|None
|
extraction_kit: dict|None
|
||||||
technician: dict|None
|
technician: dict|None
|
||||||
|
submission_category: dict|None = Field(default=dict(value=None, parsed=False), validate_default=True)
|
||||||
reagents: List[dict] = []
|
reagents: List[dict] = []
|
||||||
samples: List[Any]
|
samples: List[Any]
|
||||||
|
|
||||||
@@ -205,3 +206,11 @@ class PydSubmission(BaseModel, extra=Extra.allow):
|
|||||||
return dict(value=value, parsed=True)
|
return dict(value=value, parsed=True)
|
||||||
else:
|
else:
|
||||||
return dict(value=RSLNamer(ctx=values.data['ctx'], instr=values.data['filepath'].__str__()).submission_type.title(), parsed=False)
|
return dict(value=RSLNamer(ctx=values.data['ctx'], instr=values.data['filepath'].__str__()).submission_type.title(), parsed=False)
|
||||||
|
|
||||||
|
@field_validator("submission_category")
|
||||||
|
@classmethod
|
||||||
|
def rescue_category(cls, value, values):
|
||||||
|
if value['value'] not in ["Research", "Diagnostic", "Surveillance"]:
|
||||||
|
value['value'] = values.data['submission_type']['value']
|
||||||
|
return value
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
'''
|
'''
|
||||||
Constructs main application.
|
Constructs main application.
|
||||||
'''
|
'''
|
||||||
|
from pprint import pformat
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Tuple
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QMainWindow, QToolBar,
|
QMainWindow, QToolBar,
|
||||||
QTabWidget, QWidget, QVBoxLayout,
|
QTabWidget, QWidget, QVBoxLayout,
|
||||||
QComboBox, QHBoxLayout,
|
QComboBox, QHBoxLayout,
|
||||||
QScrollArea
|
QScrollArea, QLineEdit, QDateEdit,
|
||||||
|
QSpinBox
|
||||||
)
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QAction
|
from PyQt6.QtGui import QAction
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from backend.db import (
|
from backend.db import (
|
||||||
construct_reagent, store_object, lookup_control_types, lookup_modes
|
construct_reagent, store_object, lookup_control_types, lookup_modes
|
||||||
)
|
)
|
||||||
from .all_window_functions import extract_form_info
|
# from .all_window_functions import extract_form_info
|
||||||
from tools import check_if_app, Settings
|
from tools import check_if_app, Settings
|
||||||
from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker
|
from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker, ImportReagent
|
||||||
import logging
|
import logging
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -51,7 +55,9 @@ class App(QMainWindow):
|
|||||||
self._createToolBar()
|
self._createToolBar()
|
||||||
self._connectActions()
|
self._connectActions()
|
||||||
self._controls_getter()
|
self._controls_getter()
|
||||||
|
# self.status_bar = self.statusBar()
|
||||||
self.show()
|
self.show()
|
||||||
|
self.statusBar().showMessage('Ready', 5000)
|
||||||
|
|
||||||
|
|
||||||
def _createMenuBar(self):
|
def _createMenuBar(self):
|
||||||
@@ -73,7 +79,7 @@ class App(QMainWindow):
|
|||||||
fileMenu.addAction(self.importPCRAction)
|
fileMenu.addAction(self.importPCRAction)
|
||||||
methodsMenu.addAction(self.constructFS)
|
methodsMenu.addAction(self.constructFS)
|
||||||
reportMenu.addAction(self.generateReportAction)
|
reportMenu.addAction(self.generateReportAction)
|
||||||
maintenanceMenu.addAction(self.joinControlsAction)
|
# maintenanceMenu.addAction(self.joinControlsAction)
|
||||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||||
maintenanceMenu.addAction(self.joinPCRAction)
|
maintenanceMenu.addAction(self.joinPCRAction)
|
||||||
|
|
||||||
@@ -99,7 +105,7 @@ class App(QMainWindow):
|
|||||||
self.generateReportAction = QAction("Make Report", self)
|
self.generateReportAction = QAction("Make Report", self)
|
||||||
self.addKitAction = QAction("Import Kit", self)
|
self.addKitAction = QAction("Import Kit", self)
|
||||||
self.addOrgAction = QAction("Import Org", self)
|
self.addOrgAction = QAction("Import Org", self)
|
||||||
self.joinControlsAction = QAction("Link Controls")
|
# self.joinControlsAction = QAction("Link Controls")
|
||||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||||
self.joinPCRAction = QAction("Link PCR Logs")
|
self.joinPCRAction = QAction("Link PCR Logs")
|
||||||
self.helpAction = QAction("&About", self)
|
self.helpAction = QAction("&About", self)
|
||||||
@@ -122,7 +128,7 @@ class App(QMainWindow):
|
|||||||
self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter)
|
self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter)
|
||||||
self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter)
|
self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter)
|
||||||
self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
|
self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
|
||||||
self.joinControlsAction.triggered.connect(self.linkControls)
|
# self.joinControlsAction.triggered.connect(self.linkControls)
|
||||||
self.joinExtractionAction.triggered.connect(self.linkExtractions)
|
self.joinExtractionAction.triggered.connect(self.linkExtractions)
|
||||||
self.joinPCRAction.triggered.connect(self.linkPCR)
|
self.joinPCRAction.triggered.connect(self.linkPCR)
|
||||||
self.helpAction.triggered.connect(self.showAbout)
|
self.helpAction.triggered.connect(self.showAbout)
|
||||||
@@ -149,6 +155,7 @@ class App(QMainWindow):
|
|||||||
webbrowser.get('windows-default').open(f"file://{url.__str__()}")
|
webbrowser.get('windows-default').open(f"file://{url.__str__()}")
|
||||||
|
|
||||||
def result_reporter(self, result:dict|None=None):
|
def result_reporter(self, result:dict|None=None):
|
||||||
|
# def result_reporter(self, result:TypedDict[]|None=None):
|
||||||
"""
|
"""
|
||||||
Report any anomolous results - if any - to the user
|
Report any anomolous results - if any - to the user
|
||||||
|
|
||||||
@@ -158,6 +165,8 @@ class App(QMainWindow):
|
|||||||
if result != None:
|
if result != None:
|
||||||
msg = AlertPop(message=result['message'], status=result['status'])
|
msg = AlertPop(message=result['message'], status=result['status'])
|
||||||
msg.exec()
|
msg.exec()
|
||||||
|
else:
|
||||||
|
self.statusBar().showMessage("Action completed sucessfully.", 5000)
|
||||||
|
|
||||||
def importSubmission(self):
|
def importSubmission(self):
|
||||||
"""
|
"""
|
||||||
@@ -211,13 +220,15 @@ class App(QMainWindow):
|
|||||||
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
|
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
# extract form info
|
# extract form info
|
||||||
info = extract_form_info(dlg)
|
# info = extract_form_info(dlg)
|
||||||
|
info = dlg.parse_form()
|
||||||
logger.debug(f"Reagent info: {info}")
|
logger.debug(f"Reagent info: {info}")
|
||||||
# create reagent object
|
# create reagent object
|
||||||
reagent = construct_reagent(ctx=self.ctx, info_dict=info)
|
reagent = construct_reagent(ctx=self.ctx, info_dict=info)
|
||||||
# send reagent to db
|
# send reagent to db
|
||||||
# store_reagent(ctx=self.ctx, reagent=reagent)
|
# store_reagent(ctx=self.ctx, reagent=reagent)
|
||||||
result = store_object(ctx=self.ctx, object=reagent)
|
result = store_object(ctx=self.ctx, object=reagent)
|
||||||
|
self.result_reporter(result=result)
|
||||||
return reagent
|
return reagent
|
||||||
|
|
||||||
def generateReport(self):
|
def generateReport(self):
|
||||||
@@ -263,6 +274,7 @@ class App(QMainWindow):
|
|||||||
def linkControls(self):
|
def linkControls(self):
|
||||||
"""
|
"""
|
||||||
Adds controls pulled from irida to relevant submissions
|
Adds controls pulled from irida to relevant submissions
|
||||||
|
NOTE: Depreciated due to improvements in controls scraper.
|
||||||
"""
|
"""
|
||||||
from .main_window_functions import link_controls_function
|
from .main_window_functions import link_controls_function
|
||||||
self, result = link_controls_function(self)
|
self, result = link_controls_function(self)
|
||||||
@@ -327,7 +339,7 @@ class AddSubForm(QWidget):
|
|||||||
self.tabs.addTab(self.tab2,"Controls")
|
self.tabs.addTab(self.tab2,"Controls")
|
||||||
self.tabs.addTab(self.tab3, "Add Kit")
|
self.tabs.addTab(self.tab3, "Add Kit")
|
||||||
# Create submission adder form
|
# Create submission adder form
|
||||||
self.formwidget = QWidget(self)
|
self.formwidget = SubmissionFormWidget(self)
|
||||||
self.formlayout = QVBoxLayout(self)
|
self.formlayout = QVBoxLayout(self)
|
||||||
self.formwidget.setLayout(self.formlayout)
|
self.formwidget.setLayout(self.formlayout)
|
||||||
self.formwidget.setFixedWidth(300)
|
self.formwidget.setFixedWidth(300)
|
||||||
@@ -381,3 +393,32 @@ class AddSubForm(QWidget):
|
|||||||
self.layout.addWidget(self.tabs)
|
self.layout.addWidget(self.tabs)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
class SubmissionFormWidget(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent: QWidget) -> None:
|
||||||
|
logger.debug(f"Setting form widget...")
|
||||||
|
super().__init__(parent)
|
||||||
|
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||||
|
"qt_scrollarea_vcontainer", "submit_btn"
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse_form(self) -> Tuple[dict, list]:
|
||||||
|
logger.debug(f"Hello from parser!")
|
||||||
|
info = {}
|
||||||
|
reagents = []
|
||||||
|
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
|
||||||
|
for widget in widgets:
|
||||||
|
logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)}")
|
||||||
|
match widget:
|
||||||
|
case ImportReagent():
|
||||||
|
reagents.append(dict(name=widget.objectName().replace("lot_", ""), lot=widget.currentText()))
|
||||||
|
case QLineEdit():
|
||||||
|
info[widget.objectName()] = widget.text()
|
||||||
|
case QComboBox():
|
||||||
|
info[widget.objectName()] = widget.currentText()
|
||||||
|
case QDateEdit():
|
||||||
|
info[widget.objectName()] = widget.date().toPyDate()
|
||||||
|
logger.debug(f"Info: {pformat(info)}")
|
||||||
|
logger.debug(f"Reagents: {pformat(reagents)}")
|
||||||
|
return info, reagents
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:
|
|||||||
def extract_form_info(object) -> dict:
|
def extract_form_info(object) -> dict:
|
||||||
"""
|
"""
|
||||||
retrieves object names and values from form
|
retrieves object names and values from form
|
||||||
|
DEPRECIATED. Replaced by individual form parser methods.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
object (_type_): the form widget
|
object (_type_): the form widget
|
||||||
@@ -64,7 +65,7 @@ def extract_form_info(object) -> dict:
|
|||||||
|
|
||||||
from frontend.custom_widgets import ReagentTypeForm
|
from frontend.custom_widgets import ReagentTypeForm
|
||||||
dicto = {}
|
dicto = {}
|
||||||
reagents = {}
|
reagents = []
|
||||||
logger.debug(f"Object type: {type(object)}")
|
logger.debug(f"Object type: {type(object)}")
|
||||||
# grab all widgets in form
|
# grab all widgets in form
|
||||||
try:
|
try:
|
||||||
@@ -85,8 +86,17 @@ def extract_form_info(object) -> dict:
|
|||||||
case ReagentTypeForm():
|
case ReagentTypeForm():
|
||||||
reagent = extract_form_info(item)
|
reagent = extract_form_info(item)
|
||||||
logger.debug(f"Reagent found: {reagent}")
|
logger.debug(f"Reagent found: {reagent}")
|
||||||
reagents[reagent["name"].strip()] = {'eol_ext':int(reagent['eol'])}
|
if isinstance(reagent, tuple):
|
||||||
|
reagent = reagent[0]
|
||||||
|
# reagents[reagent["name"].strip()] = {'eol':int(reagent['eol'])}
|
||||||
|
reagents.append({k:v for k,v in reagent.items() if k not in ['', 'qt_spinbox_lineedit']})
|
||||||
# value for ad hoc check above
|
# value for ad hoc check above
|
||||||
|
if isinstance(dicto, tuple):
|
||||||
|
logger.warning(f"Got tuple for dicto for some reason.")
|
||||||
|
dicto = dicto[0]
|
||||||
|
if isinstance(reagents, tuple):
|
||||||
|
logger.warning(f"Got tuple for reagents for some reason.")
|
||||||
|
reagents = reagents[0]
|
||||||
if reagents != {}:
|
if reagents != {}:
|
||||||
return dicto, reagents
|
return dicto, reagents
|
||||||
return dicto
|
return dicto
|
||||||
|
|||||||
@@ -2,22 +2,25 @@
|
|||||||
Contains miscellaneous widgets for frontend functions
|
Contains miscellaneous widgets for frontend functions
|
||||||
'''
|
'''
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from pprint import pformat
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QVBoxLayout,
|
QLabel, QVBoxLayout,
|
||||||
QLineEdit, QComboBox, QDialog,
|
QLineEdit, QComboBox, QDialog,
|
||||||
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
|
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
|
||||||
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
|
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
|
||||||
QHBoxLayout
|
QHBoxLayout, QScrollArea
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QDate, QSize
|
from PyQt6.QtCore import Qt, QDate, QSize
|
||||||
from tools import check_not_nan, jinja_template_loading, Settings
|
from tools import check_not_nan, jinja_template_loading, Settings
|
||||||
from ..all_window_functions import extract_form_info
|
from backend.db.functions import construct_kit_from_yaml, \
|
||||||
from backend.db import construct_kit_from_yaml, \
|
|
||||||
lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association
|
lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association
|
||||||
|
from backend.db.models import SubmissionTypeKitTypeAssociation
|
||||||
|
from sqlalchemy import FLOAT, INTEGER, String
|
||||||
import logging
|
import logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .pop_ups import AlertPop
|
from .pop_ups import AlertPop
|
||||||
from backend.pydant import PydReagent
|
from backend.pydant import PydReagent
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -84,6 +87,12 @@ class AddReagentForm(QDialog):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
self.type_input.currentTextChanged.connect(self.update_names)
|
self.type_input.currentTextChanged.connect(self.update_names)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
return dict(name=self.name_input.currentText(),
|
||||||
|
lot=self.lot_input.text(),
|
||||||
|
expiry=self.exp_input.date().toPyDate(),
|
||||||
|
type=self.type_input.currentText())
|
||||||
|
|
||||||
def update_names(self):
|
def update_names(self):
|
||||||
"""
|
"""
|
||||||
Updates reagent names form field with examples from reagent type
|
Updates reagent names form field with examples from reagent type
|
||||||
@@ -121,6 +130,9 @@ class ReportDatePicker(QDialog):
|
|||||||
self.layout.addWidget(self.buttonBox)
|
self.layout.addWidget(self.buttonBox)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
||||||
|
|
||||||
class KitAdder(QWidget):
|
class KitAdder(QWidget):
|
||||||
"""
|
"""
|
||||||
dialog to get information to add kit
|
dialog to get information to add kit
|
||||||
@@ -128,8 +140,14 @@ class KitAdder(QWidget):
|
|||||||
def __init__(self, parent_ctx:Settings) -> None:
|
def __init__(self, parent_ctx:Settings) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ctx = parent_ctx
|
self.ctx = parent_ctx
|
||||||
|
main_box = QVBoxLayout(self)
|
||||||
|
scroll = QScrollArea(self)
|
||||||
|
main_box.addWidget(scroll)
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scrollContent = QWidget(scroll)
|
||||||
self.grid = QGridLayout()
|
self.grid = QGridLayout()
|
||||||
self.setLayout(self.grid)
|
# self.setLayout(self.grid)
|
||||||
|
scrollContent.setLayout(self.grid)
|
||||||
# insert submit button at top
|
# insert submit button at top
|
||||||
self.submit_btn = QPushButton("Submit")
|
self.submit_btn = QPushButton("Submit")
|
||||||
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
||||||
@@ -138,42 +156,65 @@ class KitAdder(QWidget):
|
|||||||
kit_name = QLineEdit()
|
kit_name = QLineEdit()
|
||||||
kit_name.setObjectName("kit_name")
|
kit_name.setObjectName("kit_name")
|
||||||
self.grid.addWidget(kit_name,2,1)
|
self.grid.addWidget(kit_name,2,1)
|
||||||
self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
|
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||||
# widget to get uses of kit
|
# widget to get uses of kit
|
||||||
used_for = QComboBox()
|
used_for = QComboBox()
|
||||||
used_for.setObjectName("used_for")
|
used_for.setObjectName("used_for")
|
||||||
# Insert all existing sample types
|
# Insert all existing sample types
|
||||||
# used_for.addItems(lookup_all_sample_types(ctx=parent_ctx))
|
|
||||||
used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
|
used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
|
||||||
used_for.setEditable(True)
|
used_for.setEditable(True)
|
||||||
self.grid.addWidget(used_for,3,1)
|
self.grid.addWidget(used_for,3,1)
|
||||||
# set cost per run
|
# Get all fields in SubmissionTypeKitTypeAssociation
|
||||||
self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
|
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
|
||||||
# widget to get constant cost
|
for iii, column in enumerate(self.columns):
|
||||||
const_cost = QDoubleSpinBox() #QSpinBox()
|
idx = iii + 4
|
||||||
const_cost.setObjectName("const_cost")
|
# convert field name to human readable.
|
||||||
const_cost.setMinimum(0)
|
field_name = column.name.replace("_", " ").title()
|
||||||
const_cost.setMaximum(9999)
|
self.grid.addWidget(QLabel(field_name),idx,0)
|
||||||
self.grid.addWidget(const_cost,4,1)
|
match column.type:
|
||||||
self.grid.addWidget(QLabel("Cost per column (multidrop reagents, etc.):"),5,0)
|
case FLOAT():
|
||||||
# widget to get mutable costs per column
|
add_widget = QDoubleSpinBox()
|
||||||
mut_cost_col = QDoubleSpinBox() #QSpinBox()
|
add_widget.setMinimum(0)
|
||||||
mut_cost_col.setObjectName("mut_cost_col")
|
add_widget.setMaximum(9999)
|
||||||
mut_cost_col.setMinimum(0)
|
case INTEGER():
|
||||||
mut_cost_col.setMaximum(9999)
|
add_widget = QSpinBox()
|
||||||
self.grid.addWidget(mut_cost_col,5,1)
|
add_widget.setMinimum(0)
|
||||||
self.grid.addWidget(QLabel("Cost per sample (tips, reagents, etc.):"),6,0)
|
add_widget.setMaximum(9999)
|
||||||
# widget to get mutable costs per column
|
case _:
|
||||||
mut_cost_samp = QDoubleSpinBox() #QSpinBox()
|
add_widget = QLineEdit()
|
||||||
mut_cost_samp.setObjectName("mut_cost_samp")
|
add_widget.setObjectName(column.name)
|
||||||
mut_cost_samp.setMinimum(0)
|
self.grid.addWidget(add_widget, idx,1)
|
||||||
mut_cost_samp.setMaximum(9999)
|
# self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
|
||||||
self.grid.addWidget(mut_cost_samp,6,1)
|
# # widget to get constant cost
|
||||||
|
# const_cost = QDoubleSpinBox() #QSpinBox()
|
||||||
|
# const_cost.setObjectName("const_cost")
|
||||||
|
# const_cost.setMinimum(0)
|
||||||
|
# const_cost.setMaximum(9999)
|
||||||
|
# self.grid.addWidget(const_cost,4,1)
|
||||||
|
# self.grid.addWidget(QLabel("Cost per column (multidrop reagents, etc.):"),5,0)
|
||||||
|
# # widget to get mutable costs per column
|
||||||
|
# mut_cost_col = QDoubleSpinBox() #QSpinBox()
|
||||||
|
# mut_cost_col.setObjectName("mut_cost_col")
|
||||||
|
# mut_cost_col.setMinimum(0)
|
||||||
|
# mut_cost_col.setMaximum(9999)
|
||||||
|
# self.grid.addWidget(mut_cost_col,5,1)
|
||||||
|
# self.grid.addWidget(QLabel("Cost per sample (tips, reagents, etc.):"),6,0)
|
||||||
|
# # widget to get mutable costs per column
|
||||||
|
# mut_cost_samp = QDoubleSpinBox() #QSpinBox()
|
||||||
|
# mut_cost_samp.setObjectName("mut_cost_samp")
|
||||||
|
# mut_cost_samp.setMinimum(0)
|
||||||
|
# mut_cost_samp.setMaximum(9999)
|
||||||
|
# self.grid.addWidget(mut_cost_samp,6,1)
|
||||||
# button to add additional reagent types
|
# button to add additional reagent types
|
||||||
self.add_RT_btn = QPushButton("Add Reagent Type")
|
self.add_RT_btn = QPushButton("Add Reagent Type")
|
||||||
self.grid.addWidget(self.add_RT_btn)
|
self.grid.addWidget(self.add_RT_btn)
|
||||||
self.add_RT_btn.clicked.connect(self.add_RT)
|
self.add_RT_btn.clicked.connect(self.add_RT)
|
||||||
self.submit_btn.clicked.connect(self.submit)
|
self.submit_btn.clicked.connect(self.submit)
|
||||||
|
scroll.setWidget(scrollContent)
|
||||||
|
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||||
|
"qt_scrollarea_vcontainer", "submit_btn"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def add_RT(self) -> None:
|
def add_RT(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -181,9 +222,11 @@ class KitAdder(QWidget):
|
|||||||
"""
|
"""
|
||||||
# get bottommost row
|
# get bottommost row
|
||||||
maxrow = self.grid.rowCount()
|
maxrow = self.grid.rowCount()
|
||||||
reg_form = ReagentTypeForm(parent_ctx=self.ctx)
|
reg_form = ReagentTypeForm(ctx=self.ctx)
|
||||||
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||||
self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||||
|
self.grid.addWidget(reg_form, maxrow,0,1,4)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def submit(self) -> None:
|
def submit(self) -> None:
|
||||||
@@ -191,40 +234,62 @@ class KitAdder(QWidget):
|
|||||||
send kit to database
|
send kit to database
|
||||||
"""
|
"""
|
||||||
# get form info
|
# get form info
|
||||||
info, reagents = extract_form_info(self)
|
info, reagents = self.parse_form()
|
||||||
logger.debug(f"kit info: {info}")
|
# info, reagents = extract_form_info(self)
|
||||||
yml_type = {}
|
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
|
||||||
try:
|
logger.debug(f"kit info: {pformat(info)}")
|
||||||
yml_type['password'] = info['password']
|
logger.debug(f"kit reagents: {pformat(reagents)}")
|
||||||
except KeyError:
|
info['reagent_types'] = reagents
|
||||||
pass
|
# for reagent in reagents:
|
||||||
used = info['used_for']
|
# new_dict = {}
|
||||||
yml_type[used] = {}
|
# for k,v in reagent.items():
|
||||||
yml_type[used]['kits'] = {}
|
# if "_" in k:
|
||||||
yml_type[used]['kits'][info['kit_name']] = {}
|
# key, sub_key = k.split("_")
|
||||||
yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["const_cost"]
|
# if key not in new_dict.keys():
|
||||||
yml_type[used]['kits'][info['kit_name']]['mutable_cost_column'] = info["mut_cost_col"]
|
# new_dict[key] = {}
|
||||||
yml_type[used]['kits'][info['kit_name']]['mutable_cost_sample'] = info["mut_cost_samp"]
|
# logger.debug(f"Adding key {key}, {sub_key} and value {v} to {new_dict}")
|
||||||
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
|
# new_dict[key][sub_key] = v
|
||||||
logger.debug(yml_type)
|
# else:
|
||||||
|
# new_dict[k] = v
|
||||||
|
# info['reagent_types'].append(new_dict)
|
||||||
|
logger.debug(pformat(info))
|
||||||
# send to kit constructor
|
# send to kit constructor
|
||||||
result = construct_kit_from_yaml(ctx=self.ctx, exp=yml_type)
|
result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
|
||||||
msg = AlertPop(message=result['message'], status=result['status'])
|
msg = AlertPop(message=result['message'], status=result['status'])
|
||||||
msg.exec()
|
msg.exec()
|
||||||
self.__init__(self.ctx)
|
self.__init__(self.ctx)
|
||||||
|
|
||||||
|
def parse_form(self) -> Tuple[dict, list]:
|
||||||
|
logger.debug(f"Hello from {self.__class__} parser!")
|
||||||
|
info = {}
|
||||||
|
reagents = []
|
||||||
|
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
|
||||||
|
for widget in widgets:
|
||||||
|
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||||
|
match widget:
|
||||||
|
case ReagentTypeForm():
|
||||||
|
reagents.append(widget.parse_form())
|
||||||
|
case QLineEdit():
|
||||||
|
info[widget.objectName()] = widget.text()
|
||||||
|
case QComboBox():
|
||||||
|
info[widget.objectName()] = widget.currentText()
|
||||||
|
case QDateEdit():
|
||||||
|
info[widget.objectName()] = widget.date().toPyDate()
|
||||||
|
return info, reagents
|
||||||
|
|
||||||
|
|
||||||
class ReagentTypeForm(QWidget):
|
class ReagentTypeForm(QWidget):
|
||||||
"""
|
"""
|
||||||
custom widget to add information about a new reagenttype
|
custom widget to add information about a new reagenttype
|
||||||
"""
|
"""
|
||||||
def __init__(self, ctx:dict) -> None:
|
def __init__(self, ctx:Settings) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
self.setLayout(grid)
|
self.setLayout(grid)
|
||||||
grid.addWidget(QLabel("Name (*Exactly* as it appears in the excel submission form):"),0,0)
|
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||||
# Widget to get reagent info
|
# Widget to get reagent info
|
||||||
self.reagent_getter = QComboBox()
|
self.reagent_getter = QComboBox()
|
||||||
self.reagent_getter.setObjectName("name")
|
self.reagent_getter.setObjectName("rtname")
|
||||||
# lookup all reagent type names from db
|
# lookup all reagent type names from db
|
||||||
lookup = lookup_reagent_types(ctx=ctx)
|
lookup = lookup_reagent_types(ctx=ctx)
|
||||||
logger.debug(f"Looked up ReagentType names: {lookup}")
|
logger.debug(f"Looked up ReagentType names: {lookup}")
|
||||||
@@ -233,10 +298,66 @@ class ReagentTypeForm(QWidget):
|
|||||||
grid.addWidget(self.reagent_getter,0,1)
|
grid.addWidget(self.reagent_getter,0,1)
|
||||||
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
|
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
|
||||||
# widget to get extension of life
|
# widget to get extension of life
|
||||||
eol = QSpinBox()
|
self.eol = QSpinBox()
|
||||||
eol.setObjectName('eol')
|
self.eol.setObjectName('eol')
|
||||||
eol.setMinimum(0)
|
self.eol.setMinimum(0)
|
||||||
grid.addWidget(eol, 0,3)
|
grid.addWidget(self.eol, 0,3)
|
||||||
|
grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
|
||||||
|
self.location_sheet_name = QLineEdit()
|
||||||
|
self.location_sheet_name.setObjectName("sheet")
|
||||||
|
self.location_sheet_name.setText("e.g. 'Reagent Info'")
|
||||||
|
grid.addWidget(self.location_sheet_name, 1,1)
|
||||||
|
for iii, item in enumerate(["Name", "Lot", "Expiry"]):
|
||||||
|
idx = iii + 2
|
||||||
|
grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
|
||||||
|
row = QSpinBox()
|
||||||
|
row.setFixedWidth(50)
|
||||||
|
row.setObjectName(f'{item.lower()}_row')
|
||||||
|
row.setMinimum(0)
|
||||||
|
grid.addWidget(row, idx, 1)
|
||||||
|
grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
|
||||||
|
col = QSpinBox()
|
||||||
|
col.setFixedWidth(50)
|
||||||
|
col.setObjectName(f'{item.lower()}_column')
|
||||||
|
col.setMinimum(0)
|
||||||
|
grid.addWidget(col, idx, 3)
|
||||||
|
self.setFixedHeight(175)
|
||||||
|
max_row = grid.rowCount()
|
||||||
|
self.r_button = QPushButton("Remove")
|
||||||
|
self.r_button.clicked.connect(self.remove)
|
||||||
|
grid.addWidget(self.r_button,max_row,0,1,1)
|
||||||
|
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||||
|
"qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
|
||||||
|
]
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
self.setParent(None)
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
def parse_form(self) -> dict:
|
||||||
|
logger.debug(f"Hello from {self.__class__} parser!")
|
||||||
|
info = {}
|
||||||
|
info['eol'] = self.eol.value()
|
||||||
|
info['sheet'] = self.location_sheet_name.text()
|
||||||
|
info['rtname'] = self.reagent_getter.currentText()
|
||||||
|
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
|
||||||
|
for widget in widgets:
|
||||||
|
logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||||
|
match widget:
|
||||||
|
case QLineEdit():
|
||||||
|
info[widget.objectName()] = widget.text()
|
||||||
|
case QComboBox():
|
||||||
|
info[widget.objectName()] = widget.currentText()
|
||||||
|
case QDateEdit():
|
||||||
|
info[widget.objectName()] = widget.date().toPyDate()
|
||||||
|
case QSpinBox() | QDoubleSpinBox():
|
||||||
|
if "_" in widget.objectName():
|
||||||
|
key, sub_key = widget.objectName().split("_")
|
||||||
|
if key not in info.keys():
|
||||||
|
info[key] = {}
|
||||||
|
logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
|
||||||
|
info[key][sub_key] = widget.value()
|
||||||
|
return info
|
||||||
|
|
||||||
class ControlsDatePicker(QWidget):
|
class ControlsDatePicker(QWidget):
|
||||||
"""
|
"""
|
||||||
@@ -336,3 +457,4 @@ class ParsedQLabel(QLabel):
|
|||||||
self.setText(f"Parsed {output}")
|
self.setText(f"Parsed {output}")
|
||||||
else:
|
else:
|
||||||
self.setText(f"MISSING {output}")
|
self.setText(f"MISSING {output}")
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from PyQt6.QtWidgets import (
|
|||||||
from tools import jinja_template_loading
|
from tools import jinja_template_loading
|
||||||
import logging
|
import logging
|
||||||
from backend.db.functions import lookup_kit_types, lookup_submission_type
|
from backend.db.functions import lookup_kit_types, lookup_submission_type
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ class AlertPop(QMessageBox):
|
|||||||
"""
|
"""
|
||||||
Dialog to show an alert.
|
Dialog to show an alert.
|
||||||
"""
|
"""
|
||||||
def __init__(self, message:str, status:str) -> QMessageBox:
|
def __init__(self, message:str, status:Literal['information', 'question', 'warning', 'critical']) -> QMessageBox:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# select icon by string
|
# select icon by string
|
||||||
icon = getattr(QMessageBox.Icon, status.title())
|
icon = getattr(QMessageBox.Icon, status.title())
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from xhtml2pdf import pisa
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
from .pop_ups import QuestionAsker, AlertPop
|
from .pop_ups import QuestionAsker, AlertPop
|
||||||
from ..visualizations import make_plate_barcode, make_plate_map
|
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
@@ -265,18 +265,19 @@ class SubmissionDetails(QDialog):
|
|||||||
if not check_if_app():
|
if not check_if_app():
|
||||||
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
|
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
|
||||||
logger.debug(f"Hitpicking plate...")
|
logger.debug(f"Hitpicking plate...")
|
||||||
plate_dicto = sub.hitpick_plate()
|
self.plate_dicto = sub.hitpick_plate()
|
||||||
logger.debug(f"Making platemap...")
|
logger.debug(f"Making platemap...")
|
||||||
platemap = make_plate_map(plate_dicto)
|
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
|
||||||
logger.debug(f"platemap: {platemap}")
|
# logger.debug(f"Platemap: {self.base_dict['platemap']}")
|
||||||
image_io = BytesIO()
|
# logger.debug(f"platemap: {platemap}")
|
||||||
try:
|
# image_io = BytesIO()
|
||||||
platemap.save(image_io, 'JPEG')
|
# try:
|
||||||
except AttributeError:
|
# platemap.save(image_io, 'JPEG')
|
||||||
logger.error(f"No plate map found for {sub.rsl_plate_num}")
|
# except AttributeError:
|
||||||
self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
# logger.error(f"No plate map found for {sub.rsl_plate_num}")
|
||||||
template = env.get_template("submission_details.html")
|
# self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
||||||
self.html = template.render(sub=self.base_dict)
|
self.template = env.get_template("submission_details.html")
|
||||||
|
self.html = self.template.render(sub=self.base_dict)
|
||||||
webview = QWebEngineView()
|
webview = QWebEngineView()
|
||||||
webview.setMinimumSize(900, 500)
|
webview.setMinimumSize(900, 500)
|
||||||
webview.setMaximumSize(900, 500)
|
webview.setMaximumSize(900, 500)
|
||||||
@@ -290,6 +291,8 @@ class SubmissionDetails(QDialog):
|
|||||||
btn.setParent(self)
|
btn.setParent(self)
|
||||||
btn.setFixedWidth(900)
|
btn.setFixedWidth(900)
|
||||||
btn.clicked.connect(self.export)
|
btn.clicked.connect(self.export)
|
||||||
|
with open("test.html", "w") as f:
|
||||||
|
f.write(self.html)
|
||||||
|
|
||||||
def export(self):
|
def export(self):
|
||||||
"""
|
"""
|
||||||
@@ -303,9 +306,18 @@ class SubmissionDetails(QDialog):
|
|||||||
if fname.__str__() == ".":
|
if fname.__str__() == ".":
|
||||||
logger.debug("Saving pdf was cancelled.")
|
logger.debug("Saving pdf was cancelled.")
|
||||||
return
|
return
|
||||||
|
del self.base_dict['platemap']
|
||||||
|
export_map = make_plate_map(self.plate_dicto)
|
||||||
|
image_io = BytesIO()
|
||||||
|
try:
|
||||||
|
export_map.save(image_io, 'JPEG')
|
||||||
|
except AttributeError:
|
||||||
|
logger.error(f"No plate map found")
|
||||||
|
self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
||||||
|
self.html2 = self.template.render(sub=self.base_dict)
|
||||||
try:
|
try:
|
||||||
with open(fname, "w+b") as f:
|
with open(fname, "w+b") as f:
|
||||||
pisa.CreatePDF(self.html, dest=f)
|
pisa.CreatePDF(self.html2, dest=f)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
logger.error(f"Error saving pdf: {e}")
|
logger.error(f"Error saving pdf: {e}")
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import difflib
|
|||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
import inspect
|
import inspect
|
||||||
import pprint
|
import pprint
|
||||||
|
import re
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
import json
|
||||||
from typing import Tuple, List
|
from typing import Tuple, List
|
||||||
@@ -19,12 +20,12 @@ from PyQt6.QtWidgets import (
|
|||||||
QMainWindow, QLabel, QWidget, QPushButton,
|
QMainWindow, QLabel, QWidget, QPushButton,
|
||||||
QLineEdit, QComboBox, QDateEdit
|
QLineEdit, QComboBox, QDateEdit
|
||||||
)
|
)
|
||||||
from .all_window_functions import extract_form_info, select_open_file, select_save_file
|
from .all_window_functions import select_open_file, select_save_file
|
||||||
from PyQt6.QtCore import QSignalBlocker
|
from PyQt6.QtCore import QSignalBlocker
|
||||||
from backend.db.functions import (
|
from backend.db.functions import (
|
||||||
construct_submission_info, lookup_reagents, construct_kit_from_yaml, construct_org_from_yaml, get_control_subtypes,
|
construct_submission_info, lookup_reagents, construct_kit_from_yaml, construct_org_from_yaml, get_control_subtypes,
|
||||||
update_subsampassoc_with_pcr, check_kit_integrity, update_last_used, lookup_organizations, lookup_kit_types,
|
update_subsampassoc_with_pcr, check_kit_integrity, update_last_used, lookup_organizations, lookup_kit_types,
|
||||||
lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object
|
lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object, lookup_submission_type
|
||||||
)
|
)
|
||||||
from backend.excel.parser import SheetParser, PCRParser, SampleParser
|
from backend.excel.parser import SheetParser, PCRParser, SampleParser
|
||||||
from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df
|
from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df
|
||||||
@@ -139,11 +140,22 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
|
|||||||
logger.debug(f"{field}:\n\t{value}")
|
logger.debug(f"{field}:\n\t{value}")
|
||||||
obj.samples = value
|
obj.samples = value
|
||||||
continue
|
continue
|
||||||
|
case 'submission_category':
|
||||||
|
add_widget = QComboBox()
|
||||||
|
cats = ['Diagnostic', "Surveillance", "Research"]
|
||||||
|
cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
|
||||||
|
try:
|
||||||
|
cats.insert(0, cats.pop(cats.index(value['value'])))
|
||||||
|
except ValueError:
|
||||||
|
cats.insert(0, cats.pop(cats.index(pyd.submission_type['value'])))
|
||||||
|
add_widget.addItems(cats)
|
||||||
case "ctx":
|
case "ctx":
|
||||||
continue
|
continue
|
||||||
case 'reagents':
|
case 'reagents':
|
||||||
# NOTE: This is now set to run when the extraction kit is updated.
|
# NOTE: This is now set to run when the extraction kit is updated.
|
||||||
continue
|
continue
|
||||||
|
case 'csv':
|
||||||
|
continue
|
||||||
case _:
|
case _:
|
||||||
# anything else gets added in as a line edit
|
# anything else gets added in as a line edit
|
||||||
add_widget = QLineEdit()
|
add_widget = QLineEdit()
|
||||||
@@ -166,6 +178,7 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
|
|||||||
if "csv" in pyd.model_extra:
|
if "csv" in pyd.model_extra:
|
||||||
obj.csv = pyd.model_extra['csv']
|
obj.csv = pyd.model_extra['csv']
|
||||||
logger.debug(f"All attributes of obj:\n{pprint.pformat(obj.__dict__)}")
|
logger.debug(f"All attributes of obj:\n{pprint.pformat(obj.__dict__)}")
|
||||||
|
|
||||||
return obj, result
|
return obj, result
|
||||||
|
|
||||||
def kit_reload_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
def kit_reload_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||||
@@ -208,7 +221,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
|||||||
# get current kit being used
|
# get current kit being used
|
||||||
obj.ext_kit = kit_widget.currentText()
|
obj.ext_kit = kit_widget.currentText()
|
||||||
for item in obj.reagents:
|
for item in obj.reagents:
|
||||||
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False, label_name=f"lot_{item.type}_label"))
|
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False))
|
||||||
reagent = dict(type=item.type, lot=item.lot, exp=item.exp, name=item.name)
|
reagent = dict(type=item.type, lot=item.lot, exp=item.exp, name=item.name)
|
||||||
add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit)
|
add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit)
|
||||||
obj.table_widget.formlayout.addWidget(add_widget)
|
obj.table_widget.formlayout.addWidget(add_widget)
|
||||||
@@ -218,7 +231,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
|||||||
result = dict(message=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in obj.missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning")
|
result = dict(message=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in obj.missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning")
|
||||||
for item in obj.missing_reagents:
|
for item in obj.missing_reagents:
|
||||||
# Add label that has parsed as False to show "MISSING" label.
|
# Add label that has parsed as False to show "MISSING" label.
|
||||||
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False, label_name=f"missing_{item.type}_label"))
|
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False))
|
||||||
# Set default parameters for the empty reagent.
|
# Set default parameters for the empty reagent.
|
||||||
reagent = dict(type=item.type, lot=None, exp=date.today(), name=None)
|
reagent = dict(type=item.type, lot=None, exp=date.today(), name=None)
|
||||||
# create and add widget
|
# create and add widget
|
||||||
@@ -227,7 +240,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
|||||||
obj.table_widget.formlayout.addWidget(add_widget)
|
obj.table_widget.formlayout.addWidget(add_widget)
|
||||||
# Add submit button to the form.
|
# Add submit button to the form.
|
||||||
submit_btn = QPushButton("Submit")
|
submit_btn = QPushButton("Submit")
|
||||||
submit_btn.setObjectName("lot_submit_btn")
|
submit_btn.setObjectName("submit_btn")
|
||||||
obj.table_widget.formlayout.addWidget(submit_btn)
|
obj.table_widget.formlayout.addWidget(submit_btn)
|
||||||
submit_btn.clicked.connect(obj.submit_new_sample)
|
submit_btn.clicked.connect(obj.submit_new_sample)
|
||||||
return obj, result
|
return obj, result
|
||||||
@@ -245,32 +258,37 @@ def submit_new_sample_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
|||||||
logger.debug(f"\n\nBeginning Submission\n\n")
|
logger.debug(f"\n\nBeginning Submission\n\n")
|
||||||
result = None
|
result = None
|
||||||
# extract info from the form widgets
|
# extract info from the form widgets
|
||||||
info = extract_form_info(obj.table_widget.tab1)
|
# info = extract_form_info(obj.table_widget.tab1)
|
||||||
# seperate out reagents
|
# if isinstance(info, tuple):
|
||||||
reagents = {k.replace("lot_", ""):v for k,v in info.items() if k.startswith("lot_")}
|
# logger.warning(f"Got tuple for info for some reason.")
|
||||||
info = {k:v for k,v in info.items() if not k.startswith("lot_")}
|
# info = info[0]
|
||||||
|
# # seperate out reagents
|
||||||
|
# reagents = {k.replace("lot_", ""):v for k,v in info.items() if k.startswith("lot_")}
|
||||||
|
# info = {k:v for k,v in info.items() if not k.startswith("lot_")}
|
||||||
|
info, reagents = obj.table_widget.formwidget.parse_form()
|
||||||
logger.debug(f"Info: {info}")
|
logger.debug(f"Info: {info}")
|
||||||
logger.debug(f"Reagents: {reagents}")
|
logger.debug(f"Reagents: {reagents}")
|
||||||
parsed_reagents = []
|
parsed_reagents = []
|
||||||
# compare reagents in form to reagent database
|
# compare reagents in form to reagent database
|
||||||
for reagent in reagents:
|
for reagent in reagents:
|
||||||
# Lookup any existing reagent of this type with this lot number
|
# Lookup any existing reagent of this type with this lot number
|
||||||
wanted_reagent = lookup_reagents(ctx=obj.ctx, lot_number=reagents[reagent], reagent_type=reagent)
|
wanted_reagent = lookup_reagents(ctx=obj.ctx, lot_number=reagent['lot'], reagent_type=reagent['name'])
|
||||||
logger.debug(f"Looked up reagent: {wanted_reagent}")
|
logger.debug(f"Looked up reagent: {wanted_reagent}")
|
||||||
# if reagent not found offer to add to database
|
# if reagent not found offer to add to database
|
||||||
if wanted_reagent == None:
|
if wanted_reagent == None:
|
||||||
r_lot = reagents[reagent]
|
# r_lot = reagent[reagent]
|
||||||
dlg = QuestionAsker(title=f"Add {r_lot}?", message=f"Couldn't find reagent type {reagent.strip('Lot')}: {r_lot} in the database.\n\nWould you like to add it?")
|
r_lot = reagent['lot']
|
||||||
|
dlg = QuestionAsker(title=f"Add {r_lot}?", message=f"Couldn't find reagent type {reagent['name'].strip('Lot')}: {r_lot} in the database.\n\nWould you like to add it?")
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
logger.debug(f"Looking through {pprint.pformat(obj.reagents)} for reagent {reagent}")
|
logger.debug(f"Looking through {pprint.pformat(obj.reagents)} for reagent {reagent['name']}")
|
||||||
try:
|
try:
|
||||||
picked_reagent = [item for item in obj.reagents if item.type == reagent][0]
|
picked_reagent = [item for item in obj.reagents if item.type == reagent['name']][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error(f"Couldn't find {reagent} in obj.reagents. Checking missing reagents {pprint.pformat(obj.missing_reagents)}")
|
logger.error(f"Couldn't find {reagent['name']} in obj.reagents. Checking missing reagents {pprint.pformat(obj.missing_reagents)}")
|
||||||
picked_reagent = [item for item in obj.missing_reagents if item.type == reagent][0]
|
picked_reagent = [item for item in obj.missing_reagents if item.type == reagent['name']][0]
|
||||||
logger.debug(f"checking reagent: {reagent} in obj.reagents. Result: {picked_reagent}")
|
logger.debug(f"checking reagent: {reagent['name']} in obj.reagents. Result: {picked_reagent}")
|
||||||
expiry_date = picked_reagent.exp
|
expiry_date = picked_reagent.exp
|
||||||
wanted_reagent = obj.add_reagent(reagent_lot=r_lot, reagent_type=reagent.replace("lot_", ""), expiry=expiry_date, name=picked_reagent.name)
|
wanted_reagent = obj.add_reagent(reagent_lot=r_lot, reagent_type=reagent['name'].replace("lot_", ""), expiry=expiry_date, name=picked_reagent.name)
|
||||||
else:
|
else:
|
||||||
# In this case we will have an empty reagent and the submission will fail kit integrity check
|
# In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||||
logger.debug("Will not add reagent.")
|
logger.debug("Will not add reagent.")
|
||||||
@@ -348,14 +366,13 @@ def generate_report_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||||
"""
|
"""
|
||||||
result = None
|
|
||||||
# ask for date ranges
|
# ask for date ranges
|
||||||
dlg = ReportDatePicker()
|
dlg = ReportDatePicker()
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
info = extract_form_info(dlg)
|
# info = extract_form_info(dlg)
|
||||||
|
info = dlg.parse_form()
|
||||||
logger.debug(f"Report info: {info}")
|
logger.debug(f"Report info: {info}")
|
||||||
# find submissions based on date range
|
# find submissions based on date range
|
||||||
# subs = lookup_submissions_by_date_range(ctx=obj.ctx, start_date=info['start_date'], end_date=info['end_date'])
|
|
||||||
subs = lookup_submissions(ctx=obj.ctx, start_date=info['start_date'], end_date=info['end_date'])
|
subs = lookup_submissions(ctx=obj.ctx, start_date=info['start_date'], end_date=info['end_date'])
|
||||||
# convert each object to dict
|
# convert each object to dict
|
||||||
records = [item.report_dict() for item in subs]
|
records = [item.report_dict() for item in subs]
|
||||||
@@ -542,6 +559,7 @@ def chart_maker_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
|||||||
def link_controls_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
def link_controls_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||||
"""
|
"""
|
||||||
Link scraped controls to imported submissions.
|
Link scraped controls to imported submissions.
|
||||||
|
NOTE: Depreciated due to improvements in controls scraper.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (QMainWindow): original app window
|
obj (QMainWindow): original app window
|
||||||
@@ -624,6 +642,8 @@ def link_extractions_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
|||||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
||||||
sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
||||||
# If no such submission exists, move onto the next run
|
# If no such submission exists, move onto the next run
|
||||||
|
if sub == None:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||||
count += 1
|
count += 1
|
||||||
@@ -687,6 +707,8 @@ def link_pcr_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
|||||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
||||||
sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
||||||
# if imported submission doesn't exist move on to next run
|
# if imported submission doesn't exist move on to next run
|
||||||
|
if sub == None:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -838,11 +860,14 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
|
|||||||
new_info = []
|
new_info = []
|
||||||
logger.debug(f"Parsing from relevant info map: {pprint.pformat(relevant_info_map)}")
|
logger.debug(f"Parsing from relevant info map: {pprint.pformat(relevant_info_map)}")
|
||||||
for item in relevant_info:
|
for item in relevant_info:
|
||||||
new_item = {}
|
try:
|
||||||
new_item['type'] = item
|
new_item = {}
|
||||||
new_item['location'] = relevant_info_map[item]
|
new_item['type'] = item
|
||||||
new_item['value'] = relevant_info[item]
|
new_item['location'] = relevant_info_map[item]
|
||||||
new_info.append(new_item)
|
new_item['value'] = relevant_info[item]
|
||||||
|
new_info.append(new_item)
|
||||||
|
except KeyError:
|
||||||
|
logger.error(f"Unable to fill in {item}, not found in relevant info.")
|
||||||
logger.debug(f"New reagents: {new_reagents}")
|
logger.debug(f"New reagents: {new_reagents}")
|
||||||
logger.debug(f"New info: {new_info}")
|
logger.debug(f"New info: {new_info}")
|
||||||
# open a new workbook using openpyxl
|
# open a new workbook using openpyxl
|
||||||
@@ -888,17 +913,16 @@ def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]
|
|||||||
"""
|
"""
|
||||||
def get_plates(input_sample_number:str, plates:list) -> Tuple[int, str]:
|
def get_plates(input_sample_number:str, plates:list) -> Tuple[int, str]:
|
||||||
logger.debug(f"Looking up {input_sample_number} in {plates}")
|
logger.debug(f"Looking up {input_sample_number} in {plates}")
|
||||||
# samp = lookup_ww_sample_by_processing_number(ctx=obj.ctx, processing_number=input_sample_number)
|
|
||||||
samp = lookup_samples(ctx=obj.ctx, ww_processing_num=input_sample_number)
|
samp = lookup_samples(ctx=obj.ctx, ww_processing_num=input_sample_number)
|
||||||
if samp == None:
|
if samp == None:
|
||||||
# samp = lookup_sample_by_submitter_id(ctx=obj.ctx, submitter_id=input_sample_number)
|
|
||||||
samp = lookup_samples(ctx=obj.ctx, submitter_id=input_sample_number)
|
samp = lookup_samples(ctx=obj.ctx, submitter_id=input_sample_number)
|
||||||
|
if samp == None:
|
||||||
|
return None, None
|
||||||
logger.debug(f"Got sample: {samp}")
|
logger.debug(f"Got sample: {samp}")
|
||||||
# new_plates = [(iii+1, lookup_sub_samp_association_by_plate_sample(ctx=obj.ctx, rsl_sample_num=samp, rsl_plate_num=lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=plate))) for iii, plate in enumerate(plates)]
|
|
||||||
new_plates = [(iii+1, lookup_submission_sample_association(ctx=obj.ctx, sample=samp, submission=plate)) for iii, plate in enumerate(plates)]
|
new_plates = [(iii+1, lookup_submission_sample_association(ctx=obj.ctx, sample=samp, submission=plate)) for iii, plate in enumerate(plates)]
|
||||||
logger.debug(f"Associations: {pprint.pformat(new_plates)}")
|
logger.debug(f"Associations: {pprint.pformat(new_plates)}")
|
||||||
try:
|
try:
|
||||||
plate_num, plate = next(assoc for assoc in new_plates if assoc[1] is not None)
|
plate_num, plate = next(assoc for assoc in new_plates if assoc[1])
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
plate_num, plate = None, None
|
plate_num, plate = None, None
|
||||||
logger.debug(f"Plate number {plate_num} is {plate}")
|
logger.debug(f"Plate number {plate_num} is {plate}")
|
||||||
@@ -907,11 +931,19 @@ def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]
|
|||||||
xl = pd.ExcelFile(fname)
|
xl = pd.ExcelFile(fname)
|
||||||
sprsr = SampleParser(ctx=obj.ctx, xl=xl, submission_type="First Strand")
|
sprsr = SampleParser(ctx=obj.ctx, xl=xl, submission_type="First Strand")
|
||||||
_, samples = sprsr.parse_samples(generate=False)
|
_, samples = sprsr.parse_samples(generate=False)
|
||||||
|
logger.debug(f"Samples: {pformat(samples)}")
|
||||||
|
logger.debug("Called first strand sample parser")
|
||||||
plates = sprsr.grab_plates()
|
plates = sprsr.grab_plates()
|
||||||
|
logger.debug(f"Plates: {pformat(plates)}")
|
||||||
output_samples = []
|
output_samples = []
|
||||||
logger.debug(f"Samples: {pprint.pformat(samples)}")
|
logger.debug(f"Samples: {pformat(samples)}")
|
||||||
old_plate_number = 1
|
old_plate_number = 1
|
||||||
for item in samples:
|
for item in samples:
|
||||||
|
try:
|
||||||
|
item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0]
|
||||||
|
except AttributeError:
|
||||||
|
item['well'] = item
|
||||||
|
item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip()
|
||||||
new_dict = {}
|
new_dict = {}
|
||||||
new_dict['sample'] = item['submitter_id']
|
new_dict['sample'] = item['submitter_id']
|
||||||
if item['submitter_id'] == "NTC1":
|
if item['submitter_id'] == "NTC1":
|
||||||
@@ -967,6 +999,7 @@ def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, d
|
|||||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||||
obj.reagents = []
|
obj.reagents = []
|
||||||
obj.missing_reagents = []
|
obj.missing_reagents = []
|
||||||
|
# Remove previous reagent widgets
|
||||||
[item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget) if item.objectName().startswith("lot_") or item.objectName().startswith("missing_")]
|
[item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget) if item.objectName().startswith("lot_") or item.objectName().startswith("missing_")]
|
||||||
reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
|
reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
|
||||||
logger.debug(f"Got reagents: {reagents}")
|
logger.debug(f"Got reagents: {reagents}")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
import sys
|
import sys
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from tools import check_if_app
|
from tools import check_if_app, jinja_template_loading
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
@@ -81,4 +81,31 @@ def make_plate_map(sample_list:list) -> Image:
|
|||||||
letter = row_dict[num-1]
|
letter = row_dict[num-1]
|
||||||
y = (num * 100) - 10
|
y = (num * 100) - 10
|
||||||
draw.text((10, y), letter, (0,0,0),font=font)
|
draw.text((10, y), letter, (0,0,0),font=font)
|
||||||
return new_img
|
return new_img
|
||||||
|
|
||||||
|
def make_plate_map_html(sample_list:list, plate_rows:int=8, plate_columns=12) -> str:
|
||||||
|
try:
|
||||||
|
plate_num = sample_list[0]['plate_name']
|
||||||
|
except IndexError as e:
|
||||||
|
logger.error(f"Couldn't get a plate number. Will not make plate.")
|
||||||
|
return None
|
||||||
|
except TypeError as e:
|
||||||
|
logger.error(f"No samples for this plate. Nothing to do.")
|
||||||
|
return None
|
||||||
|
for sample in sample_list:
|
||||||
|
if sample['positive']:
|
||||||
|
sample['background_color'] = "#f10f07"
|
||||||
|
else:
|
||||||
|
sample['background_color'] = "#80cbc4"
|
||||||
|
output_samples = []
|
||||||
|
for column in range(1, plate_columns+1):
|
||||||
|
for row in range(1, plate_rows+1):
|
||||||
|
try:
|
||||||
|
well = [item for item in sample_list if item['row'] == row and item['column']==column][0]
|
||||||
|
except IndexError:
|
||||||
|
well = dict(name="", row=row, column=column, background_color="#ffffff")
|
||||||
|
output_samples.append(well)
|
||||||
|
env = jinja_template_loading()
|
||||||
|
template = env.get_template("plate_map.html")
|
||||||
|
html = template.render(samples=output_samples, PLATE_ROWS=plate_rows, PLATE_COLUMNS=plate_columns)
|
||||||
|
return html
|
||||||
17
src/submissions/templates/plate_map.html
Normal file
17
src/submissions/templates/plate_map.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
|
||||||
|
{% for sample in samples %}
|
||||||
|
<div class="well" style="background-color: {{sample['background_color']}};
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 20px;
|
||||||
|
grid-column-start: {{sample['column']}};
|
||||||
|
grid-column-end: {{sample['column']}};
|
||||||
|
grid-row-start: {{sample['row']}};
|
||||||
|
grid-row-end: {{sample['row']}};
|
||||||
|
display: flex;
|
||||||
|
">
|
||||||
|
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['name'] }}
|
||||||
|
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,38 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<style>
|
||||||
|
/* Tooltip container */
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip text */
|
||||||
|
.tooltip .tooltiptext {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 120px;
|
||||||
|
background-color: black;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
/* Position the tooltip text - see examples below! */
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the tooltip text when you mouse over the tooltip container */
|
||||||
|
.tooltip:hover .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||||
</head>
|
</head>
|
||||||
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments', 'barcode', 'platemap'] %}
|
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments', 'barcode', 'platemap'] %}
|
||||||
@@ -67,7 +99,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub['platemap'] %}
|
{% if sub['platemap'] %}
|
||||||
<h3><u>Plate map:</u></h3>
|
<h3><u>Plate map:</u></h3>
|
||||||
<img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['platemap'] | safe }}">
|
{{ sub['platemap'] }}
|
||||||
|
{% endif %}
|
||||||
|
{% if sub['export_map'] %}
|
||||||
|
<h3><u>Plate map:</u></h3>
|
||||||
|
<img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -12,10 +12,10 @@
|
|||||||
<h3><u>{{ lab['lab'] }}:</u></h3>
|
<h3><u>{{ lab['lab'] }}:</u></h3>
|
||||||
{% for kit in lab['kits'] %}
|
{% for kit in lab['kits'] %}
|
||||||
<p><b>{{ kit['name'] }}</b></p>
|
<p><b>{{ kit['name'] }}</b></p>
|
||||||
<p> Plates: {{ kit['plate_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
|
<p> Runs: {{ kit['plate_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<p><b>Lab total:</b></p>
|
<p><b>Lab total:</b></p>
|
||||||
<p> Plates: {{ lab['total_plates'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p>
|
<p> Runs: {{ lab['total_plates'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p>
|
||||||
<br>
|
<br>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions")
|
|||||||
CONFIGDIR = main_aux_dir.joinpath("config")
|
CONFIGDIR = main_aux_dir.joinpath("config")
|
||||||
LOGDIR = main_aux_dir.joinpath("logs")
|
LOGDIR = main_aux_dir.joinpath("logs")
|
||||||
|
|
||||||
|
row_map = {1:"A", 2:"B", 3:"C", 4:"D", 5:"E", 6:"F", 7:"G", 8:"H"}
|
||||||
|
|
||||||
def check_not_nan(cell_contents) -> bool:
|
def check_not_nan(cell_contents) -> bool:
|
||||||
"""
|
"""
|
||||||
Check to ensure excel sheet cell contents are not blank.
|
Check to ensure excel sheet cell contents are not blank.
|
||||||
@@ -576,10 +578,11 @@ def jinja_template_loading():
|
|||||||
if check_if_app():
|
if check_if_app():
|
||||||
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
|
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
|
||||||
else:
|
else:
|
||||||
loader_path = Path(__file__).parents[1].joinpath('templates').absolute().__str__()
|
loader_path = Path(__file__).parents[1].joinpath('templates').absolute()#.__str__()
|
||||||
# jinja template loading
|
# jinja template loading
|
||||||
loader = FileSystemLoader(loader_path)
|
loader = FileSystemLoader(loader_path)
|
||||||
env = Environment(loader=loader)
|
env = Environment(loader=loader)
|
||||||
|
env.globals['STATIC_PREFIX'] = loader_path.joinpath("static", "css")
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def check_is_power_user(ctx:Settings) -> bool:
|
def check_is_power_user(ctx:Settings) -> bool:
|
||||||
@@ -632,4 +635,5 @@ def convert_well_to_row_column(input_str:str) -> Tuple[int, int]:
|
|||||||
column = int(input_str[1:])
|
column = int(input_str[1:])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None, None
|
return None, None
|
||||||
return row, column
|
return row, column
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user