Context menu for runs working.

This commit is contained in:
lwark
2025-05-22 10:00:25 -05:00
parent 75c665ea05
commit d850166e08
40 changed files with 2852 additions and 3329 deletions

View File

@@ -34,7 +34,7 @@ templates_path = ['_templates']
exclude_patterns = [] exclude_patterns = []
sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src").__str__()) sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src").__str__())
sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src/submissions").__str__()) sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src/procedure").__str__())
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
@@ -44,4 +44,4 @@ html_theme = 'alabaster'
html_static_path = ['_static'] html_static_path = ['_static']
# autodoc_mock_imports = ["backend.db.models.submissions"] # autodoc_mock_imports = ["backend.db.models.procedure"]

View File

@@ -15,7 +15,7 @@ def goodbye(ctx):
""" """
For scripts to be run, they must be added to the _configitem.startup_scripts or _configitem.teardown_scripts For scripts to be procedure, they must be added to the _configitem.startup_scripts or _configitem.teardown_scripts
rows as a key: value (name: null) entry in the JSON. rows as a key: value (name: null) entry in the JSON.
ex: {"goodbye": null, "backup_database": null} ex: {"goodbye": null, "backup_database": null}
The program will overwrite null with the actual function upon startup. The program will overwrite null with the actual function upon startup.

View File

@@ -15,7 +15,7 @@ def hello(ctx) -> None:
""" """
For scripts to be run, they must be added to the _configitem.startup_scripts or _configitem.teardown_scripts For scripts to be procedure, they must be added to the _configitem.startup_scripts or _configitem.teardown_scripts
rows as a key: value (name: null) entry in the JSON. rows as a key: value (name: null) entry in the JSON.
ex: {"hello": null, "import_irida": null} ex: {"hello": null, "import_irida": null}
The program will overwrite null with the actual function upon startup. The program will overwrite null with the actual function upon startup.

View File

@@ -5,29 +5,29 @@ from tools import Settings
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"procedure.{__name__}")
def import_irida(ctx: Settings): def import_irida(ctx: Settings):
""" """
Grabs Irida controls from secondary database. Grabs Irida control from secondary database.
Args: Args:
ctx (Settings): Settings inherited from app. ctx (Settings): Settings inherited from app.
""" """
from backend import BasicSample from backend import Sample
from backend.db import IridaControl, ControlType from backend.db import IridaControl, ControlType
# NOTE: Because the main session will be busy in another thread, this requires a new session. # NOTE: Because the main session will be busy in another thread, this requires a new session.
new_session = Session(ctx.database_session.get_bind()) new_session = Session(ctx.database_session.get_bind())
ct = new_session.query(ControlType).filter(ControlType.name == "Irida Control").first() ct = new_session.query(ControlType).filter(ControlType.name == "Irida Control").first()
existing_controls = [item.name for item in new_session.query(IridaControl)] existing_controls = [item.name for item in new_session.query(IridaControl)]
prm_list = ", ".join([f"'{thing}'" for thing in existing_controls]) prm_list = ", ".join([f"'{thing}'" for thing in existing_controls])
ctrl_db_path = ctx.directory_path.joinpath("submissions_parser_output", "submissions.db") ctrl_db_path = ctx.directory_path.joinpath("submissions_parser_output", "procedure.db")
try: try:
conn = sqlite3.connect(ctrl_db_path) conn = sqlite3.connect(ctrl_db_path)
except AttributeError as e: except AttributeError as e:
logger.error(f"Error, could not import from irida due to {e}") logger.error(f"Error, could not import from irida due to {e}")
return return
sql = "SELECT name, submitted_date, run_id, contains, matches, kraken, subtype, refseq_version, " \ sql = "SELECT name, submitted_date, procedure_id, contains, matches, kraken, subtype, refseq_version, " \
"kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \ "kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \
f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})" f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})"
cursor = conn.execute(sql) cursor = conn.execute(sql)
@@ -49,15 +49,15 @@ def import_irida(ctx: Settings):
record['submitted_date'] = datetime.strptime(record['submitted_date'], "%Y-%m-%d %H:%M:%S.%f") record['submitted_date'] = datetime.strptime(record['submitted_date'], "%Y-%m-%d %H:%M:%S.%f")
assert isinstance(record['submitted_date'], datetime) assert isinstance(record['submitted_date'], datetime)
instance = IridaControl(controltype=ct, **record) instance = IridaControl(controltype=ct, **record)
sample = new_session.query(BasicSample).filter(BasicSample.submitter_id == instance.name).first() sample = new_session.query(Sample).filter(Sample.sample_id == instance.name).first()
if sample: if sample:
instance.sample = sample instance.sample = sample
try: try:
instance.submission = sample.submissions[0] instance.clientsubmission = sample.procedure[0]
except IndexError: except IndexError:
logger.error(f"Could not get sample for {sample}") logger.error(f"Could not get sample for {sample}")
instance.submission = None instance.clientsubmission = None
# instance.run = sample.run[0] # instance.procedure = sample.procedure[0]
new_session.add(instance) new_session.add(instance)
new_session.commit() new_session.commit()
new_session.close() new_session.close()

View File

@@ -22,7 +22,7 @@ def get_week_of_month() -> int:
# Automatically completes project info for help menu and compiling. # Automatically completes project info for help menu and compiling.
__project__ = "submissions" __project__ = "procedure"
__version__ = f"{year}{str(month).zfill(2)}.{get_week_of_month()}b" __version__ = f"{year}{str(month).zfill(2)}.{get_week_of_month()}b"
__author__ = {"name": "Landon Wark", "email": "Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name": "Landon Wark", "email": "Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = f"2022-{year}, Government of Canada" __copyright__ = f"2022-{year}, Government of Canada"

View File

@@ -55,7 +55,7 @@ def update_log(mapper, connection, target):
continue continue
added = [str(item) for item in hist.added] added = [str(item) for item in hist.added]
# NOTE: Attributes left out to save space # NOTE: Attributes left out to save space
# if attr.key in ['artic_technician', 'submission_sample_associations', 'submission_reagent_associations', # if attr.key in ['artic_technician', 'clientsubmissionsampleassociation', 'submission_reagent_associations',
# 'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info', # 'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
# 'gel_controls', 'source_plates']: # 'gel_controls', 'source_plates']:
if attr.key in LogMixin.tracking_exclusion: if attr.key in LogMixin.tracking_exclusion:

View File

@@ -3,6 +3,8 @@ Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging import sys, logging
from dateutil.parser import parse
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy import Column, INTEGER, String, JSON
@@ -21,7 +23,7 @@ if 'pytest' in sys.modules:
# NOTE: For inheriting in LogMixin # NOTE: For inheriting in LogMixin
Base: DeclarativeMeta = declarative_base() Base: DeclarativeMeta = declarative_base()
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"procedure.{__name__}")
class BaseClass(Base): class BaseClass(Base):
@@ -33,12 +35,12 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns __table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
singles = ['id'] singles = ['id']
omni_removes = ["id", 'runs', "omnigui_class_dict", "omnigui_instance_dict"] omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"]
omni_sort = ["name"] omni_sort = ["name"]
omni_inheritable = [] omni_inheritable = []
searchables = [] searchables = []
misc_info = Column(JSON) _misc_info = Column(JSON)
def __repr__(self) -> str: def __repr__(self) -> str:
try: try:
@@ -122,6 +124,10 @@ class BaseClass(Base):
from test_settings import ctx from test_settings import ctx
return ctx.backup_path return ctx.backup_path
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._misc_info = dict()
@classproperty @classproperty
def jsons(cls) -> List[str]: def jsons(cls) -> List[str]:
""" """
@@ -130,7 +136,10 @@ class BaseClass(Base):
Returns: Returns:
List[str]: List of column names List[str]: List of column names
""" """
try:
return [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)] return [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
except AttributeError:
return []
@classproperty @classproperty
def timestamps(cls) -> List[str]: def timestamps(cls) -> List[str]:
@@ -140,7 +149,10 @@ class BaseClass(Base):
Returns: Returns:
List[str]: List of column names List[str]: List of column names
""" """
try:
return [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)] return [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
except AttributeError:
return []
@classmethod @classmethod
def get_default_info(cls, *args) -> dict | list | str: def get_default_info(cls, *args) -> dict | list | str:
@@ -198,11 +210,11 @@ class BaseClass(Base):
@classmethod @classmethod
def results_to_df(cls, objects: list | None = None, **kwargs) -> DataFrame: def results_to_df(cls, objects: list | None = None, **kwargs) -> DataFrame:
""" """
Converts class sub_dicts into a Dataframe for all controls of the class. Converts class sub_dicts into a Dataframe for all control of the class.
Args: Args:
objects (list): Objects to be converted to dataframe. objects (list): Objects to be converted to dataframe.
**kwargs (): Arguments necessary for the to_sub_dict method. eg extraction_kit=X **kwargs (): Arguments necessary for the to_sub_dict method. eg kittype=X
Returns: Returns:
Dataframe Dataframe
@@ -219,6 +231,24 @@ class BaseClass(Base):
records = [{k: v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects] records = [{k: v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects]
return DataFrame.from_records(records) return DataFrame.from_records(records)
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[Any, bool]:
new = False
allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute)
and not isinstance(v.property, _RelationshipDeclared)]
sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed}
logger.debug(f"Sanitized kwargs: {sanitized_kwargs}")
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
logger.debug(f"QorC Setting {k} to {v}")
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}, new: {new}")
return instance, new
@classmethod @classmethod
def query(cls, **kwargs) -> Any | List[Any]: def query(cls, **kwargs) -> Any | List[Any]:
""" """
@@ -227,6 +257,8 @@ class BaseClass(Base):
Returns: Returns:
Any | List[Any]: Result of query execution. Any | List[Any]: Result of query execution.
""" """
if "name" in kwargs.keys():
kwargs['limit'] = 1
return cls.execute_query(**kwargs) return cls.execute_query(**kwargs)
@classmethod @classmethod
@@ -243,16 +275,17 @@ class BaseClass(Base):
Any | List[Any]: Single result if limit = 1 or List if other. Any | List[Any]: Single result if limit = 1 or List if other.
""" """
# logger.debug(f"Kwargs: {kwargs}") # logger.debug(f"Kwargs: {kwargs}")
if model is None: # if model is None:
model = cls # model = cls
# logger.debug(f"Model: {model}") # logger.debug(f"Model: {model}")
if query is None: if query is None:
query: Query = cls.__database_session__.query(model) query: Query = cls.__database_session__.query(cls)
singles = model.get_default_info('singles') singles = cls.get_default_info('singles')
for k, v in kwargs.items(): for k, v in kwargs.items():
logger.info(f"Using key: {k} with value: {v}") logger.info(f"Using key: {k} with value: {v}")
try: try:
attr = getattr(model, k) attr = getattr(cls, k)
# NOTE: account for attrs that use list. # NOTE: account for attrs that use list.
if attr.property.uselist: if attr.property.uselist:
query = query.filter(attr.contains(v)) query = query.filter(attr.contains(v))
@@ -341,6 +374,26 @@ class BaseClass(Base):
""" """
return dict() return dict()
@classproperty
def details_template(cls) -> Template:
"""
Get the details jinja template for the correct class
Args:
base_dict (dict): incoming dictionary of Submission fields
Returns:
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
"""
env = jinja_template_loading()
temp_name = f"{cls.__name__.lower()}_details.html"
try:
template = env.get_template(temp_name)
except TemplateNotFound as e:
# logger.error(f"Couldn't find template {e}")
template = env.get_template("details.html")
return template
def check_all_attributes(self, attributes: dict) -> bool: def check_all_attributes(self, attributes: dict) -> bool:
""" """
Checks this instance against a dictionary of attributes to determine if they are a match. Checks this instance against a dictionary of attributes to determine if they are a match.
@@ -405,15 +458,29 @@ class BaseClass(Base):
""" """
Custom dunder method to handle potential list relationship issues. Custom dunder method to handle potential list relationship issues.
""" """
# logger.debug(f"Attempting to set: {key} to {value}")
if key.startswith("_"):
return super().__setattr__(key, value)
try:
check = not hasattr(self, key)
except:
return
if check:
try:
json.dumps(value)
except TypeError:
value = str(value)
self._misc_info.update({key: value})
return
try: try:
field_type = getattr(self.__class__, key) field_type = getattr(self.__class__, key)
except AttributeError: except AttributeError:
return super().__setattr__(key, value) return super().__setattr__(key, value)
if isinstance(field_type, InstrumentedAttribute): if isinstance(field_type, InstrumentedAttribute):
logger.debug(f"{key} is an InstrumentedAttribute.") # logger.debug(f"{key} is an InstrumentedAttribute.")
match field_type.property: match field_type.property:
case ColumnProperty(): case ColumnProperty():
logger.debug(f"Setting ColumnProperty to {value}") # logger.debug(f"Setting ColumnProperty to {value}")
return super().__setattr__(key, value) return super().__setattr__(key, value)
case _RelationshipDeclared(): case _RelationshipDeclared():
logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}") logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}")
@@ -446,10 +513,13 @@ class BaseClass(Base):
try: try:
return super().__setattr__(key, value) return super().__setattr__(key, value)
except AttributeError: except AttributeError:
logger.debug(f"Possible attempt to set relationship to simple var type.") logger.debug(f"Possible attempt to set relationship {key} to simple var type. {value}")
relationship_class = field_type.property.entity.entity relationship_class = field_type.property.entity.entity
value = relationship_class.query(name=value) value = relationship_class.query(name=value)
try:
return super().__setattr__(key, value) return super().__setattr__(key, value)
except AttributeError:
return super().__setattr__(key, None)
case _: case _:
return super().__setattr__(key, value) return super().__setattr__(key, value)
else: else:
@@ -458,7 +528,7 @@ class BaseClass(Base):
def delete(self): def delete(self):
logger.error(f"Delete has not been implemented for {self.__class__.__name__}") logger.error(f"Delete has not been implemented for {self.__class__.__name__}")
def rectify_query_date(input_date, eod: bool = False) -> str: def rectify_query_date(input_date: datetime, eod: bool = False) -> str:
""" """
Converts input into a datetime string for querying purposes Converts input into a datetime string for querying purposes
@@ -486,8 +556,7 @@ class BaseClass(Base):
class LogMixin(Base): class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',
tracking_exclusion: ClassVar = ['artic_technician', 'submission_sample_associations',
'submission_reagent_associations', 'submission_equipment_associations', 'submission_reagent_associations', 'submission_equipment_associations',
'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls', 'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls',
'source_plates'] 'source_plates']
@@ -540,13 +609,12 @@ class ConfigItem(BaseClass):
from .controls import * from .controls import *
# NOTE: import order must go: orgs, kit, runs due to circular import issues # NOTE: import order must go: orgs, kittype, run due to circular import issues
from .organizations import * from .organizations import *
from .runs import *
from .kits import * from .kits import *
from .submissions import * from .submissions import *
from .audit import AuditLog from .audit import AuditLog
# NOTE: Add a creator to the run for reagent association. Assigned here due to circular import constraints. # NOTE: Add a creator to the procedure for reagent association. Assigned here due to circular import constraints.
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator # https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator
Procedure.reagents.creator = lambda reg: ProcedureReagentAssociation(reagent=reg) # Procedure.reagents.creator = lambda reg: ProcedureReagentAssociation(reagent=reg)

View File

@@ -27,7 +27,7 @@ class ControlType(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. Irida Control) name = Column(String(255), unique=True) #: controltype name (e.g. Irida Control)
targets = Column(JSON) #: organisms checked for targets = Column(JSON) #: organisms checked for
controls = relationship("Control", back_populates="controltype") #: control samples created of this type. control = relationship("Control", back_populates="controltype") #: control sample created of this type.
@classmethod @classmethod
@setup_lookup @setup_lookup
@@ -59,16 +59,16 @@ class ControlType(BaseClass):
Get subtypes associated with this controltype (currently used only for Kraken) Get subtypes associated with this controltype (currently used only for Kraken)
Args: Args:
mode (str): analysis mode sub_type mode (str): analysis mode submissiontype
Returns: Returns:
List[str]: list of subtypes available List[str]: list of subtypes available
""" """
if not self.controls: if not self.control:
return return
# NOTE: Get first instance since all should have same subtypes # NOTE: Get first instance since all should have same subtypes
# NOTE: Get mode of instance # NOTE: Get mode of instance
jsoner = getattr(self.controls[0], mode) jsoner = getattr(self.control[0], mode)
try: try:
# NOTE: Pick genera (all should have same subtypes) # NOTE: Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0] genera = list(jsoner.keys())[0]
@@ -79,7 +79,7 @@ class ControlType(BaseClass):
return subtypes return subtypes
@property @property
def instance_class(self) -> Control: def control_class(self) -> Control:
""" """
Retrieves the Control class associated with this controltype Retrieves the Control class associated with this controltype
@@ -119,27 +119,27 @@ class Control(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
controltype_name = Column(String, ForeignKey("_controltype.name", ondelete="SET NULL", controltype_name = Column(String, ForeignKey("_controltype.name", ondelete="SET NULL",
name="fk_BC_subtype_name")) #: name of joined run type name="fk_BC_subtype_name")) #: name of joined procedure type
controltype = relationship("ControlType", back_populates="controls", controltype = relationship("ControlType", back_populates="control",
foreign_keys=[controltype_name]) #: reference to parent control type foreign_keys=[controltype_name]) #: reference to parent control type
name = Column(String(255), unique=True) #: Sample ID name = Column(String(255), unique=True) #: Sample ID
sample_id = Column(String, ForeignKey("_basicsample.id", ondelete="SET NULL", sample_id = Column(String, ForeignKey("_sample.id", ondelete="SET NULL",
name="fk_Cont_sample_id")) #: name of joined run type name="fk_Cont_sample_id")) #: name of joined procedure type
sample = relationship("BasicSample", back_populates="control") #: This control's run sample sample = relationship("Sample", back_populates="control") #: This control's procedure sample
submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics
procedure_id = Column(INTEGER, ForeignKey("_procedure.id")) #: parent run id procedure_id = Column(INTEGER, ForeignKey("_procedure.id")) #: parent procedure id
procedure = relationship("Procedure", back_populates="controls", procedure = relationship("Procedure", back_populates="control",
foreign_keys=[procedure_id]) #: parent run foreign_keys=[procedure_id]) #: parent procedure
__mapper_args__ = { # __mapper_args__ = {
"polymorphic_identity": "Basic Control", # "polymorphic_identity": "Basic Control",
"polymorphic_on": case( # "polymorphic_on": case(
(controltype_name == "PCR Control", "PCR Control"), # (controltype_name == "PCR Control", "PCR Control"),
(controltype_name == "Irida Control", "Irida Control"), # (controltype_name == "Irida Control", "Irida Control"),
else_="Basic Control" # else_="Basic Control"
), # ),
"with_polymorphic": "*", # "with_polymorphic": "*",
} # }
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.controltype_name}({self.name})>" return f"<{self.controltype_name}({self.name})>"
@@ -284,448 +284,448 @@ class Control(BaseClass):
self.__database_session__.commit() self.__database_session__.commit()
class PCRControl(Control): # class PCRControl(Control):
""" # """
Class made to hold info from Design & Analysis software. # Class made to hold info from Design & Analysis software.
""" # """
#
id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True) # id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
subtype = Column(String(16)) #: PC or NC # subtype = Column(String(16)) #: PC or NC
target = Column(String(16)) #: N1, N2, etc. # target = Column(String(16)) #: N1, N2, etc.
ct = Column(FLOAT) #: PCR result # ct = Column(FLOAT) #: PCR result
reagent_lot = Column(String(64), ForeignKey("_reagent.lot", ondelete="SET NULL", # reagent_lot = Column(String(64), ForeignKey("_reagent.lot", ondelete="SET NULL",
name="fk_reagent_lot")) # name="fk_reagent_lot"))
reagent = relationship("Reagent", foreign_keys=reagent_lot) #: reagent used for this control # reagent = relationship("Reagent", foreign_keys=reagent_lot) #: reagent used for this control
#
__mapper_args__ = dict(polymorphic_identity="PCR Control", # __mapper_args__ = dict(polymorphic_identity="PCR Control",
polymorphic_load="inline", # polymorphic_load="inline",
inherit_condition=(id == Control.id)) # inherit_condition=(id == Control.id))
#
def to_sub_dict(self) -> dict: # def to_sub_dict(self) -> dict:
""" # """
Creates dictionary of fields for this object. # Creates dictionary of fields for this object.
#
Returns: # Returns:
dict: Output dict of name, ct, subtype, target, reagent_lot and submitted_date # dict: Output dict of name, ct, subtype, target, reagent_lot and submitted_date
""" # """
return dict( # return dict(
name=self.name, # name=self.name,
ct=self.ct, # ct=self.ct,
subtype=self.subtype, # subtype=self.subtype,
target=self.target, # target=self.target,
reagent_lot=self.reagent_lot, # reagent_lot=self.reagent_lot,
submitted_date=self.submitted_date.date() # submitted_date=self.submitted_date.date()
) # )
#
@classmethod # @classmethod
@report_result # @report_result
def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]: # def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
""" # """
Creates a PCRFigure. Overrides parent # Creates a PCRFigure. Overrides parent
#
Args: # Args:
parent (__type__): Widget to contain the chart. # parent (__type__): Widget to contain the chart.
chart_settings (dict): settings passed down from chart widget # chart_settings (dict): settings passed down from chart widget
ctx (Settings): settings passed down from gui. Not used here. # ctx (Settings): settings passed down from gui. Not used here.
#
Returns: # Returns:
Tuple[Report, "PCRFigure"]: Report of status and resulting figure. # Tuple[Report, "PCRFigure"]: Report of status and resulting figure.
""" # """
from frontend.visualizations.pcr_charts import PCRFigure # from frontend.visualizations.pcr_charts import PCRFigure
parent.mode_typer.clear() # parent.mode_typer.clear()
parent.mode_typer.setEnabled(False) # parent.mode_typer.setEnabled(False)
report = Report() # report = Report()
controls = cls.query(proceduretype=chart_settings['sub_type'], start_date=chart_settings['start_date'], # control = cls.query(proceduretype=chart_settings['submissiontype'], start_date=chart_settings['start_date'],
end_date=chart_settings['end_date']) # end_date=chart_settings['end_date'])
data = [control.to_sub_dict() for control in controls] # data = [control.to_sub_dict() for control in control]
df = DataFrame.from_records(data) # df = DataFrame.from_records(data)
# NOTE: Get all PCR controls with ct over 0 # # NOTE: Get all PCR control with ct over 0
try: # try:
df = df[df.ct > 0.0] # df = df[df.ct > 0.0]
except AttributeError: # except AttributeError:
df = df # df = df
fig = PCRFigure(df=df, modes=[], settings=chart_settings) # fig = PCRFigure(df=df, modes=[], settings=chart_settings)
return report, fig # return report, fig
#
def to_pydantic(self): # def to_pydantic(self):
from backend.validators import PydPCRControl # from backend.validators import PydPCRControl
return PydPCRControl(**self.to_sub_dict(), # return PydPCRControl(**self.to_sub_dict(),
controltype_name=self.controltype_name, # controltype_name=self.controltype_name,
submission_id=self.submission_id) # clientsubmission_id=self.clientsubmission_id)
#
#
class IridaControl(Control): # class IridaControl(Control):
subtyping_allowed = ['kraken'] # subtyping_allowed = ['kraken']
#
id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True) # id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism # contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism
matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism # matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
kraken = Column(JSON) #: unstructured output from kraken_report # kraken = Column(JSON) #: unstructured output from kraken_report
subtype = Column(String(16), nullable=False) #: EN-NOS, MCS-NOS, etc # subtype = Column(String(16), nullable=False) #: EN-NOS, MCS-NOS, etc
refseq_version = Column(String(16)) #: version of refseq used in fastq parsing # refseq_version = Column(String(16)) #: version of refseq used in fastq parsing
kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing # kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing
kraken2_db_version = Column(String(32)) #: folder name of kraken2 db # kraken2_db_version = Column(String(32)) #: folder name of kraken2 db
sample_id = Column(INTEGER, # sample_id = Column(INTEGER,
ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key # ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
#
__mapper_args__ = dict(polymorphic_identity="Irida Control", # __mapper_args__ = dict(polymorphic_identity="Irida Control",
polymorphic_load="inline", # polymorphic_load="inline",
inherit_condition=(id == Control.id)) # inherit_condition=(id == Control.id))
#
@property # @property
def targets(self): # def targets(self):
if self.controltype.targets: # if self.controltype.targets:
return list(itertools.chain.from_iterable([value for key, value in self.controltype.targets.items() # return list(itertools.chain.from_iterable([value for key, value in self.controltype.targets.items()
if key == self.subtype])) # if key == self.subtype]))
else: # else:
return ["None"] # return ["None"]
#
@validates("subtype") # @validates("subtype")
def enforce_subtype_literals(self, key: str, value: str) -> str: # def enforce_subtype_literals(self, key: str, value: str) -> str:
""" # """
Validates sub_type field with acceptable values # Validates submissiontype field with acceptable values
#
Args: # Args:
key (str): Field name # key (str): Field name
value (str): Field Value # value (str): Field Value
#
Raises: # Raises:
KeyError: Raised if value is not in the acceptable list. # KeyError: Raised if value is not in the acceptable list.
#
Returns: # Returns:
str: Validated string. # str: Validated string.
""" # """
acceptables = ['ATCC49226', 'ATCC49619', 'EN-NOS', "EN-SSTI", "MCS-NOS", "MCS-SSTI", "SN-NOS", "SN-SSTI"] # acceptables = ['ATCC49226', 'ATCC49619', 'EN-NOS', "EN-SSTI", "MCS-NOS", "MCS-SSTI", "SN-NOS", "SN-SSTI"]
if value.upper() not in acceptables: # if value.upper() not in acceptables:
raise KeyError(f"Sub-type must be in {acceptables}") # raise KeyError(f"Sub-type must be in {acceptables}")
return value # return value
#
def to_sub_dict(self) -> dict: # def to_sub_dict(self) -> dict:
""" # """
Converts object into convenient dictionary for use in run summary # Converts object into convenient dictionary for use in procedure summary
#
Returns: # Returns:
dict: output dictionary containing: Name, Type, Targets, Top Kraken results # dict: output dictionary containing: Name, Type, Targets, Top Kraken results
""" # """
try: # try:
kraken = self.kraken # kraken = self.kraken
except TypeError: # except TypeError:
kraken = {} # kraken = {}
try: # try:
kraken_cnt_total = sum([item['kraken_count'] for item in kraken.values()]) # kraken_cnt_total = sum([item['kraken_count'] for item in kraken.values()])
except AttributeError: # except AttributeError:
kraken_cnt_total = 0 # kraken_cnt_total = 0
try: # try:
new_kraken = [dict(name=key, kraken_count=value['kraken_count'], # new_kraken = [dict(name=key, kraken_count=value['kraken_count'],
kraken_percent=f"{value['kraken_count'] / kraken_cnt_total:0.2%}", # kraken_percent=f"{value['kraken_count'] / kraken_cnt_total:0.2%}",
target=key in self.controltype.targets) # target=key in self.controltype.targets)
for key, value in kraken.items()] # for key, value in kraken.items()]
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)[0:10] # new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)[0:10]
except (AttributeError, ZeroDivisionError): # except (AttributeError, ZeroDivisionError):
new_kraken = [] # new_kraken = []
output = dict( # output = dict(
name=self.name, # name=self.name,
type=self.controltype.name, # type=self.controltype.name,
targets=", ".join(self.targets), # targets=", ".join(self.targets),
kraken=new_kraken # kraken=new_kraken
) # )
return output # return output
#
def convert_by_mode(self, control_sub_type: str, mode: Literal['kraken', 'matches', 'contains'], # def convert_by_mode(self, control_sub_type: str, mode: Literal['kraken', 'matches', 'contains'],
consolidate: bool = False) -> Generator[dict, None, None]: # consolidate: bool = False) -> Generator[dict, None, None]:
""" # """
split this instance into analysis types ('kraken', 'matches', 'contains') for controls graphs # split this instance into analysis types ('kraken', 'matches', 'contains') for control graphs
#
Args: # Args:
consolidate (bool): whether to merge all off-target genera. Defaults to False # consolidate (bool): whether to merge all off-target genera. Defaults to False
control_sub_type (str): control subtype, 'MCS-NOS', etc. # control_sub_type (str): control subtype, 'MCS-NOS', etc.
mode (Literal['kraken', 'matches', 'contains']): analysis type, 'contains', etc. # mode (Literal['kraken', 'matches', 'contains']): analysis type, 'contains', etc.
#
Returns: # Returns:
List[dict]: list of records # List[dict]: list of records
""" # """
try: # try:
data = self.__getattribute__(mode) # data = self.__getattribute__(mode)
except TypeError: # except TypeError:
data = {} # data = {}
if data is None: # if data is None:
data = {} # data = {}
# NOTE: Data truncation and consolidation. # # NOTE: Data truncation and consolidation.
if "kraken" in mode: # if "kraken" in mode:
data = {k: v for k, v in sorted(data.items(), key=lambda d: d[1][f"{mode}_count"], reverse=True)[:50]} # data = {k: v for k, v in sorted(data.items(), key=lambda d: d[1][f"{mode}_count"], reverse=True)[:50]}
else: # else:
if consolidate: # if consolidate:
on_tar = {k: v for k, v in data.items() if k.strip("*") in self.controltype.targets[control_sub_type]} # on_tar = {k: v for k, v in data.items() if k.strip("*") in self.controltype.targets[control_sub_type]}
off_tar = sum(v[f'{mode}_ratio'] for k, v in data.items() if # off_tar = sum(v[f'{mode}_ratio'] for k, v in data.items() if
k.strip("*") not in self.controltype.targets[control_sub_type]) # k.strip("*") not in self.controltype.targets[control_sub_type])
on_tar['Off-target'] = {f"{mode}_ratio": off_tar} # on_tar['Off-target'] = {f"{mode}_ratio": off_tar}
data = on_tar # data = on_tar
for genus in data: # for genus in data:
_dict = dict( # _dict = dict(
name=self.name, # name=self.name,
submitted_date=self.submitted_date, # submitted_date=self.submitted_date,
genus=genus, # genus=genus,
target='Target' if genus.strip("*") in self.controltype.targets[control_sub_type] else "Off-target" # target='Target' if genus.strip("*") in self.controltype.targets[control_sub_type] else "Off-target"
) # )
for key in data[genus]: # for key in data[genus]:
_dict[key] = data[genus][key] # _dict[key] = data[genus][key]
yield _dict # yield _dict
#
@classproperty # @classproperty
def modes(cls) -> List[str]: # def modes(cls) -> List[str]:
""" # """
Get all control modes from database # Get all control modes from database
#
Returns: # Returns:
List[str]: List of control mode names. # List[str]: List of control mode names.
""" # """
try: # try:
cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)] # cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
except AttributeError as e: # except AttributeError as e:
logger.error(f"Failed to get available modes from db: {e}") # logger.error(f"Failed to get available modes from db: {e}")
cols = [] # cols = []
return cols # return cols
#
@classmethod # @classmethod
def make_parent_buttons(cls, parent: QWidget) -> None: # def make_parent_buttons(cls, parent: QWidget) -> None:
""" # """
Creates buttons for controlling # Creates buttons for controlling
#
Args: # Args:
parent (QWidget): chart holding widget to add buttons to. # parent (QWidget): chart holding widget to add buttons to.
#
""" # """
super().make_parent_buttons(parent=parent) # super().make_parent_buttons(parent=parent)
rows = parent.layout.rowCount() - 2 # rows = parent.layout.rowCount() - 2
# NOTE: check box for consolidating off-target items # # NOTE: check box for consolidating off-target items
checker = QCheckBox(parent) # checker = QCheckBox(parent)
checker.setChecked(True) # checker.setChecked(True)
checker.setObjectName("irida_check") # checker.setObjectName("irida_check")
checker.setToolTip("Pools off-target genera to save time.") # checker.setToolTip("Pools off-target genera to save time.")
parent.layout.addWidget(QLabel("Consolidate Off-targets"), rows, 0, 1, 1) # parent.layout.addWidget(QLabel("Consolidate Off-targets"), rows, 0, 1, 1)
parent.layout.addWidget(checker, rows, 1, 1, 2) # parent.layout.addWidget(checker, rows, 1, 1, 2)
checker.checkStateChanged.connect(parent.update_data) # checker.checkStateChanged.connect(parent.update_data)
#
@classmethod # @classmethod
@report_result # @report_result
def make_chart(cls, chart_settings: dict, parent, ctx) -> Tuple[Report, "IridaFigure" | None]: # def make_chart(cls, chart_settings: dict, parent, ctx) -> Tuple[Report, "IridaFigure" | None]:
""" # """
Creates a IridaFigure. Overrides parent # Creates a IridaFigure. Overrides parent
#
Args: # Args:
parent (__type__): Widget to contain the chart. # parent (__type__): Widget to contain the chart.
chart_settings (dict): settings passed down from chart widget # chart_settings (dict): settings passed down from chart widget
ctx (Settings): settings passed down from gui. # ctx (Settings): settings passed down from gui.
#
Returns: # Returns:
Tuple[Report, "IridaFigure"]: Report of status and resulting figure. # Tuple[Report, "IridaFigure"]: Report of status and resulting figure.
""" # """
from frontend.visualizations import IridaFigure # from frontend.visualizations import IridaFigure
try: # try:
checker = parent.findChild(QCheckBox, name="irida_check") # checker = parent.findChild(QCheckBox, name="irida_check")
if chart_settings['mode'] == "kraken": # if chart_settings['mode'] == "kraken":
checker.setEnabled(False) # checker.setEnabled(False)
checker.setChecked(False) # checker.setChecked(False)
else: # else:
checker.setEnabled(True) # checker.setEnabled(True)
consolidate = checker.isChecked() # consolidate = checker.isChecked()
except AttributeError: # except AttributeError:
consolidate = False # consolidate = False
report = Report() # report = Report()
controls = cls.query(subtype=chart_settings['sub_type'], start_date=chart_settings['start_date'], # control = cls.query(subtype=chart_settings['submissiontype'], start_date=chart_settings['start_date'],
end_date=chart_settings['end_date']) # end_date=chart_settings['end_date'])
if not controls: # if not control:
report.add_result(Result(status="Critical", msg="No controls found in given date range.")) # report.add_result(Result(status="Critical", msg="No control found in given date range."))
return report, None # return report, None
# NOTE: change each control to list of dictionaries # # NOTE: change each control to list of dictionaries
data = [control.convert_by_mode(control_sub_type=chart_settings['sub_type'], mode=chart_settings['mode'], # data = [control.convert_by_mode(control_sub_type=chart_settings['submissiontype'], mode=chart_settings['mode'],
consolidate=consolidate) for # consolidate=consolidate) for
control in controls] # control in control]
# NOTE: flatten data to one dimensional list # # NOTE: flatten data to one dimensional list
# data = [item for sublist in data for item in sublist] # # data = [item for sublist in data for item in sublist]
data = flatten_list(data) # data = flatten_list(data)
if not data: # if not data:
report.add_result(Result(status="Critical", msg="No data found for controls in given date range.")) # report.add_result(Result(status="Critical", msg="No data found for control in given date range."))
return report, None # return report, None
df = cls.convert_data_list_to_df(input_df=data, sub_mode=chart_settings['sub_mode']) # df = cls.convert_data_list_to_df(input_df=data, sub_mode=chart_settings['sub_mode'])
if chart_settings['sub_mode'] is None: # if chart_settings['sub_mode'] is None:
title = chart_settings['sub_mode'] # title = chart_settings['sub_mode']
else: # else:
title = f"{chart_settings['mode']} - {chart_settings['sub_mode']}" # title = f"{chart_settings['mode']} - {chart_settings['sub_mode']}"
# NOTE: send dataframe to chart maker # # NOTE: send dataframe to chart maker
df, modes = cls.prep_df(ctx=ctx, df=df) # df, modes = cls.prep_df(ctx=ctx, df=df)
fig = IridaFigure(df=df, ytitle=title, modes=modes, parent=parent, # fig = IridaFigure(df=df, ytitle=title, modes=modes, parent=parent,
settings=chart_settings) # settings=chart_settings)
return report, fig # return report, fig
#
@classmethod # @classmethod
def convert_data_list_to_df(cls, input_df: list[dict], sub_mode) -> DataFrame: # def convert_data_list_to_df(cls, input_df: list[dict], sub_mode) -> DataFrame:
""" # """
Convert list of control records to dataframe # Convert list of control records to dataframe
#
Args: # Args:
input_df (list[dict]): list of dictionaries containing records # input_df (list[dict]): list of dictionaries containing records
sub_mode (str | None, optional): sub_type of run type. Defaults to None. # sub_mode (str | None, optional): submissiontype of procedure type. Defaults to None.
#
Returns: # Returns:
DataFrame: dataframe of controls # DataFrame: dataframe of control
""" # """
df = DataFrame.from_records(input_df) # df = DataFrame.from_records(input_df)
safe = ['name', 'submitted_date', 'genus', 'target'] # safe = ['name', 'submitted_date', 'genus', 'target']
for column in df.columns: # for column in df.columns:
if column not in safe: # if column not in safe:
if sub_mode is not None and column != sub_mode: # if sub_mode is not None and column != sub_mode:
continue # continue
else: # else:
safe.append(column) # safe.append(column)
if "percent" in column: # if "percent" in column:
try: # try:
count_col = next(item for item in df.columns if "count" in item) # count_col = next(item for item in df.columns if "count" in item)
except StopIteration: # except StopIteration:
continue # continue
# NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating. # # NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating.
df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum') # df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum')
df = df[[c for c in df.columns if c in safe]] # df = df[[c for c in df.columns if c in safe]]
# NOTE: move date of sample submitted on same date as previous ahead one. # # NOTE: move date of sample submitted on same date as previous ahead one.
df = cls.displace_date(df=df) # df = cls.displace_date(df=df)
# NOTE: ad hoc method to make data labels more accurate. # # NOTE: ad hoc method to make data labels more accurate.
df = cls.df_column_renamer(df=df) # df = cls.df_column_renamer(df=df)
return df # return df
#
@classmethod # @classmethod
def df_column_renamer(cls, df: DataFrame) -> DataFrame: # def df_column_renamer(cls, df: DataFrame) -> DataFrame:
""" # """
Ad hoc function I created to clarify some fields # Ad hoc function I created to clarify some fields
#
Args: # Args:
df (DataFrame): input dataframe # df (DataFrame): input dataframe
#
Returns: # Returns:
DataFrame: dataframe with 'clarified' column names # DataFrame: dataframe with 'clarified' column names
""" # """
df = df[df.columns.drop(list(df.filter(regex='_hashes')))] # df = df[df.columns.drop(list(df.filter(regex='_hashes')))]
return df.rename(columns={ # return df.rename(columns={
"contains_ratio": "contains_shared_hashes_ratio", # "contains_ratio": "contains_shared_hashes_ratio",
"matches_ratio": "matches_shared_hashes_ratio", # "matches_ratio": "matches_shared_hashes_ratio",
"kraken_count": "kraken2_read_count_(top_50)", # "kraken_count": "kraken2_read_count_(top_50)",
"kraken_percent": "kraken2_read_percent_(top_50)" # "kraken_percent": "kraken2_read_percent_(top_50)"
}) # })
#
@classmethod # @classmethod
def displace_date(cls, df: DataFrame) -> DataFrame: # def displace_date(cls, df: DataFrame) -> DataFrame:
""" # """
This function serves to split samples that were submitted on the same date by incrementing dates. # This function serves to split sample that were submitted on the same date by incrementing dates.
It will shift the date forward by one day if it is the same day as an existing date in a list. # It will shift the date forward by one day if it is the same day as an existing date in a list.
#
Args: # Args:
df (DataFrame): input dataframe composed of control records # df (DataFrame): input dataframe composed of control records
#
Returns: # Returns:
DataFrame: output dataframe with dates incremented. # DataFrame: output dataframe with dates incremented.
""" # """
# NOTE: get submitted dates for each control # # NOTE: get submitted dates for each control
dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in # dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in
sorted(df['name'].unique())] # sorted(df['name'].unique())]
previous_dates = set() # previous_dates = set()
for item in dict_list: # for item in dict_list:
df, previous_dates = cls.check_date(df=df, item=item, previous_dates=previous_dates) # df, previous_dates = cls.check_date(df=df, item=item, previous_dates=previous_dates)
return df # return df
#
@classmethod # @classmethod
def check_date(cls, df: DataFrame, item: dict, previous_dates: set) -> Tuple[DataFrame, list]: # def check_date(cls, df: DataFrame, item: dict, previous_dates: set) -> Tuple[DataFrame, list]:
""" # """
Checks if an items date is already present in df and adjusts df accordingly # Checks if an items date is already present in df and adjusts df accordingly
#
Args: # Args:
df (DataFrame): input dataframe # df (DataFrame): input dataframe
item (dict): control for checking # item (dict): control for checking
previous_dates (list): list of dates found in previous controls # previous_dates (list): list of dates found in previous control
#
Returns: # Returns:
Tuple[DataFrame, list]: Output dataframe and appended list of previous dates # Tuple[DataFrame, list]: Output dataframe and appended list of previous dates
""" # """
try: # try:
check = item['date'] in previous_dates # check = item['date'] in previous_dates
except IndexError: # except IndexError:
check = False # check = False
previous_dates.add(item['date']) # previous_dates.add(item['date'])
if check: # if check:
# NOTE: get df locations where name == item name # # NOTE: get df locations where name == item name
mask = df['name'] == item['name'] # mask = df['name'] == item['name']
# NOTE: increment date in dataframe # # NOTE: increment date in dataframe
df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1)) # df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1))
item['date'] += timedelta(days=1) # item['date'] += timedelta(days=1)
passed = False # passed = False
else: # else:
passed = True # passed = True
# NOTE: if run didn't lead to changed date, return values # # NOTE: if procedure didn't lead to changed date, return values
if passed: # if passed:
return df, previous_dates # return df, previous_dates
# NOTE: if date was changed, rerun with new date # # NOTE: if date was changed, rerun with new date
else: # else:
logger.warning(f"Date check failed, running recursion.") # logger.warning(f"Date check failed, running recursion.")
df, previous_dates = cls.check_date(df, item, previous_dates) # df, previous_dates = cls.check_date(df, item, previous_dates)
return df, previous_dates # return df, previous_dates
#
@classmethod # @classmethod
def prep_df(cls, ctx: Settings, df: DataFrame) -> Tuple[DataFrame | None, list]: # def prep_df(cls, ctx: Settings, df: DataFrame) -> Tuple[DataFrame | None, list]:
""" # """
Constructs figures based on parsed pandas dataframe. # Constructs figures based on parsed pandas dataframe.
#
Args: # Args:
ctx (Settings): settings passed down from gui # ctx (Settings): settings passed down from gui
df (pd.DataFrame): input dataframe # df (pd.DataFrame): input dataframe
ytitle (str | None, optional): title for the y-axis. Defaults to None. # ytitle (str | None, optional): title for the y-axis. Defaults to None.
#
Returns: # Returns:
Figure: Plotly figure # Figure: Plotly figure
""" # """
# NOTE: converts starred genera to normal and splits off list of starred # # NOTE: converts starred genera to normal and splits off list of starred
if df.empty: # if df.empty:
return None, [] # return None, []
df['genus'] = df['genus'].replace({'\*': ''}, regex=True).replace({"NaN": "Unknown"}) # df['genus'] = df['genus'].replace({'\*': ''}, regex=True).replace({"NaN": "Unknown"})
df['genera'] = [item[-1] if item and item[-1] == "*" else "" for item in df['genus'].to_list()] # df['genera'] = [item[-1] if item and item[-1] == "*" else "" for item in df['genus'].to_list()]
# NOTE: remove original runs, using reruns if applicable # # NOTE: remove original run, using reruns if applicable
df = cls.drop_reruns_from_df(ctx=ctx, df=df) # df = cls.drop_reruns_from_df(ctx=ctx, df=df)
# NOTE: sort by and exclude from # # NOTE: sort by and exclude from
sorts = ['submitted_date', "target", "genus"] # sorts = ['submitted_date', "target", "genus"]
exclude = ['name', 'genera'] # exclude = ['name', 'genera']
modes = [item for item in df.columns if item not in sorts and item not in exclude] # modes = [item for item in df.columns if item not in sorts and item not in exclude]
# NOTE: Set descending for any columns that have "{mode}" in the header. # # NOTE: Set descending for any columns that have "{mode}" in the header.
ascending = [False if item == "target" else True for item in sorts] # ascending = [False if item == "target" else True for item in sorts]
df = df.sort_values(by=sorts, ascending=ascending) # df = df.sort_values(by=sorts, ascending=ascending)
# NOTE: actual chart construction is done by # # NOTE: actual chart construction is done by
return df, modes # return df, modes
#
@classmethod # @classmethod
def drop_reruns_from_df(cls, ctx: Settings, df: DataFrame) -> DataFrame: # def drop_reruns_from_df(cls, ctx: Settings, df: DataFrame) -> DataFrame:
""" # """
Removes semi-duplicates from dataframe after finding sequencing repeats. # Removes semi-duplicates from dataframe after finding sequencing repeats.
#
Args: # Args:
ctx (Settings): settings passed from gui # ctx (Settings): settings passed from gui
df (DataFrame): initial dataframe # df (DataFrame): initial dataframe
#
Returns: # Returns:
DataFrame: dataframe with originals removed in favour of repeats. # DataFrame: dataframe with originals removed in favour of repeats.
""" # """
if 'rerun_regex' in ctx.model_extra: # if 'rerun_regex' in ctx.model_extra:
sample_names = get_unique_values_in_df_column(df, column_name="name") # sample_names = get_unique_values_in_df_column(df, column_name="name")
rerun_regex = re.compile(fr"{ctx.rerun_regex}") # rerun_regex = re.compile(fr"{ctx.rerun_regex}")
exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)] # exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
df = df[~df.name.isin(exclude)] # df = df[~df.name.isin(exclude)]
return df # return df
#
def to_pydantic(self) -> "PydIridaControl": # def to_pydantic(self) -> "PydIridaControl":
""" # """
Constructs a pydantic version of this object. # Constructs a pydantic version of this object.
#
Returns: # Returns:
PydIridaControl: This object as a pydantic model. # PydIridaControl: This object as a pydantic model.
""" # """
from backend.validators import PydIridaControl # from backend.validators import PydIridaControl
return PydIridaControl(**self.__dict__) # return PydIridaControl(**self.__dict__)
#
@property # @property
def is_positive_control(self): # def is_positive_control(self):
return not self.subtype.lower().startswith("en") # return not self.subtype.lower().startswith("en")

File diff suppressed because it is too large Load Diff

View File

@@ -14,32 +14,27 @@ from typing import List, Tuple
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# table containing organization/contact relationship # table containing clientlab/contact relationship
orgs_contacts = Table( clientlab_contact = Table(
"_orgs_contacts", "_clientlab_contact",
Base.metadata, Base.metadata,
Column("org_id", INTEGER, ForeignKey("_organization.id")), Column("clientlab_id", INTEGER, ForeignKey("_clientlab.id")),
Column("contact_id", INTEGER, ForeignKey("_contact.id")), Column("contact_id", INTEGER, ForeignKey("_contact.id")),
extend_existing=True extend_existing=True
) )
class Organization(BaseClass): class ClientLab(BaseClass):
""" """
Base of organization Base of clientlab
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name name = Column(String(64)) #: clientlab name
submissions = relationship("ClientSubmission", clientsubmission = relationship("ClientSubmission", back_populates="clientlab") #: procedure this clientlab has submitted
back_populates="submitting_lab") #: submissions this organization has submitted
cost_centre = Column(String()) #: cost centre used by org for payment cost_centre = Column(String()) #: cost centre used by org for payment
contacts = relationship("Contact", back_populates="organization", contact = relationship("Contact", back_populates="clientlab",
secondary=orgs_contacts) #: contacts involved with this org secondary=clientlab_contact) #: contact involved with this org
@hybrid_property
def contact(self):
return self.contacts
@classmethod @classmethod
@setup_lookup @setup_lookup
@@ -47,16 +42,16 @@ class Organization(BaseClass):
id: int | None = None, id: int | None = None,
name: str | None = None, name: str | None = None,
limit: int = 0, limit: int = 0,
) -> Organization | List[Organization]: ) -> ClientLab | List[ClientLab]:
""" """
Lookup organizations in the database by a number of parameters. Lookup clientlabs in the database by a number of parameters.
Args: Args:
name (str | None, optional): Name of the organization. Defaults to None. name (str | None, optional): Name of the clientlab. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns: Returns:
Organization|List[Organization]: ClientLab|List[ClientLab]:
""" """
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match id: match id:
@@ -89,7 +84,7 @@ class Organization(BaseClass):
name = "NA" name = "NA"
return OmniOrganization(instance_object=self, return OmniOrganization(instance_object=self,
name=name, cost_centre=cost_centre, name=name, cost_centre=cost_centre,
contact=[item.to_omni() for item in self.contacts]) contact=[item.to_omni() for item in self.contact])
class Contact(BaseClass): class Contact(BaseClass):
@@ -101,27 +96,27 @@ class Contact(BaseClass):
name = Column(String(64)) #: contact name name = Column(String(64)) #: contact name
email = Column(String(64)) #: contact email email = Column(String(64)) #: contact email
phone = Column(String(32)) #: contact phone number phone = Column(String(32)) #: contact phone number
organization = relationship("Organization", back_populates="contacts", uselist=True, clientlab = relationship("ClientLab", back_populates="contact", uselist=True,
secondary=orgs_contacts) #: relationship to joined organization secondary=clientlab_contact) #: relationship to joined clientlab
submissions = relationship("ClientSubmission", back_populates="contact") #: submissions this contact has submitted clientsubmission = relationship("ClientSubmission", back_populates="contact") #: procedure this contact has submitted
@classproperty @classproperty
def searchables(cls): def searchables(cls):
return [] return []
@classmethod # @classmethod
def query_or_create(cls, **kwargs) -> Tuple[Contact, bool]: # def query_or_create(cls, **kwargs) -> Tuple[Contact, bool]:
new = False # new = False
disallowed = [] # disallowed = []
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} # sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs) # instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list): # if not instance or isinstance(instance, list):
instance = cls() # instance = cls()
new = True # new = True
for k, v in sanitized_kwargs.items(): # for k, v in sanitized_kwargs.items():
setattr(instance, k, v) # setattr(instance, k, v)
logger.info(f"Instance from contact query or create: {instance}") # logger.info(f"Instance from contact query or create: {instance}")
return instance, new # return instance, new
@classmethod @classmethod
@setup_lookup @setup_lookup
@@ -133,7 +128,7 @@ class Contact(BaseClass):
limit: int = 0, limit: int = 0,
) -> Contact | List[Contact]: ) -> Contact | List[Contact]:
""" """
Lookup contacts in the database by a number of parameters. Lookup contact in the database by a number of parameters.
Args: Args:
name (str | None, optional): Name of the contact. Defaults to None. name (str | None, optional): Name of the contact. Defaults to None.

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@ Contains pandas and openpyxl convenience functions for interacting with excel wo
''' '''
from .parser import * from .parser import *
from .submission_parser import * from backend.excel.parsers.submission_parser import *
from .reports import * from .reports import *
from .writer import * from .writer import *

View File

@@ -1,5 +1,5 @@
""" """
contains parser objects for pulling values from client generated run sheets. contains clientsubmissionparser objects for pulling values from client generated procedure sheets.
""" """
import logging import logging
from copy import copy from copy import copy
@@ -42,11 +42,11 @@ class SheetParser(object):
raise FileNotFoundError(f"Couldn't parse file {self.filepath}") raise FileNotFoundError(f"Couldn't parse file {self.filepath}")
self.sub = OrderedDict() self.sub = OrderedDict()
# NOTE: make decision about type of sample we have # NOTE: make decision about type of sample we have
self.sub['submission_type'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath), self.sub['proceduretype'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath),
missing=True) missing=True)
self.submission_type = SubmissionType.query(name=self.sub['submission_type']) self.submission_type = SubmissionType.query(name=self.sub['proceduretype'])
self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type) self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
# NOTE: grab the info map from the run type in database # NOTE: grab the info map from the procedure type in database
self.parse_info() self.parse_info()
self.import_kit_validation_check() self.import_kit_validation_check()
self.parse_reagents() self.parse_reagents()
@@ -60,19 +60,19 @@ class SheetParser(object):
""" """
parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object) parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object)
self.info_map = parser.info_map self.info_map = parser.info_map
# NOTE: in order to accommodate generic run types we have to check for the type in the excel sheet and rerun accordingly # NOTE: in order to accommodate generic procedure types we have to check for the type in the excel sheet and rerun accordingly
try: try:
check = parser.parsed_info['submission_type']['value'] not in [None, "None", "", " "] check = parser.parsed_info['proceduretype']['value'] not in [None, "None", "", " "]
except KeyError as e: except KeyError as e:
logger.error(f"Couldn't check run type due to KeyError: {e}") logger.error(f"Couldn't check procedure type due to KeyError: {e}")
return return
logger.info( logger.info(
f"Checking for updated run type: {self.submission_type.name} against new: {parser.parsed_info['submission_type']['value']}") f"Checking for updated procedure type: {self.submission_type.name} against new: {parser.parsed_info['proceduretype']['value']}")
if self.submission_type.name != parser.parsed_info['submission_type']['value']: if self.submission_type.name != parser.parsed_info['proceduretype']['value']:
if check: if check:
# NOTE: If initial run type doesn't match parsed run type, defer to parsed run type. # NOTE: If initial procedure type doesn't match parsed procedure type, defer to parsed procedure type.
self.submission_type = SubmissionType.query(name=parser.parsed_info['submission_type']['value']) self.submission_type = SubmissionType.query(name=parser.parsed_info['proceduretype']['value'])
logger.info(f"Updated self.submission_type to {self.submission_type}. Rerunning parse.") logger.info(f"Updated self.proceduretype to {self.submission_type}. Rerunning parse.")
self.parse_info() self.parse_info()
else: else:
self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath) self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath)
@@ -82,53 +82,53 @@ class SheetParser(object):
def parse_reagents(self, extraction_kit: str | None = None): def parse_reagents(self, extraction_kit: str | None = None):
""" """
Calls reagent parser class to pull info from the excel sheet Calls reagent clientsubmissionparser class to pull info from the excel sheet
Args: Args:
extraction_kit (str | None, optional): Relevant extraction kit for reagent map. Defaults to None. extraction_kit (str | None, optional): Relevant extraction kittype for reagent map. Defaults to None.
""" """
if extraction_kit is None: if extraction_kit is None:
extraction_kit = self.sub['extraction_kit'] extraction_kit = self.sub['kittype']
parser = ReagentParser(xl=self.xl, submission_type=self.submission_type, parser = ReagentParser(xl=self.xl, submission_type=self.submission_type,
extraction_kit=extraction_kit) extraction_kit=extraction_kit)
self.sub['reagents'] = parser.parsed_reagents self.sub['reagents'] = parser.parsed_reagents
def parse_samples(self): def parse_samples(self):
""" """
Calls sample parser to pull info from the excel sheet Calls sample clientsubmissionparser to pull info from the excel sheet
""" """
parser = SampleParser(xl=self.xl, submission_type=self.submission_type) parser = SampleParser(xl=self.xl, submission_type=self.submission_type)
self.sub['samples'] = parser.parsed_samples self.sub['sample'] = parser.parsed_samples
def parse_equipment(self): def parse_equipment(self):
""" """
Calls equipment parser to pull info from the excel sheet Calls equipment clientsubmissionparser to pull info from the excel sheet
""" """
parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type) parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type)
self.sub['equipment'] = parser.parsed_equipment self.sub['equipment'] = parser.parsed_equipment
def parse_tips(self): def parse_tips(self):
""" """
Calls tips parser to pull info from the excel sheet Calls tips clientsubmissionparser to pull info from the excel sheet
""" """
parser = TipParser(xl=self.xl, submission_type=self.submission_type) parser = TipParser(xl=self.xl, submission_type=self.submission_type)
self.sub['tips'] = parser.parsed_tips self.sub['tips'] = parser.parsed_tips
def import_kit_validation_check(self): def import_kit_validation_check(self):
""" """
Enforce that the parser has an extraction kit Enforce that the clientsubmissionparser has an extraction kittype
""" """
if 'extraction_kit' not in self.sub.keys() or not check_not_nan(self.sub['extraction_kit']['value']): if 'kittype' not in self.sub.keys() or not check_not_nan(self.sub['kittype']['value']):
from frontend.widgets.pop_ups import ObjectSelector from frontend.widgets.pop_ups import ObjectSelector
dlg = ObjectSelector(title="Kit Needed", message="At minimum a kit is needed. Please select one.", dlg = ObjectSelector(title="Kit Needed", message="At minimum a kittype is needed. Please select one.",
obj_type=KitType) obj_type=KitType)
if dlg.exec(): if dlg.exec():
self.sub['extraction_kit'] = dict(value=dlg.parse_form(), missing=True) self.sub['kittype'] = dict(value=dlg.parse_form(), missing=True)
else: else:
raise ValueError("Extraction kit needed.") raise ValueError("Extraction kittype needed.")
else: else:
if isinstance(self.sub['extraction_kit'], str): if isinstance(self.sub['kittype'], str):
self.sub['extraction_kit'] = dict(value=self.sub['extraction_kit'], missing=True) self.sub['kittype'] = dict(value=self.sub['kittype'], missing=True)
def to_pydantic(self) -> PydSubmission: def to_pydantic(self) -> PydSubmission:
""" """
@@ -145,17 +145,17 @@ class InfoParser(object):
Object to parse generic info from excel sheet. Object to parse generic info from excel sheet.
""" """
def __init__(self, xl: Workbook, submission_type: str | SubmissionType, sub_object: BasicRun | None = None): def __init__(self, xl: Workbook, submission_type: str | SubmissionType, sub_object: Run | None = None):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None. sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
if sub_object is None: if sub_object is None:
sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=submission_type.name) sub_object = Run.find_polymorphic_subclass(polymorphic_identity=submission_type.name)
self.submission_type_obj = submission_type self.submission_type_obj = submission_type
self.submission_type = dict(value=self.submission_type_obj.name, missing=True) self.submission_type = dict(value=self.submission_type_obj.name, missing=True)
self.sub_object = sub_object self.sub_object = sub_object
@@ -164,12 +164,12 @@ class InfoParser(object):
@property @property
def info_map(self) -> dict: def info_map(self) -> dict:
""" """
Gets location of basic info from the submission_type object in the database. Gets location of basic info from the proceduretype object in the database.
Returns: Returns:
dict: Location map of all info for this run type dict: Location map of all info for this procedure type
""" """
# NOTE: Get the parse_info method from the run type specified # NOTE: Get the parse_info method from the procedure type specified
return self.sub_object.construct_info_map(submission_type=self.submission_type_obj, mode="read") return self.sub_object.construct_info_map(submission_type=self.submission_type_obj, mode="read")
@property @property
@@ -186,7 +186,7 @@ class InfoParser(object):
ws = self.xl[sheet] ws = self.xl[sheet]
relevant = [] relevant = []
for k, v in self.info_map.items(): for k, v in self.info_map.items():
# NOTE: If the value is hardcoded put it in the dictionary directly. Ex. Artic kit # NOTE: If the value is hardcoded put it in the dictionary directly. Ex. Artic kittype
if k == "custom": if k == "custom":
continue continue
if isinstance(v, str): if isinstance(v, str):
@@ -210,7 +210,7 @@ class InfoParser(object):
# NOTE: Get cell contents at this location # NOTE: Get cell contents at this location
value = ws.cell(row=item['row'], column=item['column']).value value = ws.cell(row=item['row'], column=item['column']).value
match item['name']: match item['name']:
case "submission_type": case "proceduretype":
value, missing = is_missing(value) value, missing = is_missing(value)
value = value.title() value = value.title()
case "submitted_date": case "submitted_date":
@@ -232,7 +232,7 @@ class InfoParser(object):
dicto[item['name']] = dict(value=value, missing=missing) dicto[item['name']] = dict(value=value, missing=missing)
except (KeyError, IndexError): except (KeyError, IndexError):
continue continue
# NOTE: Return after running the parser components held in run object. # NOTE: Return after running the clientsubmissionparser components held in procedure object.
return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl, custom_fields=self.info_map['custom']) return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl, custom_fields=self.info_map['custom'])
@@ -242,12 +242,12 @@ class ReagentParser(object):
""" """
def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str, def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str,
run_object: BasicRun | None = None): run_object: Run | None = None):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str|SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (str|SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
extraction_kit (str): Extraction kit used. extraction_kit (str): Extraction kittype used.
run_object (BasicRun | None, optional): Submission object holding methods. Defaults to None. run_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
@@ -264,15 +264,16 @@ class ReagentParser(object):
@property @property
def kit_map(self) -> dict: def kit_map(self) -> dict:
""" """
Gets location of kit reagents from database Gets location of kittype reagents from database
Args: Args:
submission_type (str): Name of run type. proceduretype (str): Name of procedure type.
Returns: Returns:
dict: locations of reagent info for the kit. dict: locations of reagent info for the kittype.
""" """
associations, self.kit_object = self.kit_object.construct_xl_map_for_use(submission_type=self.submission_type_obj) associations, self.kit_object = self.kit_object.construct_xl_map_for_use(
proceduretype=self.submission_type_obj)
reagent_map = {k: v for k, v in associations.items() if k != 'info'} reagent_map = {k: v for k, v in associations.items() if k != 'info'}
try: try:
del reagent_map['info'] del reagent_map['info']
@@ -323,16 +324,16 @@ class ReagentParser(object):
class SampleParser(object): class SampleParser(object):
""" """
Object to pull data for samples in excel sheet and construct individual sample objects Object to pull data for sample in excel sheet and construct individual sample objects
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None,
sub_object: BasicRun | None = None) -> None: sub_object: Run | None = None) -> None:
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None. sample_map (dict | None, optional): Locations in database where sample are found. Defaults to None.
sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None. sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
""" """
self.samples = [] self.samples = []
@@ -343,19 +344,19 @@ class SampleParser(object):
self.submission_type_obj = submission_type self.submission_type_obj = submission_type
if sub_object is None: if sub_object is None:
logger.warning( logger.warning(
f"Sample parser attempting to fetch run class with polymorphic identity: {self.submission_type}") f"Sample clientsubmissionparser attempting to fetch procedure class with polymorphic identity: {self.submission_type}")
sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type) sub_object = Run.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.sub_object = sub_object self.sub_object = sub_object
self.sample_type = self.sub_object.get_default_info("sample_type", submission_type=submission_type) self.sample_type = self.sub_object.get_default_info("sampletype", submission_type=submission_type)
self.samp_object = BasicSample.find_polymorphic_subclass(polymorphic_identity=self.sample_type) self.samp_object = Sample.find_polymorphic_subclass(polymorphic_identity=self.sample_type)
@property @property
def sample_map(self) -> dict: def sample_map(self) -> dict:
""" """
Gets info locations in excel book for run type. Gets info locations in excel book for procedure type.
Args: Args:
submission_type (str): run type proceduretype (str): procedure type
Returns: Returns:
dict: Info locations. dict: Info locations.
@@ -381,7 +382,7 @@ class SampleParser(object):
if check_not_nan(id): if check_not_nan(id):
if id not in invalids: if id not in invalids:
sample_dict = dict(id=id, row=ii, column=jj) sample_dict = dict(id=id, row=ii, column=jj)
sample_dict['sample_type'] = self.sample_type sample_dict['sampletype'] = self.sample_type
plate_map_samples.append(sample_dict) plate_map_samples.append(sample_dict)
else: else:
pass pass
@@ -407,7 +408,7 @@ class SampleParser(object):
row_dict[lmap['merge_on_id']] = str(row_dict[lmap['merge_on_id']]) row_dict[lmap['merge_on_id']] = str(row_dict[lmap['merge_on_id']])
except KeyError: except KeyError:
pass pass
row_dict['sample_type'] = self.sample_type row_dict['sampletype'] = self.sample_type
row_dict['submission_rank'] = ii row_dict['submission_rank'] = ii
try: try:
check = check_not_nan(row_dict[lmap['merge_on_id']]) check = check_not_nan(row_dict[lmap['merge_on_id']])
@@ -423,14 +424,14 @@ class SampleParser(object):
Merges sample info from lookup table and plate map. Merges sample info from lookup table and plate map.
Returns: Returns:
List[dict]: Reconciled samples List[dict]: Reconciled sample
""" """
if not self.plate_map_samples or not self.lookup_samples: if not self.plate_map_samples or not self.lookup_samples:
logger.warning(f"No separate samples") logger.warning(f"No separate sample")
samples = self.lookup_samples or self.plate_map_samples samples = self.lookup_samples or self.plate_map_samples
for new in samples: for new in samples:
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True): if not check_key_or_attr(key='sample_id', interest=new, check_none=True):
new['submitter_id'] = new['id'] new['sample_id'] = new['id']
new = self.sub_object.parse_samples(new) new = self.sub_object.parse_samples(new)
try: try:
del new['id'] del new['id']
@@ -459,8 +460,8 @@ class SampleParser(object):
if lsample[merge_on_id] == psample['id']), (-1, psample)) if lsample[merge_on_id] == psample['id']), (-1, psample))
if jj >= 0: if jj >= 0:
lookup_samples[jj] = {} lookup_samples[jj] = {}
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True): if not check_key_or_attr(key='sample_id', interest=new, check_none=True):
new['submitter_id'] = psample['id'] new['sample_id'] = psample['id']
new = self.sub_object.parse_samples(new) new = self.sub_object.parse_samples(new)
try: try:
del new['id'] del new['id']
@@ -478,7 +479,7 @@ class EquipmentParser(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -488,7 +489,7 @@ class EquipmentParser(object):
@property @property
def equipment_map(self) -> dict: def equipment_map(self) -> dict:
""" """
Gets the map of equipment locations in the run type's spreadsheet Gets the map of equipment locations in the procedure type's spreadsheet
Returns: Returns:
List[dict]: List of locations List[dict]: List of locations
@@ -556,7 +557,7 @@ class TipParser(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -566,7 +567,7 @@ class TipParser(object):
@property @property
def tip_map(self) -> dict: def tip_map(self) -> dict:
""" """
Gets the map of equipment locations in the run type's spreadsheet Gets the map of equipment locations in the procedure type's spreadsheet
Returns: Returns:
List[dict]: List of locations List[dict]: List of locations
@@ -609,7 +610,7 @@ class TipParser(object):
class PCRParser(object): class PCRParser(object):
"""Object to pull data from Design and Analysis PCR export file.""" """Object to pull data from Design and Analysis PCR export file."""
def __init__(self, filepath: Path | None = None, submission: BasicRun | None = None) -> None: def __init__(self, filepath: Path | None = None, submission: Run | None = None) -> None:
""" """
Args: Args:
filepath (Path | None, optional): file to parse. Defaults to None. filepath (Path | None, optional): file to parse. Defaults to None.
@@ -659,7 +660,7 @@ class PCRParser(object):
class ConcentrationParser(object): class ConcentrationParser(object):
def __init__(self, filepath: Path | None = None, run: BasicRun | None = None) -> None: def __init__(self, filepath: Path | None = None, run: Run | None = None) -> None:
if filepath is None: if filepath is None:
logger.error('No filepath given.') logger.error('No filepath given.')
self.xl = None self.xl = None
@@ -673,7 +674,7 @@ class ConcentrationParser(object):
logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.") logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.")
return None return None
if run is None: if run is None:
self.submission_obj = BasicRun() self.submission_obj = Run()
rsl_plate_num = None rsl_plate_num = None
else: else:
self.submission_obj = run self.submission_obj = run

View File

@@ -7,7 +7,7 @@ from pandas import DataFrame, ExcelWriter
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
from typing import Tuple, List from typing import Tuple, List
from backend.db.models import BasicRun from backend.db.models import Run
from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
@@ -45,9 +45,9 @@ class ReportMaker(object):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
self.runs = BasicRun.query(start_date=start_date, end_date=end_date, page_size=0) self.runs = Run.query(start_date=start_date, end_date=end_date, page_size=0)
if organizations is not None: if organizations is not None:
self.runs = [run for run in self.runs if run.client_submission.submitting_lab.name in organizations] self.runs = [run for run in self.runs if run.clientsubmission.clientlab.name in organizations]
self.detailed_df, self.summary_df = self.make_report_xlsx() self.detailed_df, self.summary_df = self.make_report_xlsx()
self.html = self.make_report_html(df=self.summary_df) self.html = self.make_report_html(df=self.summary_df)
@@ -61,14 +61,14 @@ class ReportMaker(object):
if not self.runs: if not self.runs:
return DataFrame(), DataFrame() return DataFrame(), DataFrame()
df = DataFrame.from_records([item.to_dict(report=True) for item in self.runs]) df = DataFrame.from_records([item.to_dict(report=True) for item in self.runs])
# NOTE: put submissions with the same lab together # NOTE: put procedure with the same lab together
df = df.sort_values("submitting_lab") df = df.sort_values("clientlab")
# NOTE: aggregate cost and sample count columns # NOTE: aggregate cost and sample count columns
df2 = df.groupby(["submitting_lab", "extraction_kit"]).agg( df2 = df.groupby(["clientlab", "kittype"]).agg(
{'extraction_kit': 'count', 'cost': 'sum', 'sample_count': 'sum'}) {'kittype': 'count', 'cost': 'sum', 'sample_count': 'sum'})
df2 = df2.rename(columns={"extraction_kit": 'run_count'}) df2 = df2.rename(columns={"kittype": 'run_count'})
df = df.drop('id', axis=1) df = df.drop('id', axis=1)
df = df.sort_values(['submitting_lab', "started_date"]) df = df.sort_values(['clientlab', "started_date"])
return df, df2 return df, df2
def make_report_html(self, df: DataFrame) -> str: def make_report_html(self, df: DataFrame) -> str:
@@ -156,19 +156,19 @@ class TurnaroundMaker(ReportArchetype):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
self.subs = BasicRun.query(start_date=start_date, end_date=end_date, self.subs = Run.query(start_date=start_date, end_date=end_date,
submission_type_name=submission_type, page_size=0) submissiontype_name=submission_type, page_size=0)
records = [self.build_record(sub) for sub in self.subs] records = [self.build_record(sub) for sub in self.subs]
self.df = DataFrame.from_records(records) self.df = DataFrame.from_records(records)
self.sheet_name = "Turnaround" self.sheet_name = "Turnaround"
@classmethod @classmethod
def build_record(cls, sub: BasicRun) -> dict: def build_record(cls, sub: Run) -> dict:
""" """
Build a turnaround dictionary from a run Build a turnaround dictionary from a procedure
Args: Args:
sub (BasicRun): The run to be processed. sub (BasicRun): The procedure to be processed.
Returns: Returns:
@@ -203,9 +203,9 @@ class ConcentrationMaker(ReportArchetype):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
self.subs = BasicRun.query(start_date=start_date, end_date=end_date, self.subs = Run.query(start_date=start_date, end_date=end_date,
submission_type_name=submission_type, page_size=0) submissiontype_name=submission_type, page_size=0)
# self.samples = flatten_list([sub.get_provisional_controls(controls_only=controls_only) for sub in self.runs]) # self.sample = flatten_list([sub.get_provisional_controls(controls_only=controls_only) for sub in self.run])
self.samples = flatten_list([sub.get_provisional_controls(include=include) for sub in self.subs]) self.samples = flatten_list([sub.get_provisional_controls(include=include) for sub in self.subs])
self.records = [self.build_record(sample) for sample in self.samples] self.records = [self.build_record(sample) for sample in self.samples]
self.df = DataFrame.from_records(self.records) self.df = DataFrame.from_records(self.records)
@@ -214,9 +214,9 @@ class ConcentrationMaker(ReportArchetype):
@classmethod @classmethod
def build_record(cls, control) -> dict: def build_record(cls, control) -> dict:
regex = re.compile(r"^(ATCC)|(MCS)", flags=re.IGNORECASE) regex = re.compile(r"^(ATCC)|(MCS)", flags=re.IGNORECASE)
if bool(regex.match(control.submitter_id)): if bool(regex.match(control.sample_id)):
positive = "positive" positive = "positive"
elif control.submitter_id.lower().startswith("en"): elif control.sample_id.lower().startswith("en"):
positive = "negative" positive = "negative"
else: else:
positive = "sample" positive = "sample"
@@ -224,8 +224,8 @@ class ConcentrationMaker(ReportArchetype):
concentration = float(control.concentration) concentration = float(control.concentration)
except (TypeError, ValueError): except (TypeError, ValueError):
concentration = 0.0 concentration = 0.0
return dict(name=control.submitter_id, return dict(name=control.sample_id,
submission=str(control.submission), concentration=concentration, submission=str(control.clientsubmission), concentration=concentration,
submitted_date=control.submitted_date, positive=positive) submitted_date=control.submitted_date, positive=positive)

View File

@@ -1,5 +1,5 @@
""" """
contains writer objects for pushing values to run sheet templates. contains writer objects for pushing values to procedure sheet templates.
""" """
import logging import logging
from copy import copy from copy import copy
@@ -8,7 +8,7 @@ from operator import itemgetter
from pprint import pformat from pprint import pformat
from typing import List, Generator, Tuple from typing import List, Generator, Tuple
from openpyxl import load_workbook, Workbook from openpyxl import load_workbook, Workbook
from backend.db.models import SubmissionType, KitType, BasicRun from backend.db.models import SubmissionType, KitType, Run
from backend.validators.pydant import PydSubmission from backend.validators.pydant import PydSubmission
from io import BytesIO from io import BytesIO
from collections import OrderedDict from collections import OrderedDict
@@ -24,7 +24,7 @@ class SheetWriter(object):
def __init__(self, submission: PydSubmission): def __init__(self, submission: PydSubmission):
""" """
Args: Args:
submission (PydSubmission): Object containing run information. submission (PydSubmission): Object containing procedure information.
""" """
self.sub = OrderedDict(submission.improved_dict()) self.sub = OrderedDict(submission.improved_dict())
# NOTE: Set values from pydantic object. # NOTE: Set values from pydantic object.
@@ -32,7 +32,7 @@ class SheetWriter(object):
match k: match k:
case 'filepath': case 'filepath':
self.__setattr__(k, v) self.__setattr__(k, v)
case 'submission_type': case 'proceduretype':
self.sub[k] = v['value'] self.sub[k] = v['value']
self.submission_type = SubmissionType.query(name=v['value']) self.submission_type = SubmissionType.query(name=v['value'])
self.run_object = BasicRun.find_polymorphic_subclass( self.run_object = BasicRun.find_polymorphic_subclass(
@@ -58,7 +58,7 @@ class SheetWriter(object):
""" """
Calls info writer Calls info writer
""" """
disallowed = ['filepath', 'reagents', 'samples', 'equipment', 'controls'] disallowed = ['filepath', 'reagents', 'sample', 'equipment', 'control']
info_dict = {k: v for k, v in self.sub.items() if k not in disallowed} info_dict = {k: v for k, v in self.sub.items() if k not in disallowed}
writer = InfoWriter(xl=self.xl, submission_type=self.submission_type, info_dict=info_dict) writer = InfoWriter(xl=self.xl, submission_type=self.submission_type, info_dict=info_dict)
self.xl = writer.write_info() self.xl = writer.write_info()
@@ -69,14 +69,14 @@ class SheetWriter(object):
""" """
reagent_list = self.sub['reagents'] reagent_list = self.sub['reagents']
writer = ReagentWriter(xl=self.xl, submission_type=self.submission_type, writer = ReagentWriter(xl=self.xl, submission_type=self.submission_type,
extraction_kit=self.sub['extraction_kit'], reagent_list=reagent_list) extraction_kit=self.sub['kittype'], reagent_list=reagent_list)
self.xl = writer.write_reagents() self.xl = writer.write_reagents()
def write_samples(self): def write_samples(self):
""" """
Calls sample writer Calls sample writer
""" """
sample_list = self.sub['samples'] sample_list = self.sub['sample']
writer = SampleWriter(xl=self.xl, submission_type=self.submission_type, sample_list=sample_list) writer = SampleWriter(xl=self.xl, submission_type=self.submission_type, sample_list=sample_list)
self.xl = writer.write_samples() self.xl = writer.write_samples()
@@ -99,22 +99,22 @@ class SheetWriter(object):
class InfoWriter(object): class InfoWriter(object):
""" """
object to write general run info into excel file object to write general procedure info into excel file
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict,
sub_object: BasicRun | None = None): sub_object: Run | None = None):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
info_dict (dict): Dictionary of information to write. info_dict (dict): Dictionary of information to write.
sub_object (BasicRun | None, optional): Submission object containing methods. Defaults to None. sub_object (BasicRun | None, optional): Submission object containing methods. Defaults to None.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
if sub_object is None: if sub_object is None:
sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=submission_type.name) sub_object = Run.find_polymorphic_subclass(polymorphic_identity=submission_type.name)
self.submission_type = submission_type self.submission_type = submission_type
self.sub_object = sub_object self.sub_object = sub_object
self.xl = xl self.xl = xl
@@ -196,8 +196,8 @@ class ReagentWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
extraction_kit (KitType | str): Extraction kit used. extraction_kit (KitType | str): Extraction kittype used.
reagent_list (list): List of reagent dicts to be written to excel. reagent_list (list): List of reagent dicts to be written to excel.
""" """
self.xl = xl self.xl = xl
@@ -208,7 +208,7 @@ class ReagentWriter(object):
extraction_kit = KitType.query(name=extraction_kit) extraction_kit = KitType.query(name=extraction_kit)
self.kit_object = extraction_kit self.kit_object = extraction_kit
associations, self.kit_object = self.kit_object.construct_xl_map_for_use( associations, self.kit_object = self.kit_object.construct_xl_map_for_use(
submission_type=self.submission_type_obj) proceduretype=self.submission_type_obj)
reagent_map = {k: v for k, v in associations.items()} reagent_map = {k: v for k, v in associations.items()}
self.reagents = self.reconcile_map(reagent_list=reagent_list, reagent_map=reagent_map) self.reagents = self.reconcile_map(reagent_list=reagent_list, reagent_map=reagent_map)
@@ -223,13 +223,13 @@ class ReagentWriter(object):
Returns: Returns:
List[dict]: merged dictionary List[dict]: merged dictionary
""" """
filled_roles = [item['role'] for item in reagent_list] filled_roles = [item['reagentrole'] for item in reagent_list]
for map_obj in reagent_map.keys(): for map_obj in reagent_map.keys():
if map_obj not in filled_roles: if map_obj not in filled_roles:
reagent_list.append(dict(name="Not Applicable", role=map_obj, lot="Not Applicable", expiry="Not Applicable")) reagent_list.append(dict(name="Not Applicable", role=map_obj, lot="Not Applicable", expiry="Not Applicable"))
for reagent in reagent_list: for reagent in reagent_list:
try: try:
mp_info = reagent_map[reagent['role']] mp_info = reagent_map[reagent['reagentrole']]
except KeyError: except KeyError:
continue continue
placeholder = copy(reagent) placeholder = copy(reagent)
@@ -273,7 +273,7 @@ class SampleWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
sample_list (list): List of sample dictionaries to be written to excel file. sample_list (list): List of sample dictionaries to be written to excel file.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
@@ -281,7 +281,7 @@ class SampleWriter(object):
self.submission_type = submission_type self.submission_type = submission_type
self.xl = xl self.xl = xl
self.sample_map = submission_type.sample_map['lookup_table'] self.sample_map = submission_type.sample_map['lookup_table']
# NOTE: exclude any samples without a run rank. # NOTE: exclude any sample without a procedure rank.
samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0] samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0]
self.samples = sorted(samples, key=itemgetter('submission_rank')) self.samples = sorted(samples, key=itemgetter('submission_rank'))
self.blank_lookup_table() self.blank_lookup_table()
@@ -322,7 +322,7 @@ class SampleWriter(object):
Performs writing operations. Performs writing operations.
Returns: Returns:
Workbook: Workbook with samples written Workbook: Workbook with sample written
""" """
sheet = self.xl[self.sample_map['sheet']] sheet = self.xl[self.sample_map['sheet']]
columns = self.sample_map['sample_columns'] columns = self.sample_map['sample_columns']
@@ -351,7 +351,7 @@ class EquipmentWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
equipment_list (list): List of equipment dictionaries to write to excel file. equipment_list (list): List of equipment dictionaries to write to excel file.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
@@ -376,9 +376,9 @@ class EquipmentWriter(object):
return return
for ii, equipment in enumerate(equipment_list, start=1): for ii, equipment in enumerate(equipment_list, start=1):
try: try:
mp_info = equipment_map[equipment['role']] mp_info = equipment_map[equipment['reagentrole']]
except KeyError: except KeyError:
logger.error(f"No {equipment['role']} in {pformat(equipment_map)}") logger.error(f"No {equipment['reagentrole']} in {pformat(equipment_map)}")
mp_info = None mp_info = None
placeholder = copy(equipment) placeholder = copy(equipment)
if not mp_info: if not mp_info:
@@ -433,7 +433,7 @@ class TipWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
tips_list (list): List of tip dictionaries to write to the excel file. tips_list (list): List of tip dictionaries to write to the excel file.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):

View File

@@ -5,7 +5,7 @@ import logging, re
import sys import sys
from pathlib import Path from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
from backend.db.models import BasicRun, SubmissionType from backend.db.models import Run, SubmissionType
from tools import jinja_template_loading from tools import jinja_template_loading
from jinja2 import Template from jinja2 import Template
from dateutil.parser import parse from dateutil.parser import parse
@@ -25,22 +25,22 @@ class RSLNamer(object):
self.submission_type = submission_type self.submission_type = submission_type
if not self.submission_type: if not self.submission_type:
self.submission_type = self.retrieve_submission_type(filename=filename) self.submission_type = self.retrieve_submission_type(filename=filename)
logger.info(f"got run type: {self.submission_type}") logger.info(f"got procedure type: {self.submission_type}")
if self.submission_type: if self.submission_type:
self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type) self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex( self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex(
submission_type=submission_type)) submission_type=submission_type))
if not data: if not data:
data = dict(submission_type=self.submission_type) data = dict(submission_type=self.submission_type)
if "submission_type" not in data.keys(): if "proceduretype" not in data.keys():
data['submission_type'] = self.submission_type data['proceduretype'] = self.submission_type
self.parsed_name = self.sub_object.enforce_name(instr=self.parsed_name, data=data) self.parsed_name = self.sub_object.enforce_name(instr=self.parsed_name, data=data)
logger.info(f"Parsed name: {self.parsed_name}") logger.info(f"Parsed name: {self.parsed_name}")
@classmethod @classmethod
def retrieve_submission_type(cls, filename: str | Path) -> str: def retrieve_submission_type(cls, filename: str | Path) -> str:
""" """
Gets run type from excel file properties or sheet names or regex pattern match or user input Gets procedure type from excel file properties or sheet names or regex pattern match or user input
Args: Args:
filename (str | Path): filename filename (str | Path): filename
@@ -49,7 +49,7 @@ class RSLNamer(object):
TypeError: Raised if unsupported variable type for filename given. TypeError: Raised if unsupported variable type for filename given.
Returns: Returns:
str: parsed run type str: parsed procedure type
""" """
def st_from_path(filepath: Path) -> str: def st_from_path(filepath: Path) -> str:
@@ -89,7 +89,7 @@ class RSLNamer(object):
sub_type = m.lastgroup sub_type = m.lastgroup
except AttributeError as e: except AttributeError as e:
sub_type = None sub_type = None
logger.critical(f"No run type found or run type found!: {e}") logger.critical(f"No procedure type found or procedure type found!: {e}")
return sub_type return sub_type
match filename: match filename:
@@ -107,8 +107,8 @@ class RSLNamer(object):
if "pytest" in sys.modules: if "pytest" in sys.modules:
raise ValueError("Submission Type came back as None.") raise ValueError("Submission Type came back as None.")
from frontend.widgets import ObjectSelector from frontend.widgets import ObjectSelector
dlg = ObjectSelector(title="Couldn't parse run type.", dlg = ObjectSelector(title="Couldn't parse procedure type.",
message="Please select run type from list below.", message="Please select procedure type from list below.",
obj_type=SubmissionType) obj_type=SubmissionType)
if dlg.exec(): if dlg.exec():
submission_type = dlg.parse_form() submission_type = dlg.parse_form()
@@ -118,7 +118,7 @@ class RSLNamer(object):
@classmethod @classmethod
def retrieve_rsl_number(cls, filename: str | Path, regex: re.Pattern | None = None): def retrieve_rsl_number(cls, filename: str | Path, regex: re.Pattern | None = None):
""" """
Uses regex to retrieve the plate number and run type from an input string Uses regex to retrieve the plate number and procedure type from an input string
Args: Args:
regex (str): string to construct pattern regex (str): string to construct pattern
@@ -145,14 +145,15 @@ class RSLNamer(object):
@classmethod @classmethod
def construct_new_plate_name(cls, data: dict) -> str: def construct_new_plate_name(cls, data: dict) -> str:
""" """
Make a brand-new plate name from run data. Make a brand-new plate name from procedure data.
Args: Args:
data (dict): incoming run data data (dict): incoming procedure data
Returns: Returns:
str: Output filename str: Output filename
""" """
logger.debug(data)
if "submitted_date" in data.keys(): if "submitted_date" in data.keys():
if isinstance(data['submitted_date'], dict): if isinstance(data['submitted_date'], dict):
if data['submitted_date']['value'] is not None: if data['submitted_date']['value'] is not None:
@@ -163,14 +164,16 @@ class RSLNamer(object):
today = data['submitted_date'] today = data['submitted_date']
else: else:
try: try:
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", data['rsl_plate_num']) today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", data['name'])
today = parse(today.group()) today = parse(today.group())
except (AttributeError, KeyError): except (AttributeError, KeyError):
today = datetime.now() today = datetime.now()
if "rsl_plate_num" in data.keys(): if isinstance(today, str):
plate_number = data['rsl_plate_num'].split("-")[-1][0] today = datetime.strptime(today, "%Y-%m-%d")
if "name" in data.keys():
plate_number = data['name'].split("-")[-1][0]
else: else:
previous = BasicRun.query(start_date=today, end_date=today, submissiontype=data['submission_type']) previous = Run.query(start_date=today, end_date=today, submissiontype=data['submissiontype'])
plate_number = len(previous) + 1 plate_number = len(previous) + 1
return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}" return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
@@ -205,4 +208,4 @@ class RSLNamer(object):
from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl, PydProcess, PydElastic, PydClientSubmission PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission

View File

@@ -650,22 +650,22 @@ class OmniProcess(BaseOmni):
new_assoc = st.to_sql() new_assoc = st.to_sql()
except AttributeError: except AttributeError:
new_assoc = SubmissionType.query(name=st) new_assoc = SubmissionType.query(name=st)
if new_assoc not in instance.submission_types: if new_assoc not in instance.proceduretype:
instance.submission_types.append(new_assoc) instance.proceduretype.append(new_assoc)
for er in self.equipment_roles: for er in self.equipment_roles:
try: try:
new_assoc = er.to_sql() new_assoc = er.to_sql()
except AttributeError: except AttributeError:
new_assoc = EquipmentRole.query(name=er) new_assoc = EquipmentRole.query(name=er)
if new_assoc not in instance.equipment_roles: if new_assoc not in instance.equipmentrole:
instance.equipment_roles.append(new_assoc) instance.equipmentrole.append(new_assoc)
for tr in self.tip_roles: for tr in self.tip_roles:
try: try:
new_assoc = tr.to_sql() new_assoc = tr.to_sql()
except AttributeError: except AttributeError:
new_assoc = TipRole.query(name=tr) new_assoc = TipRole.query(name=tr)
if new_assoc not in instance.tip_roles: if new_assoc not in instance.tiprole:
instance.tip_roles.append(new_assoc) instance.tiprole.append(new_assoc)
return instance return instance
@property @property

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,7 @@ class CustomFigure(Figure):
Creates list of buttons with one for each mode to be used in showing/hiding mode traces. Creates list of buttons with one for each mode to be used in showing/hiding mode traces.
Args: Args:
modes (list): list of modes used by main parser. modes (list): list of modes used by main clientsubmissionparser.
fig_len (int): number of traces in the figure fig_len (int): number of traces in the figure
Returns: Returns:

View File

@@ -28,12 +28,12 @@ class ConcentrationsChart(CustomFigure):
self.df = df self.df = df
try: try:
self.df = self.df[self.df.concentration.notnull()] self.df = self.df[self.df.concentration.notnull()]
self.df = self.df.sort_values(['submitted_date', 'run'], ascending=[True, True]).reset_index( self.df = self.df.sort_values(['submitted_date', 'procedure'], ascending=[True, True]).reset_index(
drop=True) drop=True)
self.df = self.df.reset_index().rename(columns={"index": "idx"}) self.df = self.df.reset_index().rename(columns={"index": "idx"})
# logger.debug(f"DF after changes:\n{self.df}") # logger.debug(f"DF after changes:\n{self.df}")
scatter = px.scatter(data_frame=self.df, x='run', y="concentration", scatter = px.scatter(data_frame=self.df, x='procedure', y="concentration",
hover_data=["name", "run", "submitted_date", "concentration"], hover_data=["name", "procedure", "submitted_date", "concentration"],
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"} color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}
) )
except (ValueError, AttributeError) as e: except (ValueError, AttributeError) as e:
@@ -44,11 +44,11 @@ class ConcentrationsChart(CustomFigure):
for trace in traces: for trace in traces:
self.add_trace(trace) self.add_trace(trace)
try: try:
tickvals = self.df['run'].tolist() tickvals = self.df['procedure'].tolist()
except KeyError: except KeyError:
tickvals = [] tickvals = []
try: try:
ticklabels = self.df['run'].tolist() ticklabels = self.df['procedure'].tolist()
except KeyError: except KeyError:
ticklabels = [] ticklabels = []
self.update_layout( self.update_layout(

View File

@@ -1,5 +1,5 @@
""" """
Functions for constructing irida controls graphs using plotly. Functions for constructing irida control graphs using plotly.
""" """
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
@@ -23,12 +23,12 @@ class IridaFigure(CustomFigure):
def construct_chart(self, df: pd.DataFrame, modes: list, start_date: date, end_date:date): def construct_chart(self, df: pd.DataFrame, modes: list, start_date: date, end_date:date):
""" """
Creates a plotly chart for controls from a pandas dataframe Creates a plotly chart for control from a pandas dataframe
Args: Args:
end_date (): end_date ():
start_date (): start_date ():
df (pd.DataFrame): input dataframe of controls df (pd.DataFrame): input dataframe of control
modes (list): analysis modes to construct charts for modes (list): analysis modes to construct charts for
ytitle (str | None, optional): title on the y-axis. Defaults to None. ytitle (str | None, optional): title on the y-axis. Defaults to None.

View File

@@ -1,5 +1,5 @@
""" """
Functions for constructing irida controls graphs using plotly. Functions for constructing irida control graphs using plotly.
""" """
from pprint import pformat from pprint import pformat
from . import CustomFigure from . import CustomFigure

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from pandas import ExcelWriter from pandas import ExcelWriter
from backend import Reagent, BasicSample, Organization, KitType, BasicRun from backend import Reagent, Sample, ClientSubmission, KitType, Run
from tools import ( from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development under_development
@@ -22,7 +22,7 @@ from .date_type_picker import DateTypePicker
from .functions import select_save_file from .functions import select_save_file
from .pop_ups import HTMLPop from .pop_ups import HTMLPop
from .misc import Pagifier from .misc import Pagifier
from .submission_table import SubmissionsSheet, SubmissionsTree, ClientRunModel from .submission_table import SubmissionsSheet, SubmissionsTree, ClientSubmissionRunModel
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
from .summary import Summary from .summary import Summary
@@ -30,7 +30,7 @@ from .turnaround import TurnaroundTime
from .concentrations import Concentrations from .concentrations import Concentrations
from .omni_search import SearchBox from .omni_search import SearchBox
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'procedure.{__name__}')
class App(QMainWindow): class App(QMainWindow):
@@ -57,7 +57,7 @@ class App(QMainWindow):
# NOTE: insert tabs into main app # NOTE: insert tabs into main app
self.table_widget = AddSubForm(self) self.table_widget = AddSubForm(self)
self.setCentralWidget(self.table_widget) self.setCentralWidget(self.table_widget)
# NOTE: run initial setups # NOTE: procedure initial setups
self._createActions() self._createActions()
self._createMenuBar() self._createMenuBar()
self._createToolBar() self._createToolBar()
@@ -173,14 +173,14 @@ class App(QMainWindow):
def runSampleSearch(self): def runSampleSearch(self):
""" """
Create a search for samples. Create a search for sample.
""" """
dlg = SearchBox(self, object_type=BasicSample, extras=[]) dlg = SearchBox(self, object_type=Sample, extras=[])
dlg.exec() dlg.exec()
@check_authorization @check_authorization
def edit_reagent(self, *args, **kwargs): def edit_reagent(self, *args, **kwargs):
dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="role")]) dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="reagentrole")])
dlg.exec() dlg.exec()
def update_data(self): def update_data(self):
@@ -239,7 +239,7 @@ class AddSubForm(QWidget):
self.tabs.addTab(self.tab3, "PCR Controls") self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report") self.tabs.addTab(self.tab4, "Cost Report")
self.tabs.addTab(self.tab5, "Turnaround Times") self.tabs.addTab(self.tab5, "Turnaround Times")
# NOTE: Create run adder form # NOTE: Create procedure adder form
self.formwidget = SubmissionFormContainer(self) self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self) self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout) self.formwidget.setLayout(self.formlayout)
@@ -249,12 +249,12 @@ class AddSubForm(QWidget):
self.interior.setWidgetResizable(True) self.interior.setWidgetResizable(True)
self.interior.setFixedWidth(325) self.interior.setFixedWidth(325)
self.interior.setWidget(self.formwidget) self.interior.setWidget(self.formwidget)
# NOTE: Create sheet to hold existing submissions # NOTE: Create sheet to hold existing procedure
self.sheetwidget = QWidget(self) self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self) self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout) self.sheetwidget.setLayout(self.sheetlayout)
# self.sub_wid = SubmissionsSheet(parent=parent) # self.sub_wid = SubmissionsSheet(parent=parent)
self.sub_wid = SubmissionsTree(parent=parent, model=ClientRunModel(self)) self.sub_wid = SubmissionsTree(parent=parent, model=ClientSubmissionRunModel(self))
self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size) self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size)
self.sheetlayout.addWidget(self.sub_wid) self.sheetlayout.addWidget(self.sub_wid)
self.sheetlayout.addWidget(self.pager) self.sheetlayout.addWidget(self.pager)
@@ -264,11 +264,13 @@ class AddSubForm(QWidget):
self.tab1.layout.addWidget(self.interior) self.tab1.layout.addWidget(self.interior)
self.tab1.layout.addWidget(self.sheetwidget) self.tab1.layout.addWidget(self.sheetwidget)
self.tab2.layout = QVBoxLayout(self) self.tab2.layout = QVBoxLayout(self)
self.irida_viewer = ControlsViewer(self, archetype="Irida Control") # self.irida_viewer = ControlsViewer(self, archetype="Irida Control")
self.irida_viewer = None
self.tab2.layout.addWidget(self.irida_viewer) self.tab2.layout.addWidget(self.irida_viewer)
self.tab2.setLayout(self.tab2.layout) self.tab2.setLayout(self.tab2.layout)
self.tab3.layout = QVBoxLayout(self) self.tab3.layout = QVBoxLayout(self)
self.pcr_viewer = ControlsViewer(self, archetype="PCR Control") # self.pcr_viewer = ControlsViewer(self, archetype="PCR Control")
self.pcr_viewer = None
self.tab3.layout.addWidget(self.pcr_viewer) self.tab3.layout.addWidget(self.pcr_viewer)
self.tab3.setLayout(self.tab3.layout) self.tab3.setLayout(self.tab3.layout)
summary_report = Summary(self) summary_report = Summary(self)

View File

@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (
) )
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from backend import ChartReportMaker from backend import ChartReportMaker
from backend.db import ControlType, IridaControl from backend.db import ControlType
import logging import logging
from tools import Report, report_result from tools import Report, report_result
from frontend.visualizations import CustomFigure from frontend.visualizations import CustomFigure
@@ -25,7 +25,7 @@ class ControlsViewer(InfoPane):
return return
# NOTE: set tab2 layout # NOTE: set tab2 layout
self.control_sub_typer = QComboBox() self.control_sub_typer = QComboBox()
# NOTE: fetch types of controls # NOTE: fetch types of control
con_sub_types = [item for item in self.archetype.targets.keys()] con_sub_types = [item for item in self.archetype.targets.keys()]
self.control_sub_typer.addItems(con_sub_types) self.control_sub_typer.addItems(con_sub_types)
# NOTE: create custom widget to get types of analysis -- disabled by PCR control # NOTE: create custom widget to get types of analysis -- disabled by PCR control
@@ -52,7 +52,7 @@ class ControlsViewer(InfoPane):
@report_result @report_result
def update_data(self, *args, **kwargs): def update_data(self, *args, **kwargs):
""" """
Get controls based on start/end dates Get control based on start/end dates
""" """
super().update_data() super().update_data()
# NOTE: mode_sub_type defaults to disabled # NOTE: mode_sub_type defaults to disabled
@@ -70,7 +70,7 @@ class ControlsViewer(InfoPane):
sub_types = [] sub_types = []
# NOTE: added in allowed to have subtypes in case additions made in future. # NOTE: added in allowed to have subtypes in case additions made in future.
if sub_types and self.mode.lower() in self.archetype.instance_class.subtyping_allowed: if sub_types and self.mode.lower() in self.archetype.instance_class.subtyping_allowed:
# NOTE: block signal that will rerun controls getter and update mode_sub_typer # NOTE: block signal that will rerun control getter and update mode_sub_typer
with QSignalBlocker(self.mode_sub_typer) as blocker: with QSignalBlocker(self.mode_sub_typer) as blocker:
self.mode_sub_typer.addItems(sub_types) self.mode_sub_typer.addItems(sub_types)
self.mode_sub_typer.setEnabled(True) self.mode_sub_typer.setEnabled(True)
@@ -83,7 +83,7 @@ class ControlsViewer(InfoPane):
@report_result @report_result
def chart_maker_function(self, *args, **kwargs): def chart_maker_function(self, *args, **kwargs):
""" """
Create html chart for controls reporting Create html chart for control reporting
Args: Args:
obj (QMainWindow): original app window obj (QMainWindow): original app window
@@ -98,7 +98,7 @@ class ControlsViewer(InfoPane):
else: else:
self.mode_sub_type = self.mode_sub_typer.currentText() self.mode_sub_type = self.mode_sub_typer.currentText()
months = self.diff_month(self.start_date, self.end_date) months = self.diff_month(self.start_date, self.end_date)
# NOTE: query all controls using the type/start and end dates from the gui # NOTE: query all control using the type/start and end dates from the gui
chart_settings = dict( chart_settings = dict(
sub_type=self.con_sub_type, sub_type=self.con_sub_type,
start_date=self.start_date, start_date=self.start_date,

View File

@@ -6,7 +6,7 @@ from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
) )
from backend.db.models import Equipment, BasicRun, Process from backend.db.models import Equipment, Run, Process, Procedure
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging import logging
from typing import Generator from typing import Generator
@@ -16,13 +16,13 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog): class EquipmentUsage(QDialog):
def __init__(self, parent, submission: BasicRun): def __init__(self, parent, procedure: Procedure):
super().__init__(parent) super().__init__(parent)
self.submission = submission self.procedure = procedure
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}") self.setWindowTitle(f"Equipment Checklist - {procedure.rsl_plate_num}")
self.used_equipment = self.submission.used_equipment self.used_equipment = self.procedure.equipment
self.kit = self.submission.extraction_kit self.kit = self.procedure.kittype
self.opt_equipment = submission.submission_type.get_equipment() self.opt_equipment = procedure.proceduretype.get_equipment()
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setLayout(self.layout) self.setLayout(self.layout)
self.populate_form() self.populate_form()
@@ -120,7 +120,7 @@ class RoleComboBox(QWidget):
def update_processes(self): def update_processes(self):
""" """
Changes processes when equipment is changed Changes process when equipment is changed
""" """
equip = self.box.currentText() equip = self.box.currentText()
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0]) equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
@@ -134,10 +134,10 @@ class RoleComboBox(QWidget):
""" """
process = self.process.currentText().strip() process = self.process.currentText().strip()
process = Process.query(name=process) process = Process.query(name=process)
if process.tip_roles: if process.tiprole:
for iii, tip_role in enumerate(process.tip_roles): for iii, tip_role in enumerate(process.tiprole):
widget = QComboBox() widget = QComboBox()
tip_choices = [item.name for item in tip_role.controls] tip_choices = [item.name for item in tip_role.control]
widget.setEditable(False) widget.setEditable(False)
widget.addItems(tip_choices) widget.addItems(tip_choices)
widget.setObjectName(f"tips_{tip_role.name}") widget.setObjectName(f"tips_{tip_role.name}")

View File

@@ -12,7 +12,7 @@ import logging, numpy as np
from pprint import pformat from pprint import pformat
from typing import Tuple, List from typing import Tuple, List
from pathlib import Path from pathlib import Path
from backend.db.models import BasicRun from backend.db.models import Run
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,7 +20,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
# Main window class # Main window class
class GelBox(QDialog): class GelBox(QDialog):
def __init__(self, parent, img_path: str | Path, submission: BasicRun): def __init__(self, parent, img_path: str | Path, submission: Run):
super().__init__(parent) super().__init__(parent)
# NOTE: setting title # NOTE: setting title
self.setWindowTitle(f"Gel - {img_path}") self.setWindowTitle(f"Gel - {img_path}")
@@ -135,7 +135,7 @@ class ControlsForm(QWidget):
def parse_form(self) -> Tuple[List[dict], str]: def parse_form(self) -> Tuple[List[dict], str]:
""" """
Pulls the controls statuses from the form. Pulls the control statuses from the form.
Returns: Returns:
List[dict]: output of values List[dict]: output of values

View File

@@ -39,7 +39,7 @@ class InfoPane(QWidget):
lastmonth = self.datepicker.end_date.date().addDays(-31) lastmonth = self.datepicker.end_date.date().addDays(-31)
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}." msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
logger.warning(msg) logger.warning(msg)
# NOTE: block signal that will rerun controls getter and set start date without triggering this function again # NOTE: block signal that will rerun control getter and set start date without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker: with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth) self.datepicker.start_date.setDate(lastmonth)
self.update_data() self.update_data()

View File

@@ -19,7 +19,7 @@ env = jinja_template_loading()
class StartEndDatePicker(QWidget): class StartEndDatePicker(QWidget):
""" """
custom widget to pick start and end dates for controls graphs custom widget to pick start and end dates for control graphs
""" """
def __init__(self, default_start: int) -> None: def __init__(self, default_start: int) -> None:

View File

@@ -71,7 +71,7 @@ class AddEdit(QDialog):
# logger.debug(f"We have an elastic model.") # logger.debug(f"We have an elastic model.")
parsed['instance'] = self.instance parsed['instance'] = self.instance
# NOTE: Hand-off to pydantic model for validation. # NOTE: Hand-off to pydantic model for validation.
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts. # NOTE: Also, why am I not just using the toSQL method here. I could write one for contact.
model = model(**parsed) model = model(**parsed)
return model, report return model, report

View File

@@ -144,7 +144,7 @@ class ManagerWindow(QDialog):
def update_data(self) -> None: def update_data(self) -> None:
""" """
Performs updating of widgets on first run and after options change. Performs updating of widgets on first procedure and after options change.
Returns: Returns:
None None

View File

@@ -100,7 +100,7 @@ class SearchBox(QDialog):
def update_data(self): def update_data(self):
""" """
Shows dataframe of relevant samples. Shows dataframe of relevant sample.
""" """
fields = self.parse_form() fields = self.parse_form()
sample_list_creator = self.object_type.fuzzy_search(**fields) sample_list_creator = self.object_type.fuzzy_search(**fields)

View File

@@ -1,4 +1,3 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List from typing import List
@@ -6,9 +5,12 @@ from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout
from backend.validators import PydSubmission
from backend.db.models import ClientSubmission
from backend.validators import PydSample, RSLNamer
from tools import get_application_from_parent, jinja_template_loading from tools import get_application_from_parent, jinja_template_loading
env = jinja_template_loading() env = jinja_template_loading()
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -16,9 +18,13 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SampleChecker(QDialog): class SampleChecker(QDialog):
def __init__(self, parent, title:str, pyd: PydSubmission): def __init__(self, parent, title: str, samples: List[PydSample], clientsubmission: ClientSubmission|None=None):
super().__init__(parent) super().__init__(parent)
self.pyd = pyd if clientsubmission:
self.rsl_plate_num = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
else:
self.rsl_plate_num = clientsubmission
self.samples = samples
self.setWindowTitle(title) self.setWindowTitle(title)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self) self.webview = QWebEngineView(parent=self)
@@ -36,9 +42,10 @@ class SampleChecker(QDialog):
css = f.read() css = f.read()
try: try:
samples = self.formatted_list samples = self.formatted_list
except AttributeError: except AttributeError as e:
logger.error(f"Problem getting sample list: {e}")
samples = [] samples = []
html = template.render(samples=samples, css=css) html = template.render(samples=samples, css=css, rsl_plate_num=self.rsl_plate_num)
self.webview.setHtml(html) self.webview.setHtml(html)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
@@ -51,25 +58,37 @@ class SampleChecker(QDialog):
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
def text_changed(self, submission_rank: str, key: str, new_value: str): def text_changed(self, submission_rank: str, key: str, new_value: str):
logger.debug(f"Name: {submission_rank}, Key: {key}, Value: {new_value}") logger.debug(f"Name: {submission_rank}, Key: {key}, Value: {new_value}")
match key:
case "row" | "column":
value = [new_value]
case _:
value = new_value
try: try:
item = next((sample for sample in self.pyd.samples if int(submission_rank) in sample.submission_rank)) item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
except StopIteration: except StopIteration:
logger.error(f"Unable to find sample {submission_rank}") logger.error(f"Unable to find sample {submission_rank}")
return return
item.__setattr__(key, value) item.__setattr__(key, new_value)
@pyqtSlot(str, bool)
def enable_sample(self, submission_rank: str, enabled: bool):
logger.debug(f"Name: {submission_rank}, Enabled: {enabled}")
try:
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
except StopIteration:
logger.error(f"Unable to find sample {submission_rank}")
return
item.__setattr__("enabled", enabled)
@pyqtSlot(str)
def set_rsl_plate_num(self, rsl_plate_num: str):
logger.debug(f"RSL plate num: {rsl_plate_num}")
self.rsl_plate_num = rsl_plate_num
@property @property
def formatted_list(self) -> List[dict]: def formatted_list(self) -> List[dict]:
output = [] output = []
for sample in self.pyd.sample_list: for sample in self.samples:
if sample['submitter_id'] in [item['submitter_id'] for item in output]: logger.debug(sample)
sample['color'] = "red" s = sample.improved_dict(dictionaries=False)
if s['sample_id'] in [item['sample_id'] for item in output]:
s['color'] = "red"
else: else:
sample['color'] = "black" s['color'] = "black"
output.append(sample) output.append(s)
return output return output

View File

@@ -1,5 +1,5 @@
""" """
Webview to show run and sample details. Webview to show procedure and sample details.
""" """
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit, QGridLayout) QDialogButtonBox, QTextEdit, QGridLayout)
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
from backend.db.models import BasicRun, BasicSample, Reagent, KitType, Equipment, Process, Tips from backend.db.models import Run, Sample, Reagent, KitType, Equipment, Process, Tips
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
from .functions import select_save_file, save_pdf from .functions import select_save_file, save_pdf
from pathlib import Path from pathlib import Path
@@ -18,15 +18,15 @@ from pprint import pformat
from typing import List from typing import List
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"procedure.{__name__}")
class SubmissionDetails(QDialog): class SubmissionDetails(QDialog):
""" """
a window showing text details of run a window showing text details of procedure
""" """
def __init__(self, parent, sub: BasicRun | BasicSample | Reagent) -> None: def __init__(self, parent, sub: Run | Sample | Reagent) -> None:
super().__init__(parent) super().__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -51,10 +51,10 @@ class SubmissionDetails(QDialog):
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject('backend', self) self.channel.registerObject('backend', self)
match sub: match sub:
case BasicRun(): case Run():
self.run_details(run=sub) self.run_details(run=sub)
self.rsl_plate_num = sub.rsl_plate_num self.rsl_plate_num = sub.rsl_plate_num
case BasicSample(): case Sample():
self.sample_details(sample=sub) self.sample_details(sample=sub)
case Reagent(): case Reagent():
self.reagent_details(reagent=sub) self.reagent_details(reagent=sub)
@@ -127,7 +127,7 @@ class SubmissionDetails(QDialog):
self.setWindowTitle(f"Process Details - {tips.name}") self.setWindowTitle(f"Process Details - {tips.name}")
@pyqtSlot(str) @pyqtSlot(str)
def sample_details(self, sample: str | BasicSample): def sample_details(self, sample: str | Sample):
""" """
Changes details view to summary of Sample Changes details view to summary of Sample
@@ -136,19 +136,19 @@ class SubmissionDetails(QDialog):
""" """
logger.debug(f"Sample details.") logger.debug(f"Sample details.")
if isinstance(sample, str): if isinstance(sample, str):
sample = BasicSample.query(submitter_id=sample) sample = Sample.query(sample_id=sample)
base_dict = sample.to_sub_dict(full_data=True) base_dict = sample.to_sub_dict(full_data=True)
exclude = ['submissions', 'excluded', 'colour', 'tooltip'] exclude = ['procedure', 'excluded', 'colour', 'tooltip']
base_dict['excluded'] = exclude base_dict['excluded'] = exclude
template = sample.details_template template = sample.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
html = template.render(sample=base_dict, css=css) html = template.render(sample=base_dict, css=css)
# with open(f"{sample.submitter_id}.html", 'w') as f: # with open(f"{sample.sample_id}.html", 'w') as f:
# f.write(html) # f.write(html)
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"Sample Details - {sample.submitter_id}") self.setWindowTitle(f"Sample Details - {sample.sample_id}")
@pyqtSlot(str, str) @pyqtSlot(str, str)
def reagent_details(self, reagent: str | Reagent, kit: str | KitType): def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
@@ -156,7 +156,7 @@ class SubmissionDetails(QDialog):
Changes details view to summary of Reagent Changes details view to summary of Reagent
Args: Args:
kit (str | KitType): Name of kit. kit (str | KitType): Name of kittype.
reagent (str | Reagent): Lot number of the reagent reagent (str | Reagent): Lot number of the reagent
""" """
logger.debug(f"Reagent details.") logger.debug(f"Reagent details.")
@@ -164,7 +164,7 @@ class SubmissionDetails(QDialog):
reagent = Reagent.query(lot=reagent) reagent = Reagent.query(lot=reagent)
if isinstance(kit, str): if isinstance(kit, str):
self.kit = KitType.query(name=kit) self.kit = KitType.query(name=kit)
base_dict = reagent.to_sub_dict(extraction_kit=self.kit, full_data=True) base_dict = reagent.to_sub_dict(kittype=self.kit, full_data=True)
env = jinja_template_loading() env = jinja_template_loading()
temp_name = "reagent_details.html" temp_name = "reagent_details.html"
try: try:
@@ -203,7 +203,7 @@ class SubmissionDetails(QDialog):
logger.error(f"Reagent with lot {old_lot} not found.") logger.error(f"Reagent with lot {old_lot} not found.")
@pyqtSlot(str) @pyqtSlot(str)
def run_details(self, run: str | BasicRun): def run_details(self, run: str | Run):
""" """
Sets details view to summary of Submission. Sets details view to summary of Submission.
@@ -212,24 +212,24 @@ class SubmissionDetails(QDialog):
""" """
logger.debug(f"Submission details.") logger.debug(f"Submission details.")
if isinstance(run, str): if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run) run = Run.query(name=run)
self.rsl_plate_num = run.rsl_plate_num self.rsl_plate_num = run.rsl_plate_num
self.base_dict = run.to_dict(full_data=True) self.base_dict = run.to_dict(full_data=True)
# NOTE: don't want id # NOTE: don't want id
self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked) self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked)
self.base_dict['excluded'] = run.get_default_info("details_ignore") self.base_dict['excluded'] = run.get_default_info("details_ignore")
self.base_dict, self.template = run.get_details_template(base_dict=self.base_dict) self.template = run.details_template
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
# logger.debug(f"Base dictionary of run {self.rsl_plate_num}: {pformat(self.base_dict)}") # logger.debug(f"Base dictionary of procedure {self.name}: {pformat(self.base_dict)}")
self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css) self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html) self.webview.setHtml(self.html)
@pyqtSlot(str) @pyqtSlot(str)
def sign_off(self, run: str | BasicRun) -> None: def sign_off(self, run: str | Run) -> None:
""" """
Allows power user to signify a run is complete. Allows power user to signify a procedure is complete.
Args: Args:
run (str | BasicRun): Submission to be completed run (str | BasicRun): Submission to be completed
@@ -239,7 +239,7 @@ class SubmissionDetails(QDialog):
""" """
logger.info(f"Signing off on {run} - ({getuser()})") logger.info(f"Signing off on {run} - ({getuser()})")
if isinstance(run, str): if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run) run = Run.query(name=run)
run.signed_by = getuser() run.signed_by = getuser()
run.completed_date = datetime.now() run.completed_date = datetime.now()
run.completed_date.replace(tzinfo=timezone) run.completed_date.replace(tzinfo=timezone)
@@ -248,7 +248,7 @@ class SubmissionDetails(QDialog):
def save_pdf(self): def save_pdf(self):
""" """
Renders run to html, then creates and saves .pdf file to user selected file. Renders procedure to html, then creates and saves .pdf file to user selected file.
""" """
fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf") fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf")
save_pdf(obj=self.webview, filename=fname) save_pdf(obj=self.webview, filename=fname)
@@ -256,11 +256,11 @@ class SubmissionDetails(QDialog):
class SubmissionComment(QDialog): class SubmissionComment(QDialog):
""" """
a window for adding comment text to a run a window for adding comment text to a procedure
""" """
def __init__(self, parent, submission: BasicRun) -> None: def __init__(self, parent, submission: Run) -> None:
logger.debug(parent)
super().__init__(parent) super().__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.submission = submission self.submission = submission
@@ -282,7 +282,7 @@ class SubmissionComment(QDialog):
def parse_form(self) -> List[dict]: def parse_form(self) -> List[dict]:
""" """
Adds comment to run object. Adds comment to procedure object.
""" """
commenter = getuser() commenter = getuser()
comment = self.txt_editor.toPlainText() comment = self.txt_editor.toPlainText()

View File

@@ -1,18 +1,20 @@
""" """
Contains widgets specific to the run summary and run details. Contains widgets specific to the procedure summary and procedure details.
""" """
import logging
import sys import sys, logging, re
from pprint import pformat from pprint import pformat
from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \ from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent
from backend.db.models import BasicRun, ClientSubmission
from backend.db.models import Run, ClientSubmission
from tools import Report, Result, report_result from tools import Report, Result, report_result
from .functions import select_open_file from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"procedure.{__name__}")
class pandasModel(QAbstractTableModel): class pandasModel(QAbstractTableModel):
@@ -63,7 +65,7 @@ class pandasModel(QAbstractTableModel):
class SubmissionsSheet(QTableView): class SubmissionsSheet(QTableView):
""" """
presents run summary to user in tab1 presents procedure summary to user in tab1
""" """
def __init__(self, parent) -> None: def __init__(self, parent) -> None:
@@ -78,16 +80,16 @@ class SubmissionsSheet(QTableView):
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.doubleClicked.connect(lambda x: BasicRun.query(id=x.sibling(x.row(), 0).data()).show_details(self)) self.doubleClicked.connect(lambda x: Run.query(id=x.sibling(x.row(), 0).data()).show_details(self))
# NOTE: Have to run native query here because mine just returns results? # NOTE: Have to procedure native query here because mine just returns results?
self.total_count = BasicRun.__database_session__.query(BasicRun).count() self.total_count = Run.__database_session__.query(Run).count()
def set_data(self, page: int = 1, page_size: int = 250) -> None: def set_data(self, page: int = 1, page_size: int = 250) -> None:
""" """
sets data in model sets data in model
""" """
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size) # self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = BasicRun.submissions_to_df(page=page, page_size=page_size) self.data = Run.submissions_to_df(page=page, page_size=page_size)
try: try:
self.data['Id'] = self.data['Id'].apply(str) self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4) self.data['Id'] = self.data['Id'].str.zfill(4)
@@ -108,7 +110,7 @@ class SubmissionsSheet(QTableView):
id = self.selectionModel().currentIndex() id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0) # NOTE: Convert to data in id column (i.e. column 0)
id = id.sibling(id.row(), 0).data() id = id.sibling(id.row(), 0).data()
submission = BasicRun.query(id=id) submission = Run.query(id=id)
self.menu = QMenu(self) self.menu = QMenu(self)
self.con_actions = submission.custom_context_events() self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys(): for k in self.con_actions.keys():
@@ -140,7 +142,7 @@ class SubmissionsSheet(QTableView):
def link_extractions_function(self): def link_extractions_function(self):
""" """
Link extractions from runlogs to imported submissions Link extractions from runlogs to imported procedure
Args: Args:
obj (QMainWindow): original app window obj (QMainWindow): original app window
@@ -166,9 +168,9 @@ class SubmissionsSheet(QTableView):
# NOTE: elution columns are item 6 in the comma split list to the end # NOTE: elution columns are item 6 in the comma split list to the end
for ii in range(6, len(run)): for ii in range(6, len(run)):
new_run[f"column{str(ii - 5)}_vol"] = run[ii] new_run[f"column{str(ii - 5)}_vol"] = run[ii]
# NOTE: Lookup imported submissions # NOTE: Lookup imported procedure
sub = BasicRun.query(rsl_plate_num=new_run['rsl_plate_num']) sub = Run.query(name=new_run['name'])
# NOTE: If no such run exists, move onto the next run # NOTE: If no such procedure exists, move onto the next procedure
if sub is None: if sub is None:
continue continue
try: try:
@@ -192,7 +194,7 @@ class SubmissionsSheet(QTableView):
def link_pcr_function(self): def link_pcr_function(self):
""" """
Link PCR data from run logs to an imported run Link PCR data from procedure logs to an imported procedure
Args: Args:
obj (QMainWindow): original app window obj (QMainWindow): original app window
@@ -215,9 +217,9 @@ class SubmissionsSheet(QTableView):
experiment_name=run[4].strip(), experiment_name=run[4].strip(),
end_time=run[5].strip() end_time=run[5].strip()
) )
# NOTE: lookup imported run # NOTE: lookup imported procedure
sub = BasicRun.query(rsl_number=new_run['rsl_plate_num']) sub = Run.query(rsl_number=new_run['name'])
# NOTE: if imported run doesn't exist move on to next run # NOTE: if imported procedure doesn't exist move on to next procedure
if sub is None: if sub is None:
continue continue
sub.set_attribute('pcr_info', new_run) sub.set_attribute('pcr_info', new_run)
@@ -227,9 +229,10 @@ class SubmissionsSheet(QTableView):
return report return report
class RunDelegate(QStyledItemDelegate): class ClientSubmissionDelegate(QStyledItemDelegate):
def __init__(self, parent=None): def __init__(self, parent=None):
super(RunDelegate, self).__init__(parent) super(ClientSubmissionDelegate, self).__init__(parent)
pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
icon1 = QWidget().style().standardIcon(pixmapi) icon1 = QWidget().style().standardIcon(pixmapi)
pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
@@ -238,23 +241,29 @@ class RunDelegate(QStyledItemDelegate):
self._minus_icon = icon2 self._minus_icon = icon2
def initStyleOption(self, option, index): def initStyleOption(self, option, index):
super(RunDelegate, self).initStyleOption(option, index) super(ClientSubmissionDelegate, self).initStyleOption(option, index)
if not index.parent().isValid(): if not index.parent().isValid():
is_open = bool(option.state & QStyle.StateFlag.State_Open) is_open = bool(option.state & QStyle.StateFlag.State_Open)
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
option.icon = self._minus_icon if is_open else self._plus_icon option.icon = self._minus_icon if is_open else self._plus_icon
class RunDelegate(ClientSubmissionDelegate):
pass
class SubmissionsTree(QTreeView): class SubmissionsTree(QTreeView):
""" """
https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt
""" """
def __init__(self, model, parent=None): def __init__(self, model, parent=None):
super(SubmissionsTree, self).__init__(parent) super(SubmissionsTree, self).__init__(parent)
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count() self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
self.setIndentation(0) self.setIndentation(0)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
self.clicked.connect(self.on_clicked) self.clicked.connect(self.on_clicked)
delegate = RunDelegate(self) delegate = ClientSubmissionDelegate(self)
self.setItemDelegateForColumn(0, delegate) self.setItemDelegateForColumn(0, delegate)
self.model = model self.model = model
self.setModel(self.model) self.setModel(self.model)
@@ -263,32 +272,69 @@ class SubmissionsTree(QTreeView):
# self.setStyleSheet("background-color: #0D1225;") # self.setStyleSheet("background-color: #0D1225;")
self.set_data() self.set_data()
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
# self.customContextMenuRequested.connect(self.open_menu)
for ii in range(2): for ii in range(2):
self.resizeColumnToContents(ii) self.resizeColumnToContents(ii)
@pyqtSlot(QModelIndex) @pyqtSlot(QModelIndex)
def on_clicked(self, index): def on_clicked(self, index):
if not index.parent().isValid() and index.column() == 0: if not index.parent().isValid() and index.column() == 0:
self.setExpanded(index, not self.isExpanded(index)) self.setExpanded(index, not self.isExpanded(index))
def contextMenuEvent(self, event: QContextMenuEvent):
"""
Creates actions for right click menu events.
Args:
event (_type_): the item of interest
"""
indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1)))
query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(query_obj)
# NOTE: Convert to data in id column (i.e. column 0)
# id = id.sibling(id.row(), 0).data()
# logger.debug(id.model().query_group_object(id.row()))
# clientsubmission = id.model().query_group_object(id.row())
self.menu = QMenu(self)
self.con_actions = query_obj.custom_context_events
for key in self.con_actions.keys():
if key.lower() == "add procedure":
action = QMenu(self.menu)
action.setTitle("Add Procedure")
for procedure in query_obj.allowed_procedures:
proc_name = procedure.name
proc = QAction(proc_name, action)
proc.triggered.connect(lambda _, procedure_name=proc_name: self.con_actions['Add Procedure'](obj=self, proceduretype_name=procedure_name))
action.addAction(proc)
self.menu.addMenu(action)
else:
action = QAction(key, self)
action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self))
self.menu.addAction(action)
# # NOTE: add other required actions
self.menu.popup(QCursor.pos())
def set_data(self, page: int = 1, page_size: int = 250) -> None: def set_data(self, page: int = 1, page_size: int = 250) -> None:
""" """
sets data in model sets data in model
""" """
self.clear() self.clear()
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size) self.data = [item.to_dict(full_data=True) for item in
self.data = [item.to_dict(full_data=True) for item in ClientSubmission.query(chronologic=True, page=page, page_size=page_size)] ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
logger.debug(pformat(self.data)) logger.debug(f"setting data:\n {pformat(self.data)}")
# sys.exit() # sys.exit()
for submission in self.data: for submission in self.data:
group_str = f"{submission['submission_type']}-{submission['submitter_plate_number']}-{submission['submitted_date']}" group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
group_item = self.model.add_group(group_str) group_item = self.model.add_group(group_str, query_str=submission['submitter_plate_id'])
for run in submission['runs']: for run in submission['run']:
self.model.append_element_to_group(group_item=group_item, element=run) self.model.append_element_to_group(group_item=group_item, element=run)
def clear(self): def clear(self):
if self.model != None: if self.model != None:
# self.model.clear() # works # self.model.clear() # works
@@ -302,8 +348,7 @@ class SubmissionsTree(QTreeView):
id = int(id.data()) id = int(id.data())
except ValueError: except ValueError:
return return
BasicRun.query(id=id).show_details(self) Run.query(id=id).show_details(self)
def link_extractions(self): def link_extractions(self):
pass pass
@@ -312,62 +357,64 @@ class SubmissionsTree(QTreeView):
pass pass
class ClientRunModel(QStandardItemModel): class ClientSubmissionRunModel(QStandardItemModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(ClientRunModel, self).__init__(parent) super(ClientSubmissionRunModel, self).__init__(parent)
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Technician", "Signed By"] headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
self.setColumnCount(len(headers)) self.setColumnCount(len(headers))
self.setHorizontalHeaderLabels(headers) self.setHorizontalHeaderLabels(headers)
for i in range(self.columnCount()): def add_group(self, item_name, query_str: str):
it = self.horizontalHeaderItem(i)
try:
logger.debug(it.text())
except AttributeError:
pass
# it.setForeground(QColor("#F2F2F2"))
def add_group(self, group_name):
item_root = QStandardItem() item_root = QStandardItem()
item_root.setEditable(False) item_root.setEditable(False)
item = QStandardItem(group_name) item = QStandardItem(item_name)
item.setEditable(False) item.setEditable(False)
ii = self.invisibleRootItem() ii = self.invisibleRootItem()
i = ii.rowCount() i = ii.rowCount()
for j, it in enumerate((item_root, item)): for j, it in enumerate((item_root, item)):
# NOTE: Adding item to invisible root row i, column j (wherever j comes from)
ii.setChild(i, j, it) ii.setChild(i, j, it)
ii.setEditable(False) ii.setEditable(False)
for j in range(self.columnCount()): for j in range(self.columnCount()):
it = ii.child(i, j) it = ii.child(i, j)
if it is None: if it is None:
# NOTE: Set invisible root child to empty if it is None.
it = QStandardItem() it = QStandardItem()
ii.setChild(i, j, it) ii.setChild(i, j, it)
# it.setBackground(QColor("#002842")) item_root.setData(dict(item_type=ClientSubmission, query_str=query_str), 1)
# it.setForeground(QColor("#F2F2F2"))
return item_root return item_root
def append_element_to_group(self, group_item, element: dict): def append_element_to_group(self, group_item, element: dict):
logger.debug(f"Element: {pformat(element)}") # logger.debug(f"Element: {pformat(element)}")
j = group_item.rowCount() j = group_item.rowCount()
item_icon = QStandardItem() item_icon = QStandardItem()
item_icon.setEditable(False) item_icon.setEditable(False)
# item_icon.setBackground(QColor("#0D1225")) # item_icon.setBackground(QColor("#0D1225"))
# item_icon.setData(dict(item_type="Run", query_str=element['plate_number']), 1)
# group_item.setChild(j, 0, item_icon) # group_item.setChild(j, 0, item_icon)
for i in range(self.columnCount()): for i in range(self.columnCount()):
it = self.horizontalHeaderItem(i) it = self.horizontalHeaderItem(i)
try: try:
key = it.text().lower().replace(" ", "_") key = it.text().lower().replace(" ", "_")
except AttributeError: except AttributeError:
continue key = None
if not key: if not key:
continue continue
value = str(element[key]) value = str(element[key])
item = QStandardItem(value) item = QStandardItem(value)
item.setBackground(QColor("#CFE2F3")) item.setBackground(QColor("#CFE2F3"))
item.setEditable(False) item.setEditable(False)
# item_icon.setChild(j, i, item)
item.setData(dict(item_type=Run, query_str=element['plate_number']),1)
group_item.setChild(j, i, item) group_item.setChild(j, i, item)
# group_item.setChild(j, 1, QStandardItem("B")) # group_item.setChild(j, 1, QStandardItem("B"))
def get_value(self, idx: int, column: int = 1):
return self.item(idx, column)
def query_group_object(self, idx: int):
row_obj = self.get_value(idx)
logger.debug(row_obj.query_str)
return self.sql_object.query(name=row_obj.query_str, limit=1)

View File

@@ -1,5 +1,5 @@
""" """
Contains all run related frontend functions Contains all procedure related frontend functions
""" """
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
@@ -10,11 +10,11 @@ from .functions import select_open_file, select_save_file
import logging import logging
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.excel import SheetParser, InfoParser from backend.excel import ClientSubmissionParser, SampleParser
from backend.validators import PydSubmission, PydReagent from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample
from backend.db import ( from backend.db import (
Organization, SubmissionType, Reagent, ClientLab, SubmissionType, Reagent,
ReagentRole, KitTypeReagentRoleAssociation, BasicRun ReagentRole, KitTypeReagentRoleAssociation, Run
) )
from pprint import pformat from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop from .pop_ups import QuestionAsker, AlertPop
@@ -93,7 +93,7 @@ class SubmissionFormContainer(QWidget):
@report_result @report_result
def import_submission_function(self, fname: Path | None = None) -> Report: def import_submission_function(self, fname: Path | None = None) -> Report:
""" """
Import a new run to the app window Import a new procedure to the app window
Args: Args:
obj (QMainWindow): original app window obj (QMainWindow): original app window
@@ -110,7 +110,7 @@ class SubmissionFormContainer(QWidget):
self.form.setParent(None) self.form.setParent(None)
except AttributeError: except AttributeError:
pass pass
# NOTE: initialize samples # NOTE: initialize sample
self.samples = [] self.samples = []
self.missing_info = [] self.missing_info = []
# NOTE: set file dialog # NOTE: set file dialog
@@ -121,19 +121,28 @@ class SubmissionFormContainer(QWidget):
return report return report
# NOTE: create sheetparser using excel sheet and context from gui # NOTE: create sheetparser using excel sheet and context from gui
try: try:
# self.prsr = SheetParser(filepath=fname) self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
self.parser = InfoParser(filepath=fname)
except PermissionError: except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}") logger.error(f"Couldn't get permission to access file: {fname}")
return return
except AttributeError: except AttributeError:
self.parser = InfoParser(filepath=fname) self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
self.pyd = self.parser.to_pydantic() try:
# logger.debug(f"Samples: {pformat(self.pyd.samples)}") # self.prsr = SheetParser(filepath=fname)
checker = SampleChecker(self, "Sample Checker", self.pyd) self.sampleparser = SampleParser(filepath=fname)
except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}")
return
except AttributeError:
self.sampleparser = SampleParser(filepath=fname)
self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
self.pydsamples = self.sampleparser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
checker = SampleChecker(self, "Sample Checker", self.pydsamples)
if checker.exec(): if checker.exec():
# logger.debug(pformat(self.pyd.samples)) # logger.debug(pformat(self.pydclientsubmission.sample))
self.form = self.pyd.to_form(parent=self) self.form = self.pydclientsubmission.to_form(parent=self)
self.form.samples = self.pydsamples
self.layout().addWidget(self.form) self.layout().addWidget(self.form)
else: else:
message = "Submission cancelled." message = "Submission cancelled."
@@ -150,7 +159,7 @@ class SubmissionFormContainer(QWidget):
instance (Reagent | None): Blank reagent instance to be edited and then added. instance (Reagent | None): Blank reagent instance to be edited and then added.
Returns: Returns:
models.Reagent: the constructed reagent object to add to run models.Reagent: the constructed reagent object to add to procedure
""" """
report = Report() report = Report()
if not instance: if not instance:
@@ -167,23 +176,23 @@ class SubmissionFormContainer(QWidget):
class SubmissionFormWidget(QWidget): class SubmissionFormWidget(QWidget):
update_reagent_fields = ['extraction_kit'] update_reagent_fields = ['kittype']
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None: def __init__(self, parent: QWidget, pyd: PydSubmission, disable: list | None = None) -> None:
super().__init__(parent) super().__init__(parent)
if disable is None: if disable is None:
disable = [] disable = []
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.pyd = submission self.pyd = pyd
self.missing_info = [] self.missing_info = []
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value']) self.submissiontype = SubmissionType.query(name=self.pyd.submissiontype['value'])
basic_submission_class = self.submission_type.submission_class # basic_submission_class = self.submission_type.submission_class
logger.debug(f"Basic run class: {basic_submission_class}") # logger.debug(f"Basic procedure class: {basic_submission_class}")
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value']) defaults = Run.get_default_info("form_recover", "form_ignore", submissiontype=self.pyd.submissiontype['value'])
self.recover = defaults['form_recover'] self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore'] self.ignore = defaults['form_ignore']
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()): for k in list(self.pyd.model_fields.keys()):# + list(self.pyd.model_extra.keys()):
logger.debug(f"Pydantic field: {k}") logger.debug(f"Pydantic field: {k}")
if k in self.ignore: if k in self.ignore:
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget") logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
@@ -201,8 +210,8 @@ class SubmissionFormWidget(QWidget):
except KeyError: except KeyError:
value = dict(value=None, missing=True) value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}") logger.debug(f"Pydantic value: {value}")
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type, add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype,
run_object=basic_submission_class, disable=check) run_object=Run(), disable=check)
if add_widget is not None: if add_widget is not None:
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
if k in self.__class__.update_reagent_fields: if k in self.__class__.update_reagent_fields:
@@ -212,7 +221,7 @@ class SubmissionFormWidget(QWidget):
self.layout.addWidget(self.disabler) self.layout.addWidget(self.disabler)
self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents) self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
self.setStyleSheet(main_form_style) self.setStyleSheet(main_form_style)
# self.scrape_reagents(self.extraction_kit) # self.scrape_reagents(self.kittype)
self.setLayout(self.layout) self.setLayout(self.layout)
def disable_reagents(self): def disable_reagents(self):
@@ -223,7 +232,7 @@ class SubmissionFormWidget(QWidget):
reagent.flip_check(self.disabler.checkbox.isChecked()) reagent.flip_check(self.disabler.checkbox.isChecked())
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None, def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None,
extraction_kit: str | None = None, run_object: BasicRun | None = None, extraction_kit: str | None = None, run_object: Run | None = None,
disable: bool = False) -> "self.InfoItem": disable: bool = False) -> "self.InfoItem":
""" """
Make an InfoItem widget to hold a field Make an InfoItem widget to hold a field
@@ -256,14 +265,14 @@ class SubmissionFormWidget(QWidget):
return None return None
@report_result @report_result
def scrape_reagents(self, *args, **kwargs): #extraction_kit:str, caller:str|None=None): def scrape_reagents(self, *args, **kwargs): #kittype:str, caller:str|None=None):
""" """
Extracted scrape reagents function that will run when Extracted scrape reagents function that will procedure when
form 'extraction_kit' widget is updated. form 'kittype' widget is updated.
Args: Args:
obj (QMainWindow): updated main application obj (QMainWindow): updated main application
extraction_kit (str): name of extraction kit (in 'extraction_kit' widget) extraction_kit (str): name of extraction kittype (in 'kittype' widget)
Returns: Returns:
Tuple[QMainWindow, dict]: Updated application and result Tuple[QMainWindow, dict]: Updated application and result
@@ -373,7 +382,7 @@ class SubmissionFormWidget(QWidget):
return report return report
case _: case _:
pass pass
# NOTE: add reagents to run object # NOTE: add reagents to procedure object
if base_submission is None: if base_submission is None:
return return
for reagent in base_submission.reagents: for reagent in base_submission.reagents:
@@ -393,7 +402,7 @@ class SubmissionFormWidget(QWidget):
def export_csv_function(self, fname: Path | None = None): def export_csv_function(self, fname: Path | None = None):
""" """
Save the run's csv file. Save the procedure's csv file.
Args: Args:
fname (Path | None, optional): Input filename. Defaults to None. fname (Path | None, optional): Input filename. Defaults to None.
@@ -405,7 +414,7 @@ class SubmissionFormWidget(QWidget):
except PermissionError: except PermissionError:
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.") logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
except AttributeError: except AttributeError:
logger.error(f"No csv file found in the run at this point.") logger.error(f"No csv file found in the procedure at this point.")
def parse_form(self) -> Report: def parse_form(self) -> Report:
""" """
@@ -442,11 +451,10 @@ class SubmissionFormWidget(QWidget):
report.add_result(report) report.add_result(report)
return report return report
class InfoItem(QWidget): class InfoItem(QWidget):
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None, def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
run_object: BasicRun | None = None) -> None: run_object: Run | None = None) -> None:
super().__init__(parent) super().__init__(parent)
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -492,7 +500,7 @@ class SubmissionFormWidget(QWidget):
def set_widget(self, parent: QWidget, key: str, value: dict, def set_widget(self, parent: QWidget, key: str, value: dict,
submission_type: str | SubmissionType | None = None, submission_type: str | SubmissionType | None = None,
sub_obj: BasicRun | None = None) -> QWidget: sub_obj: Run | None = None) -> QWidget:
""" """
Creates form widget Creates form widget
@@ -515,16 +523,16 @@ class SubmissionFormWidget(QWidget):
pass pass
obj = parent.parent().parent() obj = parent.parent().parent()
match key: match key:
case 'submitting_lab': case 'clientlab':
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm ) # NOTE: lookup organizations suitable for clientlab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
labs = [item.name for item in Organization.query()] labs = [item.name for item in ClientLab.query()]
if isinstance(value, dict): if isinstance(value, dict):
value = value['value'] value = value['value']
if isinstance(value, Organization): if isinstance(value, ClientLab):
value = value.name value = value.name
try: try:
looked_up_lab = Organization.query(name=value, limit=1) looked_up_lab = ClientLab.query(name=value, limit=1)
except AttributeError: except AttributeError:
looked_up_lab = None looked_up_lab = None
if looked_up_lab: if looked_up_lab:
@@ -536,28 +544,28 @@ class SubmissionFormWidget(QWidget):
# NOTE: set combobox values to lookedup values # NOTE: set combobox values to lookedup values
add_widget.addItems(labs) add_widget.addItems(labs)
add_widget.setToolTip("Select submitting lab.") add_widget.setToolTip("Select submitting lab.")
case 'extraction_kit': case 'kittype':
# NOTE: if extraction kit not available, all other values fail # NOTE: if extraction kittype not available, all other values fail
if not check_not_nan(value): if not check_not_nan(value):
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", msg = AlertPop(message="Make sure to check your extraction kittype in the excel sheet!",
status="warning") status="warning")
msg.exec() msg.exec()
# NOTE: create combobox to hold looked up kits # NOTE: create combobox to hold looked up kits
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser # NOTE: lookup existing kits by 'proceduretype' decided on by sheetparser
uses = [item.name for item in submission_type.kit_types] uses = [item.name for item in submission_type.kit_types]
obj.uses = uses obj.uses = uses
if check_not_nan(value): if check_not_nan(value):
try: try:
uses.insert(0, uses.pop(uses.index(value))) uses.insert(0, uses.pop(uses.index(value)))
except ValueError: except ValueError:
logger.warning(f"Couldn't find kit in list, skipping move to top of list.") logger.warning(f"Couldn't find kittype in list, skipping move to top of list.")
obj.ext_kit = value obj.ext_kit = value
else: else:
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}") logger.error(f"Couldn't find {obj.prsr.sub['kittype']}")
obj.ext_kit = uses[0] obj.ext_kit = uses[0]
add_widget.addItems(uses) add_widget.addItems(uses)
add_widget.setToolTip("Select extraction kit.") add_widget.setToolTip("Select extraction kittype.")
parent.extraction_kit = add_widget.currentText() parent.extraction_kit = add_widget.currentText()
case 'submission_category': case 'submission_category':
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
@@ -568,7 +576,7 @@ class SubmissionFormWidget(QWidget):
except ValueError: except ValueError:
categories.insert(0, categories.pop(categories.index(submission_type))) categories.insert(0, categories.pop(categories.index(submission_type)))
add_widget.addItems(categories) add_widget.addItems(categories)
add_widget.setToolTip("Enter run category or select from list.") add_widget.setToolTip("Enter procedure category or select from list.")
case _: case _:
if key in sub_obj.timestamps: if key in sub_obj.timestamps:
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent) add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
@@ -692,10 +700,10 @@ class SubmissionFormWidget(QWidget):
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent) wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
return wanted_reagent, report return wanted_reagent, report
else: else:
# NOTE: In this case we will have an empty reagent and the run will fail kit integrity check # NOTE: In this case we will have an empty reagent and the procedure will fail kittype integrity check
return None, report return None, report
else: else:
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly. # NOTE: Since this now gets passed in directly from the clientsubmissionparser -> pydclientsubmission -> form and the clientsubmissionparser gets the name from the db, it should no longer be necessary to query the db with reagent/kittype, but with rt name directly.
rt = ReagentRole.query(name=self.reagent.role) rt = ReagentRole.query(name=self.reagent.role)
if rt is None: if rt is None:
rt = ReagentRole.query(kittype=self.extraction_kit, reagent=wanted_reagent) rt = ReagentRole.query(kittype=self.extraction_kit, reagent=wanted_reagent)
@@ -738,7 +746,7 @@ class SubmissionFormWidget(QWidget):
def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None: def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None:
super().__init__(scrollWidget=scrollWidget) super().__init__(scrollWidget=scrollWidget)
self.setEditable(True) self.setEditable(True)
looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.role, looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.equipmentrole,
kittype=extraction_kit) kittype=extraction_kit)
relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()] relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()]
# NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows # NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows
@@ -754,7 +762,8 @@ class SubmissionFormWidget(QWidget):
looked_up_reg = None looked_up_reg = None
if looked_up_reg: if looked_up_reg:
try: try:
relevant_reagents.insert(0, relevant_reagents.pop(relevant_reagents.index(looked_up_reg.lot))) relevant_reagents.insert(0, relevant_reagents.pop(
relevant_reagents.index(looked_up_reg.lot)))
except ValueError as e: except ValueError as e:
logger.error(f"Error reordering relevant reagents: {e}") logger.error(f"Error reordering relevant reagents: {e}")
else: else:
@@ -764,9 +773,9 @@ class SubmissionFormWidget(QWidget):
relevant_reagents.insert(0, moved_reag) relevant_reagents.insert(0, moved_reag)
else: else:
pass pass
self.setObjectName(f"lot_{reagent.role}") self.setObjectName(f"lot_{reagent.equipmentrole}")
self.addItems(relevant_reagents) self.addItems(relevant_reagents)
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}") self.setToolTip(f"Enter lot number for the reagent used for {reagent.equipmentrole}")
class DisableReagents(QWidget): class DisableReagents(QWidget):
@@ -783,16 +792,22 @@ class SubmissionFormWidget(QWidget):
class ClientSubmissionFormWidget(SubmissionFormWidget): class ClientSubmissionFormWidget(SubmissionFormWidget):
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None: def __init__(self, parent: QWidget, clientsubmission: PydClientSubmission, samples: List = [],
super().__init__(parent, submission=submission, disable=disable) disable: list | None = None) -> None:
super().__init__(parent, pyd=clientsubmission, disable=disable)
try:
self.disabler.setHidden(True) self.disabler.setHidden(True)
except AttributeError:
pass
# save_btn = QPushButton("Save") # save_btn = QPushButton("Save")
self.samples = samples
logger.debug(f"Samples: {self.samples}")
start_run_btn = QPushButton("Save") start_run_btn = QPushButton("Save")
# self.layout.addWidget(save_btn) # self.layout.addWidget(save_btn)
self.layout.addWidget(start_run_btn) self.layout.addWidget(start_run_btn)
start_run_btn.clicked.connect(self.create_new_submission) start_run_btn.clicked.connect(self.create_new_submission)
@report_result
def parse_form(self) -> Report: def parse_form(self) -> Report:
""" """
Transforms form info into PydSubmission Transforms form info into PydSubmission
@@ -801,7 +816,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
Report: Report on status of parse. Report: Report on status of parse.
""" """
report = Report() report = Report()
logger.info(f"Hello from client run form parser!") logger.info(f"Hello from client procedure form parser!")
info = {} info = {}
reagents = [] reagents = []
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):
@@ -827,18 +842,20 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
report.add_result(report) report.add_result(report)
return report return report
@report_result # @report_result
def to_pydantic(self, *args): def to_pydantic(self, *args):
self.parse_form() self.parse_form()
return self.pyd return self.pyd
@report_result
def create_new_submission(self, *args) -> Report: def create_new_submission(self, *args) -> Report:
self.parse_form() pyd = self.to_pydantic()
sql = self.pyd.to_sql() sql = pyd.to_sql()
for sample in self.samples:
if isinstance(sample, PydSample):
sample = sample.to_sql()
sql.add_sample(sample=sample)
logger.debug(sql.__dict__) logger.debug(sql.__dict__)
sql.save() sql.save()
self.app.table_widget.sub_wid.set_data() self.app.table_widget.sub_wid.set_data()
self.setParent(None) self.setParent(None)

View File

@@ -3,7 +3,7 @@ Pane to hold information e.g. cost summary.
""" """
from .info_tab import InfoPane from .info_tab import InfoPane
from PyQt6.QtWidgets import QWidget, QLabel, QPushButton from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
from backend.db import Organization from backend.db import ClientLab
from backend.excel import ReportMaker from backend.excel import ReportMaker
from .misc import CheckableComboBox from .misc import CheckableComboBox
import logging import logging
@@ -24,7 +24,7 @@ class Summary(InfoPane):
self.org_select = CheckableComboBox() self.org_select = CheckableComboBox()
self.org_select.setEditable(False) self.org_select.setEditable(False)
self.org_select.addItem("Select", header=True) self.org_select.addItem("Select", header=True)
for org in [org.name for org in Organization.query()]: for org in [org.name for org in ClientLab.query()]:
self.org_select.addItem(org) self.org_select.addItem(org)
self.org_select.model().itemChanged.connect(self.update_data) self.org_select.model().itemChanged.connect(self.update_data)
self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1) self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1)

View File

@@ -9,15 +9,22 @@
{% block body %} {% block body %}
<h2><u>Sample Checker</u></h2> <h2><u>Sample Checker</u></h2>
<br> <br>
{% if rsl_plate_num %}
<label for="rsl_plate_num">RSL Plate Number:</label><br>
<input type="text" id="rsl_plate_num" name="sample_id" value="{{ rsl_plate_num }}" size="40">
{% endif %}
<br>
<p>Take a moment to verify sample names.</p> <p>Take a moment to verify sample names.</p>
<br> <br>
<form> <form>
&emsp;&emsp;Submitter ID&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Row&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; Column<br/> &emsp;&emsp;Submitter ID<br/><!--&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Row&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; Column<br/>-->
{% for sample in samples %} {% for sample in samples %}
{% if rsl_plate_num %}<input type="checkbox" id="{{ sample['submission_rank'] }}_enabled" name="vehicle1" value="Bike" {% if sample['enabled'] %}checked{% endif %}>{% endif %}
{{ '%02d' % sample['submission_rank'] }} {{ '%02d' % sample['submission_rank'] }}
<input type="text" id="{{ sample['submission_rank'] }}_id" name="submitter_id" value="{{ sample['submitter_id'] }}" size="40" style="color:{{ sample['color'] }};">> <input type="text" id="{{ sample['submission_rank'] }}_id" name="sample_id" value="{{ sample['sample_id'] }}" size="40" style="color:{{ sample['color'] }};" {% if rsl_plate_num %}disabled{% endif %}>
<input type="number" id="{{ sample['submission_rank'] }}_row" name="row" value="{{ sample['row'] }}" size="5", min="1"> <!-- <input type="number" id="{{ sample['submission_rank'] }}_row" name="row" value="{{ sample['row'] }}" size="5", min="1">-->
<input type="number" id="{{ sample['submission_rank'] }}_col" name="column" value="{{ sample['column'] }}" size="5", min="1"> <!-- <input type="number" id="{{ sample['submission_rank'] }}_col" name="column" value="{{ sample['column'] }}" size="5", min="1">-->
<br/> <br/>
{% endfor %} {% endfor %}
</form> </form>
@@ -30,15 +37,23 @@
document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){ document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value); backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
}); });
document.getElementById("{{ sample['submission_rank'] }}_row").addEventListener("input", function(){ {% if rsl_plate_num %}
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value); document.getElementById("{{ sample['submission_rank'] }}_enabled").addEventListener("input", function(){
}); backend.enable_sample("{{ sample['submission_rank'] }}", this.checked);
document.getElementById("{{ sample['submission_rank'] }}_column").addEventListener("input", function(){
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
}); });
{% endif %}
<!-- document.getElementById("{{ sample['submission_rank'] }}_row").addEventListener("input", function(){-->
<!-- backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);-->
<!-- });-->
<!-- document.getElementById("{{ sample['submission_rank'] }}_column").addEventListener("input", function(){-->
<!-- backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);-->
<!-- });-->
{% endfor %} {% endfor %}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
}, false); }, false);
document.getElementById("rsl_plate_num").addEventListener("input", function(){
backend.set_rsl_plate_num(this.value);
});
{% endblock %} {% endblock %}
</script> </script>

View File

@@ -30,7 +30,7 @@ from functools import wraps
timezone = tz("America/Winnipeg") timezone = tz("America/Winnipeg")
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"procedure.{__name__}")
logger.info(f"Package dir: {project_path}") logger.info(f"Package dir: {project_path}")
@@ -41,7 +41,7 @@ else:
os_config_dir = ".config" os_config_dir = ".config"
logger.info(f"Got platform {platform.system()}, config_dir: {os_config_dir}") logger.info(f"Got platform {platform.system()}, config_dir: {os_config_dir}")
main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions") main_aux_dir = Path.home().joinpath(f"{os_config_dir}/procedure")
CONFIGDIR = main_aux_dir.joinpath("config") CONFIGDIR = main_aux_dir.joinpath("config")
LOGDIR = main_aux_dir.joinpath("logs") LOGDIR = main_aux_dir.joinpath("logs")
@@ -343,7 +343,7 @@ class StreamToLogger(object):
class CustomLogger(Logger): class CustomLogger(Logger):
def __init__(self, name: str = "submissions", level=logging.DEBUG): def __init__(self, name: str = "procedure", level=logging.DEBUG):
super().__init__(name, level) super().__init__(name, level)
self.extra_info = None self.extra_info = None
ch = logging.StreamHandler(stream=sys.stdout) ch = logging.StreamHandler(stream=sys.stdout)
@@ -394,7 +394,7 @@ def setup_logger(verbosity: int = 3):
return return
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
logger = logging.getLogger("submissions") logger = logging.getLogger("procedure")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
# NOTE: create file handler which logs even debug messages # NOTE: create file handler which logs even debug messages
try: try:
@@ -937,7 +937,7 @@ class Settings(BaseSettings, extra="allow"):
else: else:
os_config_dir = ".config" os_config_dir = ".config"
# logger.info(f"Got platform {platform.system()}, config_dir: {os_config_dir}") # logger.info(f"Got platform {platform.system()}, config_dir: {os_config_dir}")
return Path.home().joinpath(f"{os_config_dir}/submissions") return Path.home().joinpath(f"{os_config_dir}/procedure")
@classproperty @classproperty
def configdir(cls): def configdir(cls):
@@ -955,12 +955,12 @@ class Settings(BaseSettings, extra="allow"):
else: else:
settings_path = None settings_path = None
if settings_path is None: if settings_path is None:
# NOTE: Check user .config/submissions directory # NOTE: Check user .config/procedure directory
if cls.configdir.joinpath("config.yml").exists(): if cls.configdir.joinpath("config.yml").exists():
settings_path = cls.configdir.joinpath("config.yml") settings_path = cls.configdir.joinpath("config.yml")
# NOTE: Check user .submissions directory # NOTE: Check user .procedure directory
elif Path.home().joinpath(".submissions", "config.yml").exists(): elif Path.home().joinpath(".procedure", "config.yml").exists():
settings_path = Path.home().joinpath(".submissions", "config.yml") settings_path = Path.home().joinpath(".procedure", "config.yml")
# NOTE: finally look in the local config # NOTE: finally look in the local config
else: else:
if check_if_app(): if check_if_app():
@@ -1275,7 +1275,7 @@ class Settings(BaseSettings, extra="allow"):
logger.warning(f"Logging directory {self.configdir} already exists.") logger.warning(f"Logging directory {self.configdir} already exists.")
dicto = {} dicto = {}
for k, v in self.__dict__.items(): for k, v in self.__dict__.items():
if k in ['package', 'database_session', 'submission_types']: if k in ['package', 'database_session', 'proceduretype']:
continue continue
match v: match v:
case Path(): case Path():