Addition of Equipment and SubmissionType creation.

This commit is contained in:
Landon Wark
2023-12-20 12:54:37 -06:00
parent 0dd51827a0
commit 0d64095e42
22 changed files with 755 additions and 123 deletions

View File

@@ -1,5 +1,12 @@
## 202312.03
- Enabled creation of new submission types in gui.
- Enabled Equipment addition.
## 202312.02 ## 202312.02
- Bug fixes for switching kits
## 202312.01 ## 202312.01
- Control samples info now available in plate map. - Control samples info now available in plate map.

0
None
View File

View File

@@ -1,9 +1,14 @@
- [ ] Update Artic and add in equipment listings... *sigh*.
- [x] Fix WastewaterAssociations not in Session error.
- Done... I think?
- [x] Fix submitted date always being today.
- this is an issue with the way client is filling in form with =TODAY()
- [x] SubmissionReagentAssociation.query - [x] SubmissionReagentAssociation.query
- [x] Move as much from db.functions to objects as possible. - [x] Move as much from db.functions to objects as possible.
- [x] Clean up DB objects after failed test fix. - [x] Clean up DB objects after failed test fix.
- [x] Fix tests. - [x] Fix tests.
- [x] Fix pydant.PydSample.handle_duplicate_samples? - [x] Fix pydant.PydSample.handle_duplicate_samples?
- [ ] See if the number of queries in BasicSubmission functions (and others) can be trimmed down. - [x] See if the number of queries in BasicSubmission functions (and others) can be trimmed down.
- [x] Document code - [x] Document code
- [x] Create a result object to facilitate returning function results. - [x] Create a result object to facilitate returning function results.
- [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up. - [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up.

View File

@@ -0,0 +1,52 @@
"""Adding in Equipment
Revision ID: 36a47d8837ca
Revises: 238c3c3e5863
Create Date: 2023-12-12 09:16:09.559753
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '36a47d8837ca'
down_revision = '238c3c3e5863'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_equipment',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('nickname', sa.String(length=64), nullable=True),
sa.Column('asset_number', sa.String(length=16), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_submissiontype_equipment',
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submission_types.id'], ),
sa.PrimaryKeyConstraint('equipment_id', 'submission_id')
)
op.create_table('_equipment_submissions',
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('comments', sa.String(length=1024), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submissions.id'], ),
sa.PrimaryKeyConstraint('equipment_id', 'submission_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_equipment_submissions')
op.drop_table('_submissiontype_equipment')
op.drop_table('_equipment')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""Adding times to equipSubAssoc
Revision ID: 3e94fecbbe91
Revises: cd5c225b5d2a
Create Date: 2023-12-15 09:38:33.931976
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3e94fecbbe91'
down_revision = 'cd5c225b5d2a'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('start_time', sa.TIMESTAMP(), nullable=True))
batch_op.add_column(sa.Column('end_time', sa.TIMESTAMP(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.drop_column('end_time')
batch_op.drop_column('start_time')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Adding equipment clustering
Revision ID: 761baf9d7842
Revises: 3e94fecbbe91
Create Date: 2023-12-18 14:31:21.533319
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '761baf9d7842'
down_revision = '3e94fecbbe91'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment', schema=None) as batch_op:
batch_op.add_column(sa.Column('cluster_name', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment', schema=None) as batch_op:
batch_op.drop_column('cluster_name')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Adding static option to equipSTASsoc
Revision ID: cd11db3794ed
Revises: 36a47d8837ca
Create Date: 2023-12-12 14:47:20.924443
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cd11db3794ed'
down_revision = '36a47d8837ca'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype_equipment', schema=None) as batch_op:
batch_op.add_column(sa.Column('static', sa.INTEGER(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype_equipment', schema=None) as batch_op:
batch_op.drop_column('static')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Adding process to equipSubAssoc
Revision ID: cd5c225b5d2a
Revises: cd11db3794ed
Create Date: 2023-12-15 09:13:23.492512
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cd5c225b5d2a'
down_revision = 'cd11db3794ed'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('process', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.drop_column('process')
# ### end Alembic commands ###

View File

@@ -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__ = "202312.2b" __version__ = "202312.3b"
__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"

View File

@@ -10,6 +10,7 @@ import logging
from tools import check_authorization, setup_lookup, query_return, Report, Result, Settings from tools import check_authorization, setup_lookup, query_return, Report, Result, Settings
from typing import List from typing import List
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, Organization from . import Base, BaseClass, Organization
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
@@ -55,7 +56,7 @@ class KitType(BaseClass):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<KitType({self.name})>" return f"<KitType({self.name})>"
def get_reagents(self, required:bool=False, submission_type:str|SubmissionType|None=None) -> list: def get_reagents(self, required:bool=False, submission_type:str|SubmissionType|None=None) -> List[ReagentType]:
""" """
Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation. Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation.
@@ -243,6 +244,10 @@ class ReagentType(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def to_pydantic(self):
from backend.validators.pydant import PydReagent
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
class KitTypeReagentTypeAssociation(BaseClass): class KitTypeReagentTypeAssociation(BaseClass):
""" """
table containing reagenttype/kittype associations table containing reagenttype/kittype associations
@@ -583,6 +588,14 @@ class SubmissionType(BaseClass):
kit_types = association_proxy("submissiontype_kit_associations", "kit_type") #: Proxy of kittype association kit_types = association_proxy("submissiontype_kit_associations", "kit_type") #: Proxy of kittype association
submissiontype_equipment_associations = relationship(
"SubmissionTypeEquipmentAssociation",
back_populates="submission_type",
cascade="all, delete-orphan"
)
equipment = association_proxy("submissiontype_equipment_associations", "equipment")
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<SubmissionType({self.name})>" return f"<SubmissionType({self.name})>"
@@ -595,6 +608,35 @@ class SubmissionType(BaseClass):
""" """
return ExcelFile(self.template_file).sheet_names return ExcelFile(self.template_file).sheet_names
def set_template_file(self, filepath:Path|str):
if isinstance(filepath, str):
filepath = Path(filepath)
with open (filepath, "rb") as f:
data = f.read()
self.template_file = data
self.save()
def get_equipment(self) -> list:
from backend.validators.pydant import PydEquipmentPool
# if static:
# return [item.equipment.to_pydantic() for item in self.submissiontype_equipment_associations if item.static==1]
# else:
preliminary1 = [item.equipment.to_pydantic(static=item.static) for item in self.submissiontype_equipment_associations]# if item.static==0]
preliminary2 = [item.equipment.to_pydantic(static=item.static) for item in self.submissiontype_equipment_associations]# if item.static==0]
output = []
pools = list(set([item.pool_name for item in preliminary1 if item.pool_name != None]))
for pool in pools:
c_ = []
for item in preliminary1:
if item.pool_name == pool:
c_.append(item)
preliminary2.remove(item)
if len(c_) > 0:
output.append(PydEquipmentPool(name=pool, equipment=c_))
for item in preliminary2:
output.append(item)
return output
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
@@ -772,4 +814,145 @@ class SubmissionReagentAssociation(BaseClass):
# limit = query.count() # limit = query.count()
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def to_sub_dict(self, extraction_kit):
output = self.reagent.to_sub_dict(extraction_kit)
output['comments'] = self.comments
return output
class Equipment(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
__tablename__ = "_equipment"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
nickname = Column(String(64))
asset_number = Column(String(16))
pool_name = Column(String(16))
equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation",
back_populates="equipment",
cascade="all, delete-orphan",
)
submissions = association_proxy("equipment_submission_associations", "submission")
equipment_submissiontype_associations = relationship(
"SubmissionTypeEquipmentAssociation",
back_populates="equipment",
cascade="all, delete-orphan",
)
submission_types = association_proxy("equipment_submission_associations", "submission_type")
def __repr__(self):
return f"<Equipment({self.name})>"
@classmethod
@setup_lookup
def query(cls,
name:str|None=None,
nickname:str|None=None,
asset_number:str|None=None,
limit:int=0
) -> Equipment|List[Equipment]:
query = cls.__database_session__.query(cls)
match name:
case str():
query = query.filter(cls.name==name)
limit = 1
case _:
pass
match nickname:
case str():
query = query.filter(cls.nickname==nickname)
limit = 1
case _:
pass
match asset_number:
case str():
query = query.filter(cls.asset_number==asset_number)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def to_pydantic(self, static):
from backend.validators.pydant import PydEquipment
return PydEquipment(static=static, **self.__dict__)
def save(self):
self.__database_session__.add(self)
self.__database_session__.commit()
class SubmissionEquipmentAssociation(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
__tablename__ = "_equipment_submissions"
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
process = Column(String(64)) #: name of the process run on this equipment
start_time = Column(TIMESTAMP)
end_time = Column(TIMESTAMP)
comments = Column(String(1024))
submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated submission
def __init__(self, submission, equipment):
self.submission = submission
self.equipment = equipment
def to_sub_dict(self) -> dict:
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments)
return output
def save(self):
self.__database_session__.add(self)
self.__database_session__.commit()
class SubmissionTypeEquipmentAssociation(BaseClass):
# __abstract__ = True
__tablename__ = "_submissiontype_equipment"
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
uses = Column(JSON) #: locations of equipment on the submission type excel sheet.
static = Column(INTEGER, default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
submission_type = relationship(SubmissionType, back_populates="submissiontype_equipment_associations") #: associated submission
equipment = relationship(Equipment, back_populates="equipment_submissiontype_associations") #: associated equipment
@validates('static')
def validate_age(self, key, value):
"""
Ensures only 1 & 0 used in 'static'
Args:
key (str): name of attribute
value (_type_): value of attribute
Raises:
ValueError: Raised if bad value given
Returns:
_type_: value
"""
if not 0 <= value < 2:
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value
def save(self):
self.__database_session__.add(self)
self.__database_session__.commit()

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from getpass import getuser from getpass import getuser
import math, json, logging, uuid, tempfile, re, yaml import math, json, logging, uuid, tempfile, re, yaml
from pprint import pformat from pprint import pformat
from . import Reagent, SubmissionType, KitType, Organization from . import Reagent, SubmissionType, KitType, Organization, Equipment, SubmissionEquipmentAssociation
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
@@ -69,6 +69,13 @@ class BasicSubmission(BaseClass):
# to "keyword" attribute # to "keyword" attribute
reagents = association_proxy("submission_reagent_associations", "reagent") #: Association proxy to SubmissionSampleAssociation.samples reagents = association_proxy("submission_reagent_associations", "reagent") #: Association proxy to SubmissionSampleAssociation.samples
submission_equipment_associations = relationship(
"SubmissionEquipmentAssociation",
back_populates="submission",
cascade="all, delete-orphan"
)
equipment = association_proxy("submission_equipment_associations", "equipment")
# Allows for subclassing into ex. BacterialCulture, Wastewater, etc. # Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "Basic Submission", "polymorphic_identity": "Basic Submission",
@@ -124,7 +131,7 @@ class BasicSubmission(BaseClass):
# Updated 2023-09 to use the extraction kit to pull reagents. # Updated 2023-09 to use the extraction kit to pull reagents.
if full_data: if full_data:
try: try:
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.reagents] reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.submission_reagent_associations]
except Exception as e: except Exception as e:
logger.error(f"We got an error retrieving reagents: {e}") logger.error(f"We got an error retrieving reagents: {e}")
reagents = None reagents = None
@@ -138,6 +145,13 @@ class BasicSubmission(BaseClass):
except Exception as e: except Exception as e:
logger.error(f"Error setting comment: {self.comment}") logger.error(f"Error setting comment: {self.comment}")
comments = None comments = None
try:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if len(equipment) == 0:
equipment = None
except Exception as e:
logger.error(f"Error setting equipment: {self.equipment}")
equipment = None
output = { output = {
"id": self.id, "id": self.id,
"Plate Number": self.rsl_plate_num, "Plate Number": self.rsl_plate_num,
@@ -153,7 +167,8 @@ class BasicSubmission(BaseClass):
"reagents": reagents, "reagents": reagents,
"samples": samples, "samples": samples,
"extraction_info": ext_info, "extraction_info": ext_info,
"comment": comments "comment": comments,
"equipment": equipment
} }
return output return output
@@ -447,7 +462,7 @@ class BasicSubmission(BaseClass):
logger.debug(f"Got {len(subs)} submissions.") logger.debug(f"Got {len(subs)} submissions.")
df = pd.DataFrame.from_records(subs) df = pd.DataFrame.from_records(subs)
# Exclude sub information # Exclude sub information
for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents']: for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents', 'equipment']:
try: try:
df = df.drop(item, axis=1) df = df.drop(item, axis=1)
except: except:
@@ -520,7 +535,7 @@ class BasicSubmission(BaseClass):
_type_: _description_ _type_: _description_
""" """
# assoc = SubmissionSampleAssociation.query(submission=self, sample=sample, limit=1) # assoc = SubmissionSampleAssociation.query(submission=self, sample=sample, limit=1)
assoc = [item.sample for item in self.submission_sample_associations if item.sample==sample][0] assoc = [item for item in self.submission_sample_associations if item.sample==sample][0]
for k,v in input_dict.items(): for k,v in input_dict.items():
try: try:
setattr(assoc, k, v) setattr(assoc, k, v)
@@ -751,6 +766,7 @@ class BasicSubmission(BaseClass):
msg = "This submission already exists.\nWould you like to overwrite?" msg = "This submission already exists.\nWould you like to overwrite?"
return instance, code, msg return instance, code, msg
# Below are the custom submission types # Below are the custom submission types
class BacterialCulture(BasicSubmission): class BacterialCulture(BasicSubmission):
@@ -877,6 +893,12 @@ class BacterialCulture(BasicSubmission):
template += "_{{ submitting_lab }}_{{ submitter_plate_num }}" template += "_{{ submitting_lab }}_{{ submitter_plate_num }}"
return template return template
@classmethod
def parse_info(cls, input_dict: dict, xl: pd.ExcelFile | None = None) -> dict:
input_dict = super().parse_info(input_dict, xl)
input_dict['submitted_date']['missing'] = True
return input_dict
class Wastewater(BasicSubmission): class Wastewater(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
@@ -1009,7 +1031,8 @@ class Wastewater(BasicSubmission):
Returns: Returns:
str: String for regex construction str: String for regex construction
""" """
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789]|$)R?\d?)?)" # return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)"
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
class WastewaterArtic(BasicSubmission): class WastewaterArtic(BasicSubmission):
""" """
@@ -1416,7 +1439,9 @@ class BasicSample(BaseClass):
return instance return instance
def save(self): def save(self):
raise AttributeError(f"Save not implemented for {self.__class__}") # raise AttributeError(f"Save not implemented for {self.__class__}")
self.__database_session__.add(self)
self.__database_session__.commit()
def delete(self): def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1735,4 +1760,3 @@ class WastewaterAssociation(SubmissionSampleAssociation):
pcr_results = Column(JSON) #: imported PCR status from QuantStudio pcr_results = Column(JSON) #: imported PCR status from QuantStudio
__mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"} __mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}

View File

@@ -13,7 +13,7 @@ import logging, re
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
from dateutil.parser import parse, ParserError from dateutil.parser import parse, ParserError
from tools import check_not_nan, convert_nans_to_nones, Settings from tools import check_not_nan, convert_nans_to_nones, Settings, is_missing
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -186,23 +186,15 @@ class InfoParser(object):
value = df.iat[relevant[item]['row']-1, relevant[item]['column']-1] value = df.iat[relevant[item]['row']-1, relevant[item]['column']-1]
match item: match item:
case "submission_type": case "submission_type":
value, missing = is_missing(value)
value = value.title() value = value.title()
case _: case _:
pass value, missing = is_missing(value)
logger.debug(f"Setting {item} on {sheet} to {value}") logger.debug(f"Setting {item} on {sheet} to {value}")
if check_not_nan(value): try:
if value != "None": dicto[item] = dict(value=value, missing=missing)
try: except (KeyError, IndexError):
dicto[item] = dict(value=value, missing=False) continue
except (KeyError, IndexError):
continue
else:
try:
dicto[item] = dict(value=value, missing=True)
except (KeyError, IndexError):
continue
else:
dicto[item] = dict(value=convert_nans_to_nones(value), missing=True)
return self.custom_parser(input_dict=dicto, xl=self.xl) return self.custom_parser(input_dict=dicto, xl=self.xl)
class ReagentParser(object): class ReagentParser(object):
@@ -293,7 +285,9 @@ class SampleParser(object):
self.xl = xl self.xl = xl
self.submission_type = submission_type self.submission_type = submission_type
sample_info_map = self.fetch_sample_info_map(submission_type=submission_type) sample_info_map = self.fetch_sample_info_map(submission_type=submission_type)
logger.debug(f"sample_info_map: {sample_info_map}")
self.plate_map = self.construct_plate_map(plate_map_location=sample_info_map['plate_map']) self.plate_map = self.construct_plate_map(plate_map_location=sample_info_map['plate_map'])
logger.debug(f"plate_map: {self.plate_map}")
self.lookup_table = self.construct_lookup_table(lookup_table_location=sample_info_map['lookup_table']) self.lookup_table = self.construct_lookup_table(lookup_table_location=sample_info_map['lookup_table'])
if "plates" in sample_info_map: if "plates" in sample_info_map:
self.plates = sample_info_map['plates'] self.plates = sample_info_map['plates']
@@ -332,10 +326,12 @@ class SampleParser(object):
Returns: Returns:
pd.DataFrame: Plate map grid pd.DataFrame: Plate map grid
""" """
logger.debug(f"Plate map location: {plate_map_location}")
df = self.xl.parse(plate_map_location['sheet'], header=None, dtype=object) df = self.xl.parse(plate_map_location['sheet'], header=None, dtype=object)
df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']] df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']]
df = pd.DataFrame(df.values[1:], columns=df.iloc[0]) df = pd.DataFrame(df.values[1:], columns=df.iloc[0])
df = df.set_index(df.columns[0]) df = df.set_index(df.columns[0])
logger.debug(f"Vanilla platemap: {df}")
# custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type) # custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type)
custom_mapper = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) custom_mapper = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
df = custom_mapper.custom_platemap(self.xl, df) df = custom_mapper.custom_platemap(self.xl, df)
@@ -440,6 +436,7 @@ class SampleParser(object):
""" """
result = None result = None
new_samples = [] new_samples = []
logger.debug(f"Starting samples: {pformat(self.samples)}")
for ii, sample in enumerate(self.samples): for ii, sample in enumerate(self.samples):
# try: # try:
# if sample['submitter_id'] in [check_sample['sample'].submitter_id for check_sample in new_samples]: # if sample['submitter_id'] in [check_sample['sample'].submitter_id for check_sample in new_samples]:

View File

@@ -127,9 +127,10 @@ class PydReagent(BaseModel):
reagent.name = value reagent.name = value
case "comment": case "comment":
continue continue
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission) if submission != None:
assoc.comments = self.comment assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
reagent.reagent_submission_associations.append(assoc) assoc.comments = self.comment
reagent.reagent_submission_associations.append(assoc)
# add end-of-life extension from reagent type to expiry date # add end-of-life extension from reagent type to expiry date
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions # NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
return reagent, report return reagent, report
@@ -199,7 +200,8 @@ class PydSample(BaseModel, extra='allow'):
row=row, column=column) row=row, column=column)
try: try:
instance.sample_submission_associations.append(association) instance.sample_submission_associations.append(association)
except IntegrityError: except IntegrityError as e:
logger.error(f"Could not attach submission sample association due to: {e}")
instance.metadata.session.rollback() instance.metadata.session.rollback()
return instance, report return instance, report
@@ -420,13 +422,18 @@ class PydSubmission(BaseModel, extra='allow'):
if isinstance(value, dict): if isinstance(value, dict):
value = value['value'] value = value['value']
logger.debug(f"Setting {key} to {value}") logger.debug(f"Setting {key} to {value}")
try: match key:
instance.set_attribute(key=key, value=value) case "samples":
except AttributeError as e: for sample in self.samples:
logger.debug(f"Could not set attribute: {key} to {value} due to: \n\n {e}") sample, _ = sample.toSQL(submission=instance)
continue case _:
except KeyError: try:
continue instance.set_attribute(key=key, value=value)
except AttributeError as e:
logger.debug(f"Could not set attribute: {key} to {value} due to: \n\n {e}")
continue
except KeyError:
continue
try: try:
logger.debug(f"Calculating costs for procedure...") logger.debug(f"Calculating costs for procedure...")
instance.calculate_base_cost() instance.calculate_base_cost()
@@ -735,4 +742,35 @@ class PydKit(BaseModel):
[item.toSQL(instance) for item in self.reagent_types] [item.toSQL(instance) for item in self.reagent_types]
return instance, report return instance, report
class PydEquipment(BaseModel, extra='ignore'):
name: str
nickname: str|None
asset_number: str
pool_name: str|None
static: bool|int
@field_validator("static")
@classmethod
def to_boolean(cls, value):
match value:
case int():
if value == 0:
return False
else:
return True
case _:
return value
def toForm(self, parent):
from frontend.widgets.equipment_usage import EquipmentCheckBox
return EquipmentCheckBox(parent=parent, equipment=self)
class PydEquipmentPool(BaseModel):
name: str
equipment: List[PydEquipment]
def toForm(self, parent):
from frontend.widgets.equipment_usage import PoolComboBox
return PoolComboBox(parent=parent, pool=self)

View File

@@ -18,6 +18,8 @@ from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
from .kit_creator import KitAdder from .kit_creator import KitAdder
from .submission_type_creator import SubbmissionTypeAdder
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger") logger.info("Hello, I am a logger")
@@ -207,11 +209,13 @@ class AddSubForm(QWidget):
self.tab1 = QWidget() self.tab1 = QWidget()
self.tab2 = QWidget() self.tab2 = QWidget()
self.tab3 = QWidget() self.tab3 = QWidget()
self.tab4 = QWidget()
self.tabs.resize(300,200) self.tabs.resize(300,200)
# Add tabs # Add tabs
self.tabs.addTab(self.tab1,"Submissions") self.tabs.addTab(self.tab1,"Submissions")
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 SubmissionType")
self.tabs.addTab(self.tab4, "Add Kit")
# Create submission adder form # Create submission adder form
self.formwidget = SubmissionFormContainer(self) self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self) self.formlayout = QVBoxLayout(self)
@@ -238,10 +242,14 @@ class AddSubForm(QWidget):
self.tab2.layout.addWidget(self.controls_viewer) self.tab2.layout.addWidget(self.controls_viewer)
self.tab2.setLayout(self.tab2.layout) self.tab2.setLayout(self.tab2.layout)
# create custom widget to add new tabs # create custom widget to add new tabs
adder = KitAdder(self) ST_adder = SubbmissionTypeAdder(self)
self.tab3.layout = QVBoxLayout(self) self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(adder) self.tab3.layout.addWidget(ST_adder)
self.tab3.setLayout(self.tab3.layout) self.tab3.setLayout(self.tab3.layout)
kit_adder = KitAdder(self)
self.tab4.layout = QVBoxLayout(self)
self.tab4.layout.addWidget(kit_adder)
self.tab4.setLayout(self.tab4.layout)
# add tabs to main widget # add tabs to main widget
self.layout.addWidget(self.tabs) self.layout.addWidget(self.tabs)
self.setLayout(self.layout) self.setLayout(self.layout)

View File

@@ -0,0 +1,89 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QHBoxLayout,
QVBoxLayout, QDialogButtonBox)
from backend.db.models import SubmissionType
from backend.validators.pydant import PydEquipment, PydEquipmentPool
class EquipmentUsage(QDialog):
def __init__(self, parent, submission_type:SubmissionType|str) -> QDialog:
super().__init__(parent)
self.setWindowTitle("Equipment Checklist")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
# self.static_equipment = submission_type.get_equipment()
self.opt_equipment = submission_type.get_equipment()
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.populate_form()
def populate_form(self):
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
for eq in self.opt_equipment:
self.layout.addWidget(eq.toForm(parent=self))
self.layout.addWidget(self.buttonBox)
def parse_form(self):
output = []
for widget in self.findChildren(QWidget):
match widget:
case (EquipmentCheckBox()|PoolComboBox()) :
output.append(widget.parse_form())
case _:
pass
return [item for item in output if item != None]
class EquipmentCheckBox(QWidget):
def __init__(self, parent, equipment:PydEquipment) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
self.label = QLabel()
self.label.setMaximumWidth(125)
self.label.setMinimumWidth(125)
self.check = QCheckBox()
if equipment.static:
self.check.setChecked(True)
# self.check.setEnabled(False)
if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})"
else:
text = equipment.name
self.setObjectName(equipment.name)
self.label.setText(text)
self.layout.addWidget(self.label)
self.layout.addWidget(self.check)
self.setLayout(self.layout)
def parse_form(self) -> str|None:
if self.check.isChecked():
return self.objectName()
else:
return None
class PoolComboBox(QWidget):
def __init__(self, parent, pool:PydEquipmentPool) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
# label = QLabel()
# label.setText(pool.name)
self.box = QComboBox()
self.box.setMaximumWidth(125)
self.box.setMinimumWidth(125)
self.box.addItems([item.name for item in pool.equipment])
self.check = QCheckBox()
# self.layout.addWidget(label)
self.layout.addWidget(self.box)
self.layout.addWidget(self.check)
self.setLayout(self.layout)
def parse_form(self) -> str:
if self.check.isChecked():
return self.box.currentText()
else:
return None

View File

@@ -82,7 +82,7 @@ class KitAdder(QWidget):
print(self.app) print(self.app)
# get bottommost row # get bottommost row
maxrow = self.grid.rowCount() maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm() reg_form = ReagentTypeForm(parent=self)
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) self.grid.addWidget(reg_form, maxrow,0,1,4)
@@ -139,8 +139,8 @@ class ReagentTypeForm(QWidget):
""" """
custom widget to add information about a new reagenttype custom widget to add information about a new reagenttype
""" """
def __init__(self) -> None: def __init__(self, parent) -> None:
super().__init__() super().__init__(parent)
grid = QGridLayout() grid = QGridLayout()
self.setLayout(grid) self.setLayout(grid)
grid.addWidget(QLabel("Reagent Type Name"),0,0) grid.addWidget(QLabel("Reagent Type Name"),0,0)

View File

@@ -53,7 +53,7 @@ class KitSelector(QDialog):
super().__init__() super().__init__()
self.setWindowTitle(title) self.setWindowTitle(title)
self.widget = QComboBox() self.widget = QComboBox()
kits = [item.__str__() for item in KitType.query()] kits = [item.name for item in KitType.query()]
self.widget.addItems(kits) self.widget.addItems(kits)
self.widget.setEditable(False) self.widget.setEditable(False)
# set yes/no buttons # set yes/no buttons

View File

@@ -15,11 +15,12 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.models import BasicSubmission from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation
from backend.excel import make_report_html, make_report_xlsx from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from xhtml2pdf import pisa from xhtml2pdf import pisa
from .pop_ups import QuestionAsker from .pop_ups import QuestionAsker
from .equipment_usage import EquipmentUsage
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from .functions import select_save_file, select_open_file from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker from .misc import ReportDatePicker
@@ -159,22 +160,44 @@ class SubmissionsSheet(QTableView):
# barcodeAction = QAction("Print Barcode", self) # barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", self) commentAction = QAction("Add Comment", self)
backupAction = QAction("Backup", self) backupAction = QAction("Backup", self)
equipAction = QAction("Add Equipment", self)
# hitpickAction = QAction("Hitpicks", self) # hitpickAction = QAction("Hitpicks", self)
renameAction.triggered.connect(lambda: self.delete_item(event)) renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details()) detailsAction.triggered.connect(lambda: self.show_details())
# barcodeAction.triggered.connect(lambda: self.create_barcode()) # barcodeAction.triggered.connect(lambda: self.create_barcode())
commentAction.triggered.connect(lambda: self.add_comment()) commentAction.triggered.connect(lambda: self.add_comment())
backupAction.triggered.connect(lambda: self.regenerate_submission_form()) backupAction.triggered.connect(lambda: self.regenerate_submission_form())
equipAction.triggered.connect(lambda: self.add_equipment())
# hitpickAction.triggered.connect(lambda: self.hit_pick()) # hitpickAction.triggered.connect(lambda: self.hit_pick())
self.menu.addAction(detailsAction) self.menu.addAction(detailsAction)
self.menu.addAction(renameAction) self.menu.addAction(renameAction)
# self.menu.addAction(barcodeAction) # self.menu.addAction(barcodeAction)
self.menu.addAction(commentAction) self.menu.addAction(commentAction)
self.menu.addAction(backupAction) self.menu.addAction(backupAction)
self.menu.addAction(equipAction)
# self.menu.addAction(hitpickAction) # self.menu.addAction(hitpickAction)
# add other required actions # add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
def add_equipment(self):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value)
def add_equipment_function(self, rsl_plate_id):
submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type)
if dlg.exec():
equipment = dlg.parse_form()
for equip in equipment:
e = Equipment.query(name=equip)
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
def delete_item(self, event): def delete_item(self, event):
""" """
Confirms user deletion and sends id to backend for deletion. Confirms user deletion and sends id to backend for deletion.
@@ -193,65 +216,6 @@ class SubmissionsSheet(QTableView):
return return
self.setData() self.setData()
# def hit_pick(self):
# """
# Extract positive samples from submissions with PCR results and export to csv.
# NOTE: For this to work for arbitrary samples, positive samples must have 'positive' in their name
# """
# # Get all selected rows
# indices = self.selectionModel().selectedIndexes()
# # convert to id numbers
# indices = [index.sibling(index.row(), 0).data() for index in indices]
# # biomek can handle 4 plates maximum
# if len(indices) > 4:
# logger.error(f"Error: Had to truncate number of plates to 4.")
# indices = indices[:4]
# # lookup ids in the database
# # subs = [lookup_submissions(ctx=self.ctx, id=id) for id in indices]
# subs = [BasicSubmission.query(id=id) for id in indices]
# # full list of samples
# dicto = []
# # list to contain plate images
# images = []
# for iii, sub in enumerate(subs):
# # second check to make sure there aren't too many plates
# if iii > 3:
# logger.error(f"Error: Had to truncate number of plates to 4.")
# continue
# plate_dicto = sub.hitpick_plate(plate_number=iii+1)
# if plate_dicto == None:
# continue
# image = make_plate_map(plate_dicto)
# images.append(image)
# for item in plate_dicto:
# if len(dicto) < 94:
# dicto.append(item)
# else:
# logger.error(f"We had to truncate the number of samples to 94.")
# logger.debug(f"We found {len(dicto)} to hitpick")
# # convert all samples to dataframe
# df = make_hitpicks(dicto)
# df = df[df.positive != False]
# logger.debug(f"Size of the dataframe: {df.shape[0]}")
# msg = AlertPop(message=f"We found {df.shape[0]} samples to hitpick", status="INFORMATION")
# msg.exec()
# if df.size == 0:
# return
# date = datetime.strftime(datetime.today(), "%Y-%m-%d")
# # ask for filename and save as csv.
# home_dir = Path(self.ctx.directory_path).joinpath(f"Hitpicks_{date}.csv").resolve().__str__()
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".csv")[0])
# if fname.__str__() == ".":
# logger.debug("Saving csv was cancelled.")
# return
# df.to_csv(fname.__str__(), index=False)
# # show plate maps
# for image in images:
# try:
# image.show()
# except Exception as e:
# logger.error(f"Could not show image: {e}.")
def link_extractions(self): def link_extractions(self):
self.link_extractions_function() self.link_extractions_function()
self.app.report.add_result(self.report) self.app.report.add_result(self.report)

View File

@@ -0,0 +1,118 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QDateEdit
)
from sqlalchemy import FLOAT, INTEGER
from sqlalchemy.orm.attributes import InstrumentedAttribute
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentAssociation, BasicSubmission
from backend.validators import PydReagentType, PydKit
import logging
from pprint import pformat
from tools import Report
from typing import Tuple
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}")
class SubbmissionTypeAdder(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.report = Report()
self.app = parent.parent()
self.template_path = ""
main_box = QVBoxLayout(self)
scroll = QScrollArea(self)
main_box.addWidget(scroll)
scroll.setWidgetResizable(True)
scrollContent = QWidget(scroll)
self.grid = QGridLayout()
scrollContent.setLayout(self.grid)
# insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Submission Type Name:"),2,0)
# widget to get kit name
self.st_name = QLineEdit()
self.st_name.setObjectName("submission_type_name")
self.grid.addWidget(self.st_name,2,1,1,2)
self.grid.addWidget(QLabel("Template File"),3,0)
template_selector = QPushButton("Select")
self.grid.addWidget(template_selector,3,1)
self.template_label = QLabel("None")
self.grid.addWidget(self.template_label,3,2)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost']
self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)}
self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude}
for iii, key in enumerate(self.columns):
idx = iii + 4
# convert field name to human readable.
# field_name = key
# self.grid.addWidget(QLabel(field_name),idx,0)
# print(self.columns[key].type)
# match self.columns[key].type:
# case FLOAT():
# add_widget = QDoubleSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case INTEGER():
# add_widget = QSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case _:
# add_widget = QLineEdit()
# add_widget.setObjectName(key)
self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
scroll.setWidget(scrollContent)
self.submit_btn.clicked.connect(self.submit)
template_selector.clicked.connect(self.get_template_path)
def submit(self):
info = self.parse_form()
ST = SubmissionType(name=self.st_name.text(), info_map=info)
with open(self.template_path, "rb") as f:
ST.template_file = f.read()
logger.debug(ST.__dict__)
def parse_form(self):
widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
return [{widget.objectName():widget.parse_form()} for widget in widgets]
def get_template_path(self):
self.template_path = select_open_file(obj=self, file_extension="xlsx")
self.template_label.setText(self.template_path.__str__())
class InfoWidget(QWidget):
def __init__(self, parent: QWidget, key) -> None:
super().__init__(parent)
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel(key.replace("_", " ").title()),0,0,1,4)
self.setObjectName(key)
grid.addWidget(QLabel("Sheet Names (comma seperated):"),1,0)
self.sheet = QLineEdit()
self.sheet.setObjectName("sheets")
grid.addWidget(self.sheet, 1,1,1,3)
grid.addWidget(QLabel("Row:"),2,0,alignment=Qt.AlignmentFlag.AlignRight)
self.row = QSpinBox()
self.row.setObjectName("row")
grid.addWidget(self.row,2,1)
grid.addWidget(QLabel("Column:"),2,2,alignment=Qt.AlignmentFlag.AlignRight)
self.column = QSpinBox()
self.column.setObjectName("column")
grid.addWidget(self.column,2,3)
def parse_form(self):
return dict(
sheets = self.sheet.text().split(","),
row = self.row.value(),
column = self.column.value()
)

View File

@@ -62,12 +62,13 @@ class SubmissionFormContainer(QWidget):
self.app.result_reporter() self.app.result_reporter()
def scrape_reagents(self, *args, **kwargs): def scrape_reagents(self, *args, **kwargs):
print(f"\n\n{inspect.stack()[1].function}\n\n") caller = inspect.stack()[1].function.__repr__().replace("'", "")
self.scrape_reagents_function(args[0]) logger.debug(f"Args: {args}, kwargs: {kwargs}")
self.scrape_reagents_function(args[0], caller=caller)
self.kit_integrity_completion() self.kit_integrity_completion()
self.app.report.add_result(self.report) self.app.report.add_result(self.report)
self.report = Report() self.report = Report()
match inspect.stack()[1].function: match inspect.stack()[1].function.__repr__():
case "import_submission_function": case "import_submission_function":
pass pass
case _: case _:
@@ -83,7 +84,7 @@ class SubmissionFormContainer(QWidget):
self.kit_integrity_completion_function() self.kit_integrity_completion_function()
self.app.report.add_result(self.report) self.app.report.add_result(self.report)
self.report = Report() self.report = Report()
match inspect.stack()[1].function: match inspect.stack()[1].function.__repr__():
case "import_submission_function": case "import_submission_function":
pass pass
case _: case _:
@@ -161,7 +162,7 @@ class SubmissionFormContainer(QWidget):
logger.debug(f"Outgoing report: {self.report.results}") logger.debug(f"Outgoing report: {self.report.results}")
logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}") logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
def scrape_reagents_function(self, extraction_kit:str): def scrape_reagents_function(self, extraction_kit:str, caller:str|None=None):
""" """
Extracted scrape reagents function that will run when Extracted scrape reagents function that will run when
form 'extraction_kit' widget is updated. form 'extraction_kit' widget is updated.
@@ -173,6 +174,9 @@ class SubmissionFormContainer(QWidget):
Returns: Returns:
Tuple[QMainWindow, dict]: Updated application and result Tuple[QMainWindow, dict]: Updated application and result
""" """
self.form.reagents = []
logger.debug(f"\n\n{caller}\n\n")
# assert caller == "import_submission_function"
report = Report() report = Report()
logger.debug(f"Extraction kit: {extraction_kit}") logger.debug(f"Extraction kit: {extraction_kit}")
# obj.reagents = [] # obj.reagents = []
@@ -195,7 +199,15 @@ class SubmissionFormContainer(QWidget):
# obj.reagents.append(reagent) # obj.reagents.append(reagent)
# else: # else:
# obj.missing_reagents.append(reagent) # obj.missing_reagents.append(reagent)
self.form.reagents = self.prsr.sub['reagents'] match caller:
case "import_submission_function":
self.form.reagents = self.prsr.sub['reagents']
case _:
already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing]
names = list(set([item.type for item in already_have]))
logger.debug(f"reagents: {already_have}")
reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.pyd.submission_type) if item.name not in names]
self.form.reagents = already_have + reagents
# logger.debug(f"Imported reagents: {obj.reagents}") # logger.debug(f"Imported reagents: {obj.reagents}")
# logger.debug(f"Missing reagents: {obj.missing_reagents}") # logger.debug(f"Missing reagents: {obj.missing_reagents}")
self.report.add_result(report) self.report.add_result(report)
@@ -221,6 +233,7 @@ class SubmissionFormContainer(QWidget):
self.ext_kit = kit_widget.currentText() self.ext_kit = kit_widget.currentText()
# for reagent in obj.pyd.reagents: # for reagent in obj.pyd.reagents:
for reagent in self.form.reagents: for reagent in self.form.reagents:
logger.debug(f"Creating widget for {reagent}")
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit) add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
# add_widget.setParent(sub_form_container.form) # add_widget.setParent(sub_form_container.form)
self.form.layout().addWidget(add_widget) self.form.layout().addWidget(add_widget)

View File

@@ -32,10 +32,10 @@
visibility: visible; visibility: visible;
font-size: large; font-size: large;
} }
</style> </style>
<title>Submission Details for {{ sub['Plate Number'] }}</title> <title>Submission Details for {{ sub['Plate Number'] }}</title>
</head> </head>
{% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map'] %} {% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %}
<body> <body>
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>&nbsp;&nbsp;&nbsp;{% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %} <h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>&nbsp;&nbsp;&nbsp;{% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
<p>{% for key, value in sub.items() if key not in excluded %} <p>{% for key, value in sub.items() if key not in excluded %}
@@ -45,6 +45,12 @@
<p>{% for item in sub['reagents'] %} <p>{% for item in sub['reagents'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['type'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['type'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
{% endfor %}</p> {% endfor %}</p>
{% if sub['equipment'] %}
<h3><u>Equipment:</u></h3>
<p>{% for item in sub['equipment'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['name'] }}:</b> {{ item['asset_number']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}<br>
{% endfor %}</p>
{% endif %}
{% if sub['samples'] %} {% if sub['samples'] %}
<h3><u>Samples:</u></h3> <h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %} <p>{% for item in sub['samples'] %}

View File

@@ -3,7 +3,6 @@ Contains miscellaenous functions used by both frontend and backend.
''' '''
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
import re
import numpy as np import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect import logging, re, yaml, sys, os, stat, platform, getpass, inspect
import pandas as pd import pandas as pd
@@ -99,13 +98,6 @@ def check_regex_match(pattern:str, check:str) -> bool:
except TypeError: except TypeError:
return False return False
# def massage_common_reagents(reagent_name:str):
# logger.debug(f"Attempting to massage {reagent_name}")
# if reagent_name.endswith("water") or "H2O" in reagent_name.upper():
# reagent_name = "molecular_grade_water"
# reagent_name = reagent_name.replace("µ", "u")
# return reagent_name
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler): class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
def doRollover(self): def doRollover(self):
@@ -143,7 +135,7 @@ class Settings(BaseSettings):
Pydantic model to hold settings Pydantic model to hold settings
Raises: Raises:
FileNotFoundError: _description_ FileNotFoundError: Error if database not found.
""" """
directory_path: Path directory_path: Path
@@ -516,4 +508,10 @@ def readInChunks(fileObj, chunkSize=2048):
def get_first_blank_df_row(df:pd.DataFrame) -> int: def get_first_blank_df_row(df:pd.DataFrame) -> int:
return len(df) + 1 return len(df) + 1
def is_missing(value:Any) -> Tuple[Any, bool]:
if check_not_nan(value):
return value, False
else:
return convert_nans_to_nones(value), True
ctx = get_config(None) ctx = get_config(None)