Context menu for runs working.
This commit is contained in:
@@ -34,7 +34,7 @@ templates_path = ['_templates']
|
||||
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/submissions").__str__())
|
||||
sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src/procedure").__str__())
|
||||
|
||||
# -- 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']
|
||||
|
||||
|
||||
# autodoc_mock_imports = ["backend.db.models.submissions"]
|
||||
# autodoc_mock_imports = ["backend.db.models.procedure"]
|
||||
@@ -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.
|
||||
ex: {"goodbye": null, "backup_database": null}
|
||||
The program will overwrite null with the actual function upon startup.
|
||||
|
||||
@@ -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.
|
||||
ex: {"hello": null, "import_irida": null}
|
||||
The program will overwrite null with the actual function upon startup.
|
||||
|
||||
@@ -5,29 +5,29 @@ from tools import Settings
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
def import_irida(ctx: Settings):
|
||||
"""
|
||||
Grabs Irida controls from secondary database.
|
||||
Grabs Irida control from secondary database.
|
||||
|
||||
Args:
|
||||
ctx (Settings): Settings inherited from app.
|
||||
"""
|
||||
from backend import BasicSample
|
||||
from backend import Sample
|
||||
from backend.db import IridaControl, ControlType
|
||||
# NOTE: Because the main session will be busy in another thread, this requires a new session.
|
||||
new_session = Session(ctx.database_session.get_bind())
|
||||
ct = new_session.query(ControlType).filter(ControlType.name == "Irida Control").first()
|
||||
existing_controls = [item.name for item in new_session.query(IridaControl)]
|
||||
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:
|
||||
conn = sqlite3.connect(ctrl_db_path)
|
||||
except AttributeError as e:
|
||||
logger.error(f"Error, could not import from irida due to {e}")
|
||||
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 " \
|
||||
f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})"
|
||||
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")
|
||||
assert isinstance(record['submitted_date'], datetime)
|
||||
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:
|
||||
instance.sample = sample
|
||||
try:
|
||||
instance.submission = sample.submissions[0]
|
||||
instance.clientsubmission = sample.procedure[0]
|
||||
except IndexError:
|
||||
logger.error(f"Could not get sample for {sample}")
|
||||
instance.submission = None
|
||||
# instance.run = sample.run[0]
|
||||
instance.clientsubmission = None
|
||||
# instance.procedure = sample.procedure[0]
|
||||
new_session.add(instance)
|
||||
new_session.commit()
|
||||
new_session.close()
|
||||
|
||||
@@ -22,7 +22,7 @@ def get_week_of_month() -> int:
|
||||
|
||||
|
||||
# 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"
|
||||
__author__ = {"name": "Landon Wark", "email": "Landon.Wark@phac-aspc.gc.ca"}
|
||||
__copyright__ = f"2022-{year}, Government of Canada"
|
||||
|
||||
@@ -55,7 +55,7 @@ def update_log(mapper, connection, target):
|
||||
continue
|
||||
added = [str(item) for item in hist.added]
|
||||
# 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',
|
||||
# 'gel_controls', 'source_plates']:
|
||||
if attr.key in LogMixin.tracking_exclusion:
|
||||
|
||||
@@ -3,6 +3,8 @@ Contains all models for sqlalchemy
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, logging
|
||||
|
||||
from dateutil.parser import parse
|
||||
from pandas import DataFrame
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Column, INTEGER, String, JSON
|
||||
@@ -21,7 +23,7 @@ if 'pytest' in sys.modules:
|
||||
# NOTE: For inheriting in LogMixin
|
||||
Base: DeclarativeMeta = declarative_base()
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
|
||||
class BaseClass(Base):
|
||||
@@ -33,12 +35,12 @@ class BaseClass(Base):
|
||||
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
|
||||
|
||||
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_inheritable = []
|
||||
searchables = []
|
||||
|
||||
misc_info = Column(JSON)
|
||||
_misc_info = Column(JSON)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
@@ -122,6 +124,10 @@ class BaseClass(Base):
|
||||
from test_settings import ctx
|
||||
return ctx.backup_path
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._misc_info = dict()
|
||||
|
||||
@classproperty
|
||||
def jsons(cls) -> List[str]:
|
||||
"""
|
||||
@@ -130,7 +136,10 @@ class BaseClass(Base):
|
||||
Returns:
|
||||
List[str]: List of column names
|
||||
"""
|
||||
return [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
|
||||
try:
|
||||
return [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
@classproperty
|
||||
def timestamps(cls) -> List[str]:
|
||||
@@ -140,7 +149,10 @@ class BaseClass(Base):
|
||||
Returns:
|
||||
List[str]: List of column names
|
||||
"""
|
||||
return [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
|
||||
try:
|
||||
return [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_default_info(cls, *args) -> dict | list | str:
|
||||
@@ -155,7 +167,7 @@ class BaseClass(Base):
|
||||
return dict(singles=singles)
|
||||
|
||||
@classmethod
|
||||
def find_regular_subclass(cls, name: str|None = None) -> Any:
|
||||
def find_regular_subclass(cls, name: str | None = None) -> Any:
|
||||
"""
|
||||
Args:
|
||||
name (str): name of subclass of interest.
|
||||
@@ -198,11 +210,11 @@ class BaseClass(Base):
|
||||
@classmethod
|
||||
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:
|
||||
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:
|
||||
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]
|
||||
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
|
||||
def query(cls, **kwargs) -> Any | List[Any]:
|
||||
"""
|
||||
@@ -227,6 +257,8 @@ class BaseClass(Base):
|
||||
Returns:
|
||||
Any | List[Any]: Result of query execution.
|
||||
"""
|
||||
if "name" in kwargs.keys():
|
||||
kwargs['limit'] = 1
|
||||
return cls.execute_query(**kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -243,16 +275,17 @@ class BaseClass(Base):
|
||||
Any | List[Any]: Single result if limit = 1 or List if other.
|
||||
"""
|
||||
# logger.debug(f"Kwargs: {kwargs}")
|
||||
if model is None:
|
||||
model = cls
|
||||
# if model is None:
|
||||
# model = cls
|
||||
# logger.debug(f"Model: {model}")
|
||||
if query is None:
|
||||
query: Query = cls.__database_session__.query(model)
|
||||
singles = model.get_default_info('singles')
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
singles = cls.get_default_info('singles')
|
||||
for k, v in kwargs.items():
|
||||
|
||||
logger.info(f"Using key: {k} with value: {v}")
|
||||
try:
|
||||
attr = getattr(model, k)
|
||||
attr = getattr(cls, k)
|
||||
# NOTE: account for attrs that use list.
|
||||
if attr.property.uselist:
|
||||
query = query.filter(attr.contains(v))
|
||||
@@ -341,6 +374,26 @@ class BaseClass(Base):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
# 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:
|
||||
field_type = getattr(self.__class__, key)
|
||||
except AttributeError:
|
||||
return super().__setattr__(key, value)
|
||||
if isinstance(field_type, InstrumentedAttribute):
|
||||
logger.debug(f"{key} is an InstrumentedAttribute.")
|
||||
# logger.debug(f"{key} is an InstrumentedAttribute.")
|
||||
match field_type.property:
|
||||
case ColumnProperty():
|
||||
logger.debug(f"Setting ColumnProperty to {value}")
|
||||
# logger.debug(f"Setting ColumnProperty to {value}")
|
||||
return super().__setattr__(key, value)
|
||||
case _RelationshipDeclared():
|
||||
logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}")
|
||||
@@ -446,10 +513,13 @@ class BaseClass(Base):
|
||||
try:
|
||||
return super().__setattr__(key, value)
|
||||
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
|
||||
value = relationship_class.query(name=value)
|
||||
return super().__setattr__(key, value)
|
||||
try:
|
||||
return super().__setattr__(key, value)
|
||||
except AttributeError:
|
||||
return super().__setattr__(key, None)
|
||||
case _:
|
||||
return super().__setattr__(key, value)
|
||||
else:
|
||||
@@ -458,7 +528,7 @@ class BaseClass(Base):
|
||||
def delete(self):
|
||||
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
|
||||
|
||||
@@ -486,8 +556,7 @@ class BaseClass(Base):
|
||||
|
||||
|
||||
class LogMixin(Base):
|
||||
|
||||
tracking_exclusion: ClassVar = ['artic_technician', 'submission_sample_associations',
|
||||
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',
|
||||
'submission_reagent_associations', 'submission_equipment_associations',
|
||||
'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls',
|
||||
'source_plates']
|
||||
@@ -540,13 +609,12 @@ class ConfigItem(BaseClass):
|
||||
|
||||
|
||||
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 .runs import *
|
||||
from .kits import *
|
||||
from .submissions import *
|
||||
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
|
||||
Procedure.reagents.creator = lambda reg: ProcedureReagentAssociation(reagent=reg)
|
||||
# Procedure.reagents.creator = lambda reg: ProcedureReagentAssociation(reagent=reg)
|
||||
|
||||
@@ -27,7 +27,7 @@ class ControlType(BaseClass):
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(255), unique=True) #: controltype name (e.g. Irida Control)
|
||||
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
|
||||
@setup_lookup
|
||||
@@ -59,16 +59,16 @@ class ControlType(BaseClass):
|
||||
Get subtypes associated with this controltype (currently used only for Kraken)
|
||||
|
||||
Args:
|
||||
mode (str): analysis mode sub_type
|
||||
mode (str): analysis mode submissiontype
|
||||
|
||||
Returns:
|
||||
List[str]: list of subtypes available
|
||||
"""
|
||||
if not self.controls:
|
||||
if not self.control:
|
||||
return
|
||||
# NOTE: Get first instance since all should have same subtypes
|
||||
# NOTE: Get mode of instance
|
||||
jsoner = getattr(self.controls[0], mode)
|
||||
jsoner = getattr(self.control[0], mode)
|
||||
try:
|
||||
# NOTE: Pick genera (all should have same subtypes)
|
||||
genera = list(jsoner.keys())[0]
|
||||
@@ -79,7 +79,7 @@ class ControlType(BaseClass):
|
||||
return subtypes
|
||||
|
||||
@property
|
||||
def instance_class(self) -> Control:
|
||||
def control_class(self) -> Control:
|
||||
"""
|
||||
Retrieves the Control class associated with this controltype
|
||||
|
||||
@@ -119,27 +119,27 @@ class Control(BaseClass):
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
controltype_name = Column(String, ForeignKey("_controltype.name", ondelete="SET NULL",
|
||||
name="fk_BC_subtype_name")) #: name of joined run type
|
||||
controltype = relationship("ControlType", back_populates="controls",
|
||||
name="fk_BC_subtype_name")) #: name of joined procedure type
|
||||
controltype = relationship("ControlType", back_populates="control",
|
||||
foreign_keys=[controltype_name]) #: reference to parent control type
|
||||
name = Column(String(255), unique=True) #: Sample ID
|
||||
sample_id = Column(String, ForeignKey("_basicsample.id", ondelete="SET NULL",
|
||||
name="fk_Cont_sample_id")) #: name of joined run type
|
||||
sample = relationship("BasicSample", back_populates="control") #: This control's run sample
|
||||
sample_id = Column(String, ForeignKey("_sample.id", ondelete="SET NULL",
|
||||
name="fk_Cont_sample_id")) #: name of joined procedure type
|
||||
sample = relationship("Sample", back_populates="control") #: This control's procedure sample
|
||||
submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics
|
||||
procedure_id = Column(INTEGER, ForeignKey("_procedure.id")) #: parent run id
|
||||
procedure = relationship("Procedure", back_populates="controls",
|
||||
foreign_keys=[procedure_id]) #: parent run
|
||||
procedure_id = Column(INTEGER, ForeignKey("_procedure.id")) #: parent procedure id
|
||||
procedure = relationship("Procedure", back_populates="control",
|
||||
foreign_keys=[procedure_id]) #: parent procedure
|
||||
|
||||
__mapper_args__ = {
|
||||
"polymorphic_identity": "Basic Control",
|
||||
"polymorphic_on": case(
|
||||
(controltype_name == "PCR Control", "PCR Control"),
|
||||
(controltype_name == "Irida Control", "Irida Control"),
|
||||
else_="Basic Control"
|
||||
),
|
||||
"with_polymorphic": "*",
|
||||
}
|
||||
# __mapper_args__ = {
|
||||
# "polymorphic_identity": "Basic Control",
|
||||
# "polymorphic_on": case(
|
||||
# (controltype_name == "PCR Control", "PCR Control"),
|
||||
# (controltype_name == "Irida Control", "Irida Control"),
|
||||
# else_="Basic Control"
|
||||
# ),
|
||||
# "with_polymorphic": "*",
|
||||
# }
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.controltype_name}({self.name})>"
|
||||
@@ -284,448 +284,448 @@ class Control(BaseClass):
|
||||
self.__database_session__.commit()
|
||||
|
||||
|
||||
class PCRControl(Control):
|
||||
"""
|
||||
Class made to hold info from Design & Analysis software.
|
||||
"""
|
||||
|
||||
id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
|
||||
subtype = Column(String(16)) #: PC or NC
|
||||
target = Column(String(16)) #: N1, N2, etc.
|
||||
ct = Column(FLOAT) #: PCR result
|
||||
reagent_lot = Column(String(64), ForeignKey("_reagent.lot", ondelete="SET NULL",
|
||||
name="fk_reagent_lot"))
|
||||
reagent = relationship("Reagent", foreign_keys=reagent_lot) #: reagent used for this control
|
||||
|
||||
__mapper_args__ = dict(polymorphic_identity="PCR Control",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == Control.id))
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
"""
|
||||
Creates dictionary of fields for this object.
|
||||
|
||||
Returns:
|
||||
dict: Output dict of name, ct, subtype, target, reagent_lot and submitted_date
|
||||
"""
|
||||
return dict(
|
||||
name=self.name,
|
||||
ct=self.ct,
|
||||
subtype=self.subtype,
|
||||
target=self.target,
|
||||
reagent_lot=self.reagent_lot,
|
||||
submitted_date=self.submitted_date.date()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@report_result
|
||||
def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
|
||||
"""
|
||||
Creates a PCRFigure. Overrides parent
|
||||
|
||||
Args:
|
||||
parent (__type__): Widget to contain the chart.
|
||||
chart_settings (dict): settings passed down from chart widget
|
||||
ctx (Settings): settings passed down from gui. Not used here.
|
||||
|
||||
Returns:
|
||||
Tuple[Report, "PCRFigure"]: Report of status and resulting figure.
|
||||
"""
|
||||
from frontend.visualizations.pcr_charts import PCRFigure
|
||||
parent.mode_typer.clear()
|
||||
parent.mode_typer.setEnabled(False)
|
||||
report = Report()
|
||||
controls = cls.query(proceduretype=chart_settings['sub_type'], start_date=chart_settings['start_date'],
|
||||
end_date=chart_settings['end_date'])
|
||||
data = [control.to_sub_dict() for control in controls]
|
||||
df = DataFrame.from_records(data)
|
||||
# NOTE: Get all PCR controls with ct over 0
|
||||
try:
|
||||
df = df[df.ct > 0.0]
|
||||
except AttributeError:
|
||||
df = df
|
||||
fig = PCRFigure(df=df, modes=[], settings=chart_settings)
|
||||
return report, fig
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydPCRControl
|
||||
return PydPCRControl(**self.to_sub_dict(),
|
||||
controltype_name=self.controltype_name,
|
||||
submission_id=self.submission_id)
|
||||
|
||||
|
||||
class IridaControl(Control):
|
||||
subtyping_allowed = ['kraken']
|
||||
|
||||
id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
|
||||
contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism
|
||||
matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
|
||||
kraken = Column(JSON) #: unstructured output from kraken_report
|
||||
subtype = Column(String(16), nullable=False) #: EN-NOS, MCS-NOS, etc
|
||||
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_db_version = Column(String(32)) #: folder name of kraken2 db
|
||||
sample_id = Column(INTEGER,
|
||||
ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
|
||||
|
||||
__mapper_args__ = dict(polymorphic_identity="Irida Control",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == Control.id))
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
if self.controltype.targets:
|
||||
return list(itertools.chain.from_iterable([value for key, value in self.controltype.targets.items()
|
||||
if key == self.subtype]))
|
||||
else:
|
||||
return ["None"]
|
||||
|
||||
@validates("subtype")
|
||||
def enforce_subtype_literals(self, key: str, value: str) -> str:
|
||||
"""
|
||||
Validates sub_type field with acceptable values
|
||||
|
||||
Args:
|
||||
key (str): Field name
|
||||
value (str): Field Value
|
||||
|
||||
Raises:
|
||||
KeyError: Raised if value is not in the acceptable list.
|
||||
|
||||
Returns:
|
||||
str: Validated string.
|
||||
"""
|
||||
acceptables = ['ATCC49226', 'ATCC49619', 'EN-NOS', "EN-SSTI", "MCS-NOS", "MCS-SSTI", "SN-NOS", "SN-SSTI"]
|
||||
if value.upper() not in acceptables:
|
||||
raise KeyError(f"Sub-type must be in {acceptables}")
|
||||
return value
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
"""
|
||||
Converts object into convenient dictionary for use in run summary
|
||||
|
||||
Returns:
|
||||
dict: output dictionary containing: Name, Type, Targets, Top Kraken results
|
||||
"""
|
||||
try:
|
||||
kraken = self.kraken
|
||||
except TypeError:
|
||||
kraken = {}
|
||||
try:
|
||||
kraken_cnt_total = sum([item['kraken_count'] for item in kraken.values()])
|
||||
except AttributeError:
|
||||
kraken_cnt_total = 0
|
||||
try:
|
||||
new_kraken = [dict(name=key, kraken_count=value['kraken_count'],
|
||||
kraken_percent=f"{value['kraken_count'] / kraken_cnt_total:0.2%}",
|
||||
target=key in self.controltype.targets)
|
||||
for key, value in kraken.items()]
|
||||
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)[0:10]
|
||||
except (AttributeError, ZeroDivisionError):
|
||||
new_kraken = []
|
||||
output = dict(
|
||||
name=self.name,
|
||||
type=self.controltype.name,
|
||||
targets=", ".join(self.targets),
|
||||
kraken=new_kraken
|
||||
)
|
||||
return output
|
||||
|
||||
def convert_by_mode(self, control_sub_type: str, mode: Literal['kraken', 'matches', 'contains'],
|
||||
consolidate: bool = False) -> Generator[dict, None, None]:
|
||||
"""
|
||||
split this instance into analysis types ('kraken', 'matches', 'contains') for controls graphs
|
||||
|
||||
Args:
|
||||
consolidate (bool): whether to merge all off-target genera. Defaults to False
|
||||
control_sub_type (str): control subtype, 'MCS-NOS', etc.
|
||||
mode (Literal['kraken', 'matches', 'contains']): analysis type, 'contains', etc.
|
||||
|
||||
Returns:
|
||||
List[dict]: list of records
|
||||
"""
|
||||
try:
|
||||
data = self.__getattribute__(mode)
|
||||
except TypeError:
|
||||
data = {}
|
||||
if data is None:
|
||||
data = {}
|
||||
# NOTE: Data truncation and consolidation.
|
||||
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]}
|
||||
else:
|
||||
if consolidate:
|
||||
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
|
||||
k.strip("*") not in self.controltype.targets[control_sub_type])
|
||||
on_tar['Off-target'] = {f"{mode}_ratio": off_tar}
|
||||
data = on_tar
|
||||
for genus in data:
|
||||
_dict = dict(
|
||||
name=self.name,
|
||||
submitted_date=self.submitted_date,
|
||||
genus=genus,
|
||||
target='Target' if genus.strip("*") in self.controltype.targets[control_sub_type] else "Off-target"
|
||||
)
|
||||
for key in data[genus]:
|
||||
_dict[key] = data[genus][key]
|
||||
yield _dict
|
||||
|
||||
@classproperty
|
||||
def modes(cls) -> List[str]:
|
||||
"""
|
||||
Get all control modes from database
|
||||
|
||||
Returns:
|
||||
List[str]: List of control mode names.
|
||||
"""
|
||||
try:
|
||||
cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
|
||||
except AttributeError as e:
|
||||
logger.error(f"Failed to get available modes from db: {e}")
|
||||
cols = []
|
||||
return cols
|
||||
|
||||
@classmethod
|
||||
def make_parent_buttons(cls, parent: QWidget) -> None:
|
||||
"""
|
||||
Creates buttons for controlling
|
||||
|
||||
Args:
|
||||
parent (QWidget): chart holding widget to add buttons to.
|
||||
|
||||
"""
|
||||
super().make_parent_buttons(parent=parent)
|
||||
rows = parent.layout.rowCount() - 2
|
||||
# NOTE: check box for consolidating off-target items
|
||||
checker = QCheckBox(parent)
|
||||
checker.setChecked(True)
|
||||
checker.setObjectName("irida_check")
|
||||
checker.setToolTip("Pools off-target genera to save time.")
|
||||
parent.layout.addWidget(QLabel("Consolidate Off-targets"), rows, 0, 1, 1)
|
||||
parent.layout.addWidget(checker, rows, 1, 1, 2)
|
||||
checker.checkStateChanged.connect(parent.update_data)
|
||||
|
||||
@classmethod
|
||||
@report_result
|
||||
def make_chart(cls, chart_settings: dict, parent, ctx) -> Tuple[Report, "IridaFigure" | None]:
|
||||
"""
|
||||
Creates a IridaFigure. Overrides parent
|
||||
|
||||
Args:
|
||||
parent (__type__): Widget to contain the chart.
|
||||
chart_settings (dict): settings passed down from chart widget
|
||||
ctx (Settings): settings passed down from gui.
|
||||
|
||||
Returns:
|
||||
Tuple[Report, "IridaFigure"]: Report of status and resulting figure.
|
||||
"""
|
||||
from frontend.visualizations import IridaFigure
|
||||
try:
|
||||
checker = parent.findChild(QCheckBox, name="irida_check")
|
||||
if chart_settings['mode'] == "kraken":
|
||||
checker.setEnabled(False)
|
||||
checker.setChecked(False)
|
||||
else:
|
||||
checker.setEnabled(True)
|
||||
consolidate = checker.isChecked()
|
||||
except AttributeError:
|
||||
consolidate = False
|
||||
report = Report()
|
||||
controls = cls.query(subtype=chart_settings['sub_type'], start_date=chart_settings['start_date'],
|
||||
end_date=chart_settings['end_date'])
|
||||
if not controls:
|
||||
report.add_result(Result(status="Critical", msg="No controls found in given date range."))
|
||||
return report, None
|
||||
# NOTE: change each control to list of dictionaries
|
||||
data = [control.convert_by_mode(control_sub_type=chart_settings['sub_type'], mode=chart_settings['mode'],
|
||||
consolidate=consolidate) for
|
||||
control in controls]
|
||||
# NOTE: flatten data to one dimensional list
|
||||
# data = [item for sublist in data for item in sublist]
|
||||
data = flatten_list(data)
|
||||
if not data:
|
||||
report.add_result(Result(status="Critical", msg="No data found for controls in given date range."))
|
||||
return report, None
|
||||
df = cls.convert_data_list_to_df(input_df=data, sub_mode=chart_settings['sub_mode'])
|
||||
if chart_settings['sub_mode'] is None:
|
||||
title = chart_settings['sub_mode']
|
||||
else:
|
||||
title = f"{chart_settings['mode']} - {chart_settings['sub_mode']}"
|
||||
# NOTE: send dataframe to chart maker
|
||||
df, modes = cls.prep_df(ctx=ctx, df=df)
|
||||
fig = IridaFigure(df=df, ytitle=title, modes=modes, parent=parent,
|
||||
settings=chart_settings)
|
||||
return report, fig
|
||||
|
||||
@classmethod
|
||||
def convert_data_list_to_df(cls, input_df: list[dict], sub_mode) -> DataFrame:
|
||||
"""
|
||||
Convert list of control records to dataframe
|
||||
|
||||
Args:
|
||||
input_df (list[dict]): list of dictionaries containing records
|
||||
sub_mode (str | None, optional): sub_type of run type. Defaults to None.
|
||||
|
||||
Returns:
|
||||
DataFrame: dataframe of controls
|
||||
"""
|
||||
df = DataFrame.from_records(input_df)
|
||||
safe = ['name', 'submitted_date', 'genus', 'target']
|
||||
for column in df.columns:
|
||||
if column not in safe:
|
||||
if sub_mode is not None and column != sub_mode:
|
||||
continue
|
||||
else:
|
||||
safe.append(column)
|
||||
if "percent" in column:
|
||||
try:
|
||||
count_col = next(item for item in df.columns if "count" in item)
|
||||
except StopIteration:
|
||||
continue
|
||||
# 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 = df[[c for c in df.columns if c in safe]]
|
||||
# NOTE: move date of sample submitted on same date as previous ahead one.
|
||||
df = cls.displace_date(df=df)
|
||||
# NOTE: ad hoc method to make data labels more accurate.
|
||||
df = cls.df_column_renamer(df=df)
|
||||
return df
|
||||
|
||||
@classmethod
|
||||
def df_column_renamer(cls, df: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Ad hoc function I created to clarify some fields
|
||||
|
||||
Args:
|
||||
df (DataFrame): input dataframe
|
||||
|
||||
Returns:
|
||||
DataFrame: dataframe with 'clarified' column names
|
||||
"""
|
||||
df = df[df.columns.drop(list(df.filter(regex='_hashes')))]
|
||||
return df.rename(columns={
|
||||
"contains_ratio": "contains_shared_hashes_ratio",
|
||||
"matches_ratio": "matches_shared_hashes_ratio",
|
||||
"kraken_count": "kraken2_read_count_(top_50)",
|
||||
"kraken_percent": "kraken2_read_percent_(top_50)"
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def displace_date(cls, df: DataFrame) -> DataFrame:
|
||||
"""
|
||||
This function serves to split samples 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.
|
||||
|
||||
Args:
|
||||
df (DataFrame): input dataframe composed of control records
|
||||
|
||||
Returns:
|
||||
DataFrame: output dataframe with dates incremented.
|
||||
"""
|
||||
# NOTE: get submitted dates for each control
|
||||
dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in
|
||||
sorted(df['name'].unique())]
|
||||
previous_dates = set()
|
||||
for item in dict_list:
|
||||
df, previous_dates = cls.check_date(df=df, item=item, previous_dates=previous_dates)
|
||||
return df
|
||||
|
||||
@classmethod
|
||||
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
|
||||
|
||||
Args:
|
||||
df (DataFrame): input dataframe
|
||||
item (dict): control for checking
|
||||
previous_dates (list): list of dates found in previous controls
|
||||
|
||||
Returns:
|
||||
Tuple[DataFrame, list]: Output dataframe and appended list of previous dates
|
||||
"""
|
||||
try:
|
||||
check = item['date'] in previous_dates
|
||||
except IndexError:
|
||||
check = False
|
||||
previous_dates.add(item['date'])
|
||||
if check:
|
||||
# NOTE: get df locations where name == item name
|
||||
mask = df['name'] == item['name']
|
||||
# NOTE: increment date in dataframe
|
||||
df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1))
|
||||
item['date'] += timedelta(days=1)
|
||||
passed = False
|
||||
else:
|
||||
passed = True
|
||||
# NOTE: if run didn't lead to changed date, return values
|
||||
if passed:
|
||||
return df, previous_dates
|
||||
# NOTE: if date was changed, rerun with new date
|
||||
else:
|
||||
logger.warning(f"Date check failed, running recursion.")
|
||||
df, previous_dates = cls.check_date(df, item, previous_dates)
|
||||
return df, previous_dates
|
||||
|
||||
@classmethod
|
||||
def prep_df(cls, ctx: Settings, df: DataFrame) -> Tuple[DataFrame | None, list]:
|
||||
"""
|
||||
Constructs figures based on parsed pandas dataframe.
|
||||
|
||||
Args:
|
||||
ctx (Settings): settings passed down from gui
|
||||
df (pd.DataFrame): input dataframe
|
||||
ytitle (str | None, optional): title for the y-axis. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Figure: Plotly figure
|
||||
"""
|
||||
# NOTE: converts starred genera to normal and splits off list of starred
|
||||
if df.empty:
|
||||
return None, []
|
||||
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()]
|
||||
# NOTE: remove original runs, using reruns if applicable
|
||||
df = cls.drop_reruns_from_df(ctx=ctx, df=df)
|
||||
# NOTE: sort by and exclude from
|
||||
sorts = ['submitted_date', "target", "genus"]
|
||||
exclude = ['name', 'genera']
|
||||
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.
|
||||
ascending = [False if item == "target" else True for item in sorts]
|
||||
df = df.sort_values(by=sorts, ascending=ascending)
|
||||
# NOTE: actual chart construction is done by
|
||||
return df, modes
|
||||
|
||||
@classmethod
|
||||
def drop_reruns_from_df(cls, ctx: Settings, df: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Removes semi-duplicates from dataframe after finding sequencing repeats.
|
||||
|
||||
Args:
|
||||
ctx (Settings): settings passed from gui
|
||||
df (DataFrame): initial dataframe
|
||||
|
||||
Returns:
|
||||
DataFrame: dataframe with originals removed in favour of repeats.
|
||||
"""
|
||||
if 'rerun_regex' in ctx.model_extra:
|
||||
sample_names = get_unique_values_in_df_column(df, column_name="name")
|
||||
rerun_regex = re.compile(fr"{ctx.rerun_regex}")
|
||||
exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
|
||||
df = df[~df.name.isin(exclude)]
|
||||
return df
|
||||
|
||||
def to_pydantic(self) -> "PydIridaControl":
|
||||
"""
|
||||
Constructs a pydantic version of this object.
|
||||
|
||||
Returns:
|
||||
PydIridaControl: This object as a pydantic model.
|
||||
"""
|
||||
from backend.validators import PydIridaControl
|
||||
return PydIridaControl(**self.__dict__)
|
||||
|
||||
@property
|
||||
def is_positive_control(self):
|
||||
return not self.subtype.lower().startswith("en")
|
||||
# class PCRControl(Control):
|
||||
# """
|
||||
# Class made to hold info from Design & Analysis software.
|
||||
# """
|
||||
#
|
||||
# id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
|
||||
# subtype = Column(String(16)) #: PC or NC
|
||||
# target = Column(String(16)) #: N1, N2, etc.
|
||||
# ct = Column(FLOAT) #: PCR result
|
||||
# reagent_lot = Column(String(64), ForeignKey("_reagent.lot", ondelete="SET NULL",
|
||||
# name="fk_reagent_lot"))
|
||||
# reagent = relationship("Reagent", foreign_keys=reagent_lot) #: reagent used for this control
|
||||
#
|
||||
# __mapper_args__ = dict(polymorphic_identity="PCR Control",
|
||||
# polymorphic_load="inline",
|
||||
# inherit_condition=(id == Control.id))
|
||||
#
|
||||
# def to_sub_dict(self) -> dict:
|
||||
# """
|
||||
# Creates dictionary of fields for this object.
|
||||
#
|
||||
# Returns:
|
||||
# dict: Output dict of name, ct, subtype, target, reagent_lot and submitted_date
|
||||
# """
|
||||
# return dict(
|
||||
# name=self.name,
|
||||
# ct=self.ct,
|
||||
# subtype=self.subtype,
|
||||
# target=self.target,
|
||||
# reagent_lot=self.reagent_lot,
|
||||
# submitted_date=self.submitted_date.date()
|
||||
# )
|
||||
#
|
||||
# @classmethod
|
||||
# @report_result
|
||||
# def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
|
||||
# """
|
||||
# Creates a PCRFigure. Overrides parent
|
||||
#
|
||||
# Args:
|
||||
# parent (__type__): Widget to contain the chart.
|
||||
# chart_settings (dict): settings passed down from chart widget
|
||||
# ctx (Settings): settings passed down from gui. Not used here.
|
||||
#
|
||||
# Returns:
|
||||
# Tuple[Report, "PCRFigure"]: Report of status and resulting figure.
|
||||
# """
|
||||
# from frontend.visualizations.pcr_charts import PCRFigure
|
||||
# parent.mode_typer.clear()
|
||||
# parent.mode_typer.setEnabled(False)
|
||||
# report = Report()
|
||||
# control = cls.query(proceduretype=chart_settings['submissiontype'], start_date=chart_settings['start_date'],
|
||||
# end_date=chart_settings['end_date'])
|
||||
# data = [control.to_sub_dict() for control in control]
|
||||
# df = DataFrame.from_records(data)
|
||||
# # NOTE: Get all PCR control with ct over 0
|
||||
# try:
|
||||
# df = df[df.ct > 0.0]
|
||||
# except AttributeError:
|
||||
# df = df
|
||||
# fig = PCRFigure(df=df, modes=[], settings=chart_settings)
|
||||
# return report, fig
|
||||
#
|
||||
# def to_pydantic(self):
|
||||
# from backend.validators import PydPCRControl
|
||||
# return PydPCRControl(**self.to_sub_dict(),
|
||||
# controltype_name=self.controltype_name,
|
||||
# clientsubmission_id=self.clientsubmission_id)
|
||||
#
|
||||
#
|
||||
# class IridaControl(Control):
|
||||
# subtyping_allowed = ['kraken']
|
||||
#
|
||||
# id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
|
||||
# contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism
|
||||
# matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
|
||||
# kraken = Column(JSON) #: unstructured output from kraken_report
|
||||
# subtype = Column(String(16), nullable=False) #: EN-NOS, MCS-NOS, etc
|
||||
# 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_db_version = Column(String(32)) #: folder name of kraken2 db
|
||||
# sample_id = Column(INTEGER,
|
||||
# ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
|
||||
#
|
||||
# __mapper_args__ = dict(polymorphic_identity="Irida Control",
|
||||
# polymorphic_load="inline",
|
||||
# inherit_condition=(id == Control.id))
|
||||
#
|
||||
# @property
|
||||
# def targets(self):
|
||||
# if self.controltype.targets:
|
||||
# return list(itertools.chain.from_iterable([value for key, value in self.controltype.targets.items()
|
||||
# if key == self.subtype]))
|
||||
# else:
|
||||
# return ["None"]
|
||||
#
|
||||
# @validates("subtype")
|
||||
# def enforce_subtype_literals(self, key: str, value: str) -> str:
|
||||
# """
|
||||
# Validates submissiontype field with acceptable values
|
||||
#
|
||||
# Args:
|
||||
# key (str): Field name
|
||||
# value (str): Field Value
|
||||
#
|
||||
# Raises:
|
||||
# KeyError: Raised if value is not in the acceptable list.
|
||||
#
|
||||
# Returns:
|
||||
# str: Validated string.
|
||||
# """
|
||||
# acceptables = ['ATCC49226', 'ATCC49619', 'EN-NOS', "EN-SSTI", "MCS-NOS", "MCS-SSTI", "SN-NOS", "SN-SSTI"]
|
||||
# if value.upper() not in acceptables:
|
||||
# raise KeyError(f"Sub-type must be in {acceptables}")
|
||||
# return value
|
||||
#
|
||||
# def to_sub_dict(self) -> dict:
|
||||
# """
|
||||
# Converts object into convenient dictionary for use in procedure summary
|
||||
#
|
||||
# Returns:
|
||||
# dict: output dictionary containing: Name, Type, Targets, Top Kraken results
|
||||
# """
|
||||
# try:
|
||||
# kraken = self.kraken
|
||||
# except TypeError:
|
||||
# kraken = {}
|
||||
# try:
|
||||
# kraken_cnt_total = sum([item['kraken_count'] for item in kraken.values()])
|
||||
# except AttributeError:
|
||||
# kraken_cnt_total = 0
|
||||
# try:
|
||||
# new_kraken = [dict(name=key, kraken_count=value['kraken_count'],
|
||||
# kraken_percent=f"{value['kraken_count'] / kraken_cnt_total:0.2%}",
|
||||
# target=key in self.controltype.targets)
|
||||
# for key, value in kraken.items()]
|
||||
# new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)[0:10]
|
||||
# except (AttributeError, ZeroDivisionError):
|
||||
# new_kraken = []
|
||||
# output = dict(
|
||||
# name=self.name,
|
||||
# type=self.controltype.name,
|
||||
# targets=", ".join(self.targets),
|
||||
# kraken=new_kraken
|
||||
# )
|
||||
# return output
|
||||
#
|
||||
# def convert_by_mode(self, control_sub_type: str, mode: Literal['kraken', 'matches', 'contains'],
|
||||
# consolidate: bool = False) -> Generator[dict, None, None]:
|
||||
# """
|
||||
# split this instance into analysis types ('kraken', 'matches', 'contains') for control graphs
|
||||
#
|
||||
# Args:
|
||||
# consolidate (bool): whether to merge all off-target genera. Defaults to False
|
||||
# control_sub_type (str): control subtype, 'MCS-NOS', etc.
|
||||
# mode (Literal['kraken', 'matches', 'contains']): analysis type, 'contains', etc.
|
||||
#
|
||||
# Returns:
|
||||
# List[dict]: list of records
|
||||
# """
|
||||
# try:
|
||||
# data = self.__getattribute__(mode)
|
||||
# except TypeError:
|
||||
# data = {}
|
||||
# if data is None:
|
||||
# data = {}
|
||||
# # NOTE: Data truncation and consolidation.
|
||||
# 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]}
|
||||
# else:
|
||||
# if consolidate:
|
||||
# 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
|
||||
# k.strip("*") not in self.controltype.targets[control_sub_type])
|
||||
# on_tar['Off-target'] = {f"{mode}_ratio": off_tar}
|
||||
# data = on_tar
|
||||
# for genus in data:
|
||||
# _dict = dict(
|
||||
# name=self.name,
|
||||
# submitted_date=self.submitted_date,
|
||||
# genus=genus,
|
||||
# target='Target' if genus.strip("*") in self.controltype.targets[control_sub_type] else "Off-target"
|
||||
# )
|
||||
# for key in data[genus]:
|
||||
# _dict[key] = data[genus][key]
|
||||
# yield _dict
|
||||
#
|
||||
# @classproperty
|
||||
# def modes(cls) -> List[str]:
|
||||
# """
|
||||
# Get all control modes from database
|
||||
#
|
||||
# Returns:
|
||||
# List[str]: List of control mode names.
|
||||
# """
|
||||
# try:
|
||||
# cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
|
||||
# except AttributeError as e:
|
||||
# logger.error(f"Failed to get available modes from db: {e}")
|
||||
# cols = []
|
||||
# return cols
|
||||
#
|
||||
# @classmethod
|
||||
# def make_parent_buttons(cls, parent: QWidget) -> None:
|
||||
# """
|
||||
# Creates buttons for controlling
|
||||
#
|
||||
# Args:
|
||||
# parent (QWidget): chart holding widget to add buttons to.
|
||||
#
|
||||
# """
|
||||
# super().make_parent_buttons(parent=parent)
|
||||
# rows = parent.layout.rowCount() - 2
|
||||
# # NOTE: check box for consolidating off-target items
|
||||
# checker = QCheckBox(parent)
|
||||
# checker.setChecked(True)
|
||||
# checker.setObjectName("irida_check")
|
||||
# checker.setToolTip("Pools off-target genera to save time.")
|
||||
# parent.layout.addWidget(QLabel("Consolidate Off-targets"), rows, 0, 1, 1)
|
||||
# parent.layout.addWidget(checker, rows, 1, 1, 2)
|
||||
# checker.checkStateChanged.connect(parent.update_data)
|
||||
#
|
||||
# @classmethod
|
||||
# @report_result
|
||||
# def make_chart(cls, chart_settings: dict, parent, ctx) -> Tuple[Report, "IridaFigure" | None]:
|
||||
# """
|
||||
# Creates a IridaFigure. Overrides parent
|
||||
#
|
||||
# Args:
|
||||
# parent (__type__): Widget to contain the chart.
|
||||
# chart_settings (dict): settings passed down from chart widget
|
||||
# ctx (Settings): settings passed down from gui.
|
||||
#
|
||||
# Returns:
|
||||
# Tuple[Report, "IridaFigure"]: Report of status and resulting figure.
|
||||
# """
|
||||
# from frontend.visualizations import IridaFigure
|
||||
# try:
|
||||
# checker = parent.findChild(QCheckBox, name="irida_check")
|
||||
# if chart_settings['mode'] == "kraken":
|
||||
# checker.setEnabled(False)
|
||||
# checker.setChecked(False)
|
||||
# else:
|
||||
# checker.setEnabled(True)
|
||||
# consolidate = checker.isChecked()
|
||||
# except AttributeError:
|
||||
# consolidate = False
|
||||
# report = Report()
|
||||
# control = cls.query(subtype=chart_settings['submissiontype'], start_date=chart_settings['start_date'],
|
||||
# end_date=chart_settings['end_date'])
|
||||
# if not control:
|
||||
# report.add_result(Result(status="Critical", msg="No control found in given date range."))
|
||||
# return report, None
|
||||
# # NOTE: change each control to list of dictionaries
|
||||
# data = [control.convert_by_mode(control_sub_type=chart_settings['submissiontype'], mode=chart_settings['mode'],
|
||||
# consolidate=consolidate) for
|
||||
# control in control]
|
||||
# # NOTE: flatten data to one dimensional list
|
||||
# # data = [item for sublist in data for item in sublist]
|
||||
# data = flatten_list(data)
|
||||
# if not data:
|
||||
# report.add_result(Result(status="Critical", msg="No data found for control in given date range."))
|
||||
# return report, None
|
||||
# df = cls.convert_data_list_to_df(input_df=data, sub_mode=chart_settings['sub_mode'])
|
||||
# if chart_settings['sub_mode'] is None:
|
||||
# title = chart_settings['sub_mode']
|
||||
# else:
|
||||
# title = f"{chart_settings['mode']} - {chart_settings['sub_mode']}"
|
||||
# # NOTE: send dataframe to chart maker
|
||||
# df, modes = cls.prep_df(ctx=ctx, df=df)
|
||||
# fig = IridaFigure(df=df, ytitle=title, modes=modes, parent=parent,
|
||||
# settings=chart_settings)
|
||||
# return report, fig
|
||||
#
|
||||
# @classmethod
|
||||
# def convert_data_list_to_df(cls, input_df: list[dict], sub_mode) -> DataFrame:
|
||||
# """
|
||||
# Convert list of control records to dataframe
|
||||
#
|
||||
# Args:
|
||||
# input_df (list[dict]): list of dictionaries containing records
|
||||
# sub_mode (str | None, optional): submissiontype of procedure type. Defaults to None.
|
||||
#
|
||||
# Returns:
|
||||
# DataFrame: dataframe of control
|
||||
# """
|
||||
# df = DataFrame.from_records(input_df)
|
||||
# safe = ['name', 'submitted_date', 'genus', 'target']
|
||||
# for column in df.columns:
|
||||
# if column not in safe:
|
||||
# if sub_mode is not None and column != sub_mode:
|
||||
# continue
|
||||
# else:
|
||||
# safe.append(column)
|
||||
# if "percent" in column:
|
||||
# try:
|
||||
# count_col = next(item for item in df.columns if "count" in item)
|
||||
# except StopIteration:
|
||||
# continue
|
||||
# # 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 = df[[c for c in df.columns if c in safe]]
|
||||
# # NOTE: move date of sample submitted on same date as previous ahead one.
|
||||
# df = cls.displace_date(df=df)
|
||||
# # NOTE: ad hoc method to make data labels more accurate.
|
||||
# df = cls.df_column_renamer(df=df)
|
||||
# return df
|
||||
#
|
||||
# @classmethod
|
||||
# def df_column_renamer(cls, df: DataFrame) -> DataFrame:
|
||||
# """
|
||||
# Ad hoc function I created to clarify some fields
|
||||
#
|
||||
# Args:
|
||||
# df (DataFrame): input dataframe
|
||||
#
|
||||
# Returns:
|
||||
# DataFrame: dataframe with 'clarified' column names
|
||||
# """
|
||||
# df = df[df.columns.drop(list(df.filter(regex='_hashes')))]
|
||||
# return df.rename(columns={
|
||||
# "contains_ratio": "contains_shared_hashes_ratio",
|
||||
# "matches_ratio": "matches_shared_hashes_ratio",
|
||||
# "kraken_count": "kraken2_read_count_(top_50)",
|
||||
# "kraken_percent": "kraken2_read_percent_(top_50)"
|
||||
# })
|
||||
#
|
||||
# @classmethod
|
||||
# def displace_date(cls, df: DataFrame) -> DataFrame:
|
||||
# """
|
||||
# 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.
|
||||
#
|
||||
# Args:
|
||||
# df (DataFrame): input dataframe composed of control records
|
||||
#
|
||||
# Returns:
|
||||
# DataFrame: output dataframe with dates incremented.
|
||||
# """
|
||||
# # NOTE: get submitted dates for each control
|
||||
# dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in
|
||||
# sorted(df['name'].unique())]
|
||||
# previous_dates = set()
|
||||
# for item in dict_list:
|
||||
# df, previous_dates = cls.check_date(df=df, item=item, previous_dates=previous_dates)
|
||||
# return df
|
||||
#
|
||||
# @classmethod
|
||||
# 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
|
||||
#
|
||||
# Args:
|
||||
# df (DataFrame): input dataframe
|
||||
# item (dict): control for checking
|
||||
# previous_dates (list): list of dates found in previous control
|
||||
#
|
||||
# Returns:
|
||||
# Tuple[DataFrame, list]: Output dataframe and appended list of previous dates
|
||||
# """
|
||||
# try:
|
||||
# check = item['date'] in previous_dates
|
||||
# except IndexError:
|
||||
# check = False
|
||||
# previous_dates.add(item['date'])
|
||||
# if check:
|
||||
# # NOTE: get df locations where name == item name
|
||||
# mask = df['name'] == item['name']
|
||||
# # NOTE: increment date in dataframe
|
||||
# df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1))
|
||||
# item['date'] += timedelta(days=1)
|
||||
# passed = False
|
||||
# else:
|
||||
# passed = True
|
||||
# # NOTE: if procedure didn't lead to changed date, return values
|
||||
# if passed:
|
||||
# return df, previous_dates
|
||||
# # NOTE: if date was changed, rerun with new date
|
||||
# else:
|
||||
# logger.warning(f"Date check failed, running recursion.")
|
||||
# df, previous_dates = cls.check_date(df, item, previous_dates)
|
||||
# return df, previous_dates
|
||||
#
|
||||
# @classmethod
|
||||
# def prep_df(cls, ctx: Settings, df: DataFrame) -> Tuple[DataFrame | None, list]:
|
||||
# """
|
||||
# Constructs figures based on parsed pandas dataframe.
|
||||
#
|
||||
# Args:
|
||||
# ctx (Settings): settings passed down from gui
|
||||
# df (pd.DataFrame): input dataframe
|
||||
# ytitle (str | None, optional): title for the y-axis. Defaults to None.
|
||||
#
|
||||
# Returns:
|
||||
# Figure: Plotly figure
|
||||
# """
|
||||
# # NOTE: converts starred genera to normal and splits off list of starred
|
||||
# if df.empty:
|
||||
# return None, []
|
||||
# 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()]
|
||||
# # NOTE: remove original run, using reruns if applicable
|
||||
# df = cls.drop_reruns_from_df(ctx=ctx, df=df)
|
||||
# # NOTE: sort by and exclude from
|
||||
# sorts = ['submitted_date', "target", "genus"]
|
||||
# exclude = ['name', 'genera']
|
||||
# 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.
|
||||
# ascending = [False if item == "target" else True for item in sorts]
|
||||
# df = df.sort_values(by=sorts, ascending=ascending)
|
||||
# # NOTE: actual chart construction is done by
|
||||
# return df, modes
|
||||
#
|
||||
# @classmethod
|
||||
# def drop_reruns_from_df(cls, ctx: Settings, df: DataFrame) -> DataFrame:
|
||||
# """
|
||||
# Removes semi-duplicates from dataframe after finding sequencing repeats.
|
||||
#
|
||||
# Args:
|
||||
# ctx (Settings): settings passed from gui
|
||||
# df (DataFrame): initial dataframe
|
||||
#
|
||||
# Returns:
|
||||
# DataFrame: dataframe with originals removed in favour of repeats.
|
||||
# """
|
||||
# if 'rerun_regex' in ctx.model_extra:
|
||||
# sample_names = get_unique_values_in_df_column(df, column_name="name")
|
||||
# rerun_regex = re.compile(fr"{ctx.rerun_regex}")
|
||||
# exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
|
||||
# df = df[~df.name.isin(exclude)]
|
||||
# return df
|
||||
#
|
||||
# def to_pydantic(self) -> "PydIridaControl":
|
||||
# """
|
||||
# Constructs a pydantic version of this object.
|
||||
#
|
||||
# Returns:
|
||||
# PydIridaControl: This object as a pydantic model.
|
||||
# """
|
||||
# from backend.validators import PydIridaControl
|
||||
# return PydIridaControl(**self.__dict__)
|
||||
#
|
||||
# @property
|
||||
# def is_positive_control(self):
|
||||
# return not self.subtype.lower().startswith("en")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,32 +14,27 @@ from typing import List, Tuple
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
# table containing organization/contact relationship
|
||||
orgs_contacts = Table(
|
||||
"_orgs_contacts",
|
||||
# table containing clientlab/contact relationship
|
||||
clientlab_contact = Table(
|
||||
"_clientlab_contact",
|
||||
Base.metadata,
|
||||
Column("org_id", INTEGER, ForeignKey("_organization.id")),
|
||||
Column("clientlab_id", INTEGER, ForeignKey("_clientlab.id")),
|
||||
Column("contact_id", INTEGER, ForeignKey("_contact.id")),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
|
||||
class Organization(BaseClass):
|
||||
class ClientLab(BaseClass):
|
||||
"""
|
||||
Base of organization
|
||||
Base of clientlab
|
||||
"""
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64)) #: organization name
|
||||
submissions = relationship("ClientSubmission",
|
||||
back_populates="submitting_lab") #: submissions this organization has submitted
|
||||
name = Column(String(64)) #: clientlab name
|
||||
clientsubmission = relationship("ClientSubmission", back_populates="clientlab") #: procedure this clientlab has submitted
|
||||
cost_centre = Column(String()) #: cost centre used by org for payment
|
||||
contacts = relationship("Contact", back_populates="organization",
|
||||
secondary=orgs_contacts) #: contacts involved with this org
|
||||
|
||||
@hybrid_property
|
||||
def contact(self):
|
||||
return self.contacts
|
||||
contact = relationship("Contact", back_populates="clientlab",
|
||||
secondary=clientlab_contact) #: contact involved with this org
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
@@ -47,16 +42,16 @@ class Organization(BaseClass):
|
||||
id: int | None = None,
|
||||
name: str | None = None,
|
||||
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:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
Organization|List[Organization]:
|
||||
ClientLab|List[ClientLab]:
|
||||
"""
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
match id:
|
||||
@@ -89,7 +84,7 @@ class Organization(BaseClass):
|
||||
name = "NA"
|
||||
return OmniOrganization(instance_object=self,
|
||||
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):
|
||||
@@ -101,27 +96,27 @@ class Contact(BaseClass):
|
||||
name = Column(String(64)) #: contact name
|
||||
email = Column(String(64)) #: contact email
|
||||
phone = Column(String(32)) #: contact phone number
|
||||
organization = relationship("Organization", back_populates="contacts", uselist=True,
|
||||
secondary=orgs_contacts) #: relationship to joined organization
|
||||
submissions = relationship("ClientSubmission", back_populates="contact") #: submissions this contact has submitted
|
||||
clientlab = relationship("ClientLab", back_populates="contact", uselist=True,
|
||||
secondary=clientlab_contact) #: relationship to joined clientlab
|
||||
clientsubmission = relationship("ClientSubmission", back_populates="contact") #: procedure this contact has submitted
|
||||
|
||||
@classproperty
|
||||
def searchables(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def query_or_create(cls, **kwargs) -> Tuple[Contact, bool]:
|
||||
new = False
|
||||
disallowed = []
|
||||
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
|
||||
instance = cls.query(**sanitized_kwargs)
|
||||
if not instance or isinstance(instance, list):
|
||||
instance = cls()
|
||||
new = True
|
||||
for k, v in sanitized_kwargs.items():
|
||||
setattr(instance, k, v)
|
||||
logger.info(f"Instance from contact query or create: {instance}")
|
||||
return instance, new
|
||||
# @classmethod
|
||||
# def query_or_create(cls, **kwargs) -> Tuple[Contact, bool]:
|
||||
# new = False
|
||||
# disallowed = []
|
||||
# sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
|
||||
# instance = cls.query(**sanitized_kwargs)
|
||||
# if not instance or isinstance(instance, list):
|
||||
# instance = cls()
|
||||
# new = True
|
||||
# for k, v in sanitized_kwargs.items():
|
||||
# setattr(instance, k, v)
|
||||
# logger.info(f"Instance from contact query or create: {instance}")
|
||||
# return instance, new
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
@@ -133,7 +128,7 @@ class Contact(BaseClass):
|
||||
limit: int = 0,
|
||||
) -> Contact | List[Contact]:
|
||||
"""
|
||||
Lookup contacts in the database by a number of parameters.
|
||||
Lookup contact in the database by a number of parameters.
|
||||
|
||||
Args:
|
||||
name (str | None, optional): Name of the contact. Defaults to None.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@ Contains pandas and openpyxl convenience functions for interacting with excel wo
|
||||
'''
|
||||
|
||||
from .parser import *
|
||||
from .submission_parser import *
|
||||
from backend.excel.parsers.submission_parser import *
|
||||
from .reports import *
|
||||
from .writer import *
|
||||
|
||||
@@ -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
|
||||
from copy import copy
|
||||
@@ -42,11 +42,11 @@ class SheetParser(object):
|
||||
raise FileNotFoundError(f"Couldn't parse file {self.filepath}")
|
||||
self.sub = OrderedDict()
|
||||
# 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)
|
||||
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)
|
||||
# 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.import_kit_validation_check()
|
||||
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)
|
||||
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:
|
||||
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:
|
||||
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
|
||||
logger.info(
|
||||
f"Checking for updated run type: {self.submission_type.name} against new: {parser.parsed_info['submission_type']['value']}")
|
||||
if self.submission_type.name != 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['proceduretype']['value']:
|
||||
if check:
|
||||
# NOTE: If initial run type doesn't match parsed run type, defer to parsed run type.
|
||||
self.submission_type = SubmissionType.query(name=parser.parsed_info['submission_type']['value'])
|
||||
logger.info(f"Updated self.submission_type to {self.submission_type}. Rerunning parse.")
|
||||
# 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['proceduretype']['value'])
|
||||
logger.info(f"Updated self.proceduretype to {self.submission_type}. Rerunning parse.")
|
||||
self.parse_info()
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Calls reagent parser class to pull info from the excel sheet
|
||||
Calls reagent clientsubmissionparser class to pull info from the excel sheet
|
||||
|
||||
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:
|
||||
extraction_kit = self.sub['extraction_kit']
|
||||
extraction_kit = self.sub['kittype']
|
||||
parser = ReagentParser(xl=self.xl, submission_type=self.submission_type,
|
||||
extraction_kit=extraction_kit)
|
||||
self.sub['reagents'] = parser.parsed_reagents
|
||||
|
||||
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)
|
||||
self.sub['samples'] = parser.parsed_samples
|
||||
self.sub['sample'] = parser.parsed_samples
|
||||
|
||||
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)
|
||||
self.sub['equipment'] = parser.parsed_equipment
|
||||
|
||||
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)
|
||||
self.sub['tips'] = parser.parsed_tips
|
||||
|
||||
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
|
||||
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)
|
||||
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:
|
||||
raise ValueError("Extraction kit needed.")
|
||||
raise ValueError("Extraction kittype needed.")
|
||||
else:
|
||||
if isinstance(self.sub['extraction_kit'], str):
|
||||
self.sub['extraction_kit'] = dict(value=self.sub['extraction_kit'], missing=True)
|
||||
if isinstance(self.sub['kittype'], str):
|
||||
self.sub['kittype'] = dict(value=self.sub['kittype'], missing=True)
|
||||
|
||||
def to_pydantic(self) -> PydSubmission:
|
||||
"""
|
||||
@@ -145,17 +145,17 @@ class InfoParser(object):
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
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 = dict(value=self.submission_type_obj.name, missing=True)
|
||||
self.sub_object = sub_object
|
||||
@@ -164,12 +164,12 @@ class InfoParser(object):
|
||||
@property
|
||||
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:
|
||||
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")
|
||||
|
||||
@property
|
||||
@@ -186,7 +186,7 @@ class InfoParser(object):
|
||||
ws = self.xl[sheet]
|
||||
relevant = []
|
||||
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":
|
||||
continue
|
||||
if isinstance(v, str):
|
||||
@@ -210,7 +210,7 @@ class InfoParser(object):
|
||||
# NOTE: Get cell contents at this location
|
||||
value = ws.cell(row=item['row'], column=item['column']).value
|
||||
match item['name']:
|
||||
case "submission_type":
|
||||
case "proceduretype":
|
||||
value, missing = is_missing(value)
|
||||
value = value.title()
|
||||
case "submitted_date":
|
||||
@@ -232,7 +232,7 @@ class InfoParser(object):
|
||||
dicto[item['name']] = dict(value=value, missing=missing)
|
||||
except (KeyError, IndexError):
|
||||
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'])
|
||||
|
||||
|
||||
@@ -242,12 +242,12 @@ class ReagentParser(object):
|
||||
"""
|
||||
|
||||
def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str,
|
||||
run_object: BasicRun | None = None):
|
||||
run_object: Run | None = None):
|
||||
"""
|
||||
Args:
|
||||
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||
submission_type (str|SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
|
||||
extraction_kit (str): Extraction kit used.
|
||||
submission_type (str|SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
|
||||
extraction_kit (str): Extraction kittype used.
|
||||
run_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
@@ -264,15 +264,16 @@ class ReagentParser(object):
|
||||
@property
|
||||
def kit_map(self) -> dict:
|
||||
"""
|
||||
Gets location of kit reagents from database
|
||||
Gets location of kittype reagents from database
|
||||
|
||||
Args:
|
||||
submission_type (str): Name of run type.
|
||||
proceduretype (str): Name of procedure type.
|
||||
|
||||
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'}
|
||||
try:
|
||||
del reagent_map['info']
|
||||
@@ -323,16 +324,16 @@ class ReagentParser(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,
|
||||
sub_object: BasicRun | None = None) -> None:
|
||||
sub_object: Run | None = None) -> None:
|
||||
"""
|
||||
Args:
|
||||
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||
submission_type (SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
|
||||
sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None.
|
||||
submission_type (SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
|
||||
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.
|
||||
"""
|
||||
self.samples = []
|
||||
@@ -343,19 +344,19 @@ class SampleParser(object):
|
||||
self.submission_type_obj = submission_type
|
||||
if sub_object is None:
|
||||
logger.warning(
|
||||
f"Sample parser attempting to fetch run class with polymorphic identity: {self.submission_type}")
|
||||
sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
||||
f"Sample clientsubmissionparser attempting to fetch procedure class with polymorphic identity: {self.submission_type}")
|
||||
sub_object = Run.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
||||
self.sub_object = sub_object
|
||||
self.sample_type = self.sub_object.get_default_info("sample_type", submission_type=submission_type)
|
||||
self.samp_object = BasicSample.find_polymorphic_subclass(polymorphic_identity=self.sample_type)
|
||||
self.sample_type = self.sub_object.get_default_info("sampletype", submission_type=submission_type)
|
||||
self.samp_object = Sample.find_polymorphic_subclass(polymorphic_identity=self.sample_type)
|
||||
|
||||
@property
|
||||
def sample_map(self) -> dict:
|
||||
"""
|
||||
Gets info locations in excel book for run type.
|
||||
Gets info locations in excel book for procedure type.
|
||||
|
||||
Args:
|
||||
submission_type (str): run type
|
||||
proceduretype (str): procedure type
|
||||
|
||||
Returns:
|
||||
dict: Info locations.
|
||||
@@ -381,7 +382,7 @@ class SampleParser(object):
|
||||
if check_not_nan(id):
|
||||
if id not in invalids:
|
||||
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)
|
||||
else:
|
||||
pass
|
||||
@@ -407,7 +408,7 @@ class SampleParser(object):
|
||||
row_dict[lmap['merge_on_id']] = str(row_dict[lmap['merge_on_id']])
|
||||
except KeyError:
|
||||
pass
|
||||
row_dict['sample_type'] = self.sample_type
|
||||
row_dict['sampletype'] = self.sample_type
|
||||
row_dict['submission_rank'] = ii
|
||||
try:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
List[dict]: Reconciled samples
|
||||
List[dict]: Reconciled sample
|
||||
"""
|
||||
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
|
||||
for new in samples:
|
||||
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True):
|
||||
new['submitter_id'] = new['id']
|
||||
if not check_key_or_attr(key='sample_id', interest=new, check_none=True):
|
||||
new['sample_id'] = new['id']
|
||||
new = self.sub_object.parse_samples(new)
|
||||
try:
|
||||
del new['id']
|
||||
@@ -459,8 +460,8 @@ class SampleParser(object):
|
||||
if lsample[merge_on_id] == psample['id']), (-1, psample))
|
||||
if jj >= 0:
|
||||
lookup_samples[jj] = {}
|
||||
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True):
|
||||
new['submitter_id'] = psample['id']
|
||||
if not check_key_or_attr(key='sample_id', interest=new, check_none=True):
|
||||
new['sample_id'] = psample['id']
|
||||
new = self.sub_object.parse_samples(new)
|
||||
try:
|
||||
del new['id']
|
||||
@@ -478,7 +479,7 @@ class EquipmentParser(object):
|
||||
"""
|
||||
Args:
|
||||
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):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
@@ -488,7 +489,7 @@ class EquipmentParser(object):
|
||||
@property
|
||||
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:
|
||||
List[dict]: List of locations
|
||||
@@ -556,7 +557,7 @@ class TipParser(object):
|
||||
"""
|
||||
Args:
|
||||
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):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
@@ -566,7 +567,7 @@ class TipParser(object):
|
||||
@property
|
||||
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:
|
||||
List[dict]: List of locations
|
||||
@@ -609,7 +610,7 @@ class TipParser(object):
|
||||
class PCRParser(object):
|
||||
"""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:
|
||||
filepath (Path | None, optional): file to parse. Defaults to None.
|
||||
@@ -659,7 +660,7 @@ class PCRParser(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:
|
||||
logger.error('No filepath given.')
|
||||
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.")
|
||||
return None
|
||||
if run is None:
|
||||
self.submission_obj = BasicRun()
|
||||
self.submission_obj = Run()
|
||||
rsl_plate_num = None
|
||||
else:
|
||||
self.submission_obj = run
|
||||
|
||||
@@ -7,7 +7,7 @@ from pandas import DataFrame, ExcelWriter
|
||||
from pathlib import Path
|
||||
from datetime import date
|
||||
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 PyQt6.QtWidgets import QWidget
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
@@ -45,9 +45,9 @@ class ReportMaker(object):
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
# 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:
|
||||
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.html = self.make_report_html(df=self.summary_df)
|
||||
|
||||
@@ -61,14 +61,14 @@ class ReportMaker(object):
|
||||
if not self.runs:
|
||||
return DataFrame(), DataFrame()
|
||||
df = DataFrame.from_records([item.to_dict(report=True) for item in self.runs])
|
||||
# NOTE: put submissions with the same lab together
|
||||
df = df.sort_values("submitting_lab")
|
||||
# NOTE: put procedure with the same lab together
|
||||
df = df.sort_values("clientlab")
|
||||
# NOTE: aggregate cost and sample count columns
|
||||
df2 = df.groupby(["submitting_lab", "extraction_kit"]).agg(
|
||||
{'extraction_kit': 'count', 'cost': 'sum', 'sample_count': 'sum'})
|
||||
df2 = df2.rename(columns={"extraction_kit": 'run_count'})
|
||||
df2 = df.groupby(["clientlab", "kittype"]).agg(
|
||||
{'kittype': 'count', 'cost': 'sum', 'sample_count': 'sum'})
|
||||
df2 = df2.rename(columns={"kittype": 'run_count'})
|
||||
df = df.drop('id', axis=1)
|
||||
df = df.sort_values(['submitting_lab', "started_date"])
|
||||
df = df.sort_values(['clientlab', "started_date"])
|
||||
return df, df2
|
||||
|
||||
def make_report_html(self, df: DataFrame) -> str:
|
||||
@@ -156,19 +156,19 @@ class TurnaroundMaker(ReportArchetype):
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
# NOTE: Set page size to zero to override limiting query size.
|
||||
self.subs = BasicRun.query(start_date=start_date, end_date=end_date,
|
||||
submission_type_name=submission_type, page_size=0)
|
||||
self.subs = Run.query(start_date=start_date, end_date=end_date,
|
||||
submissiontype_name=submission_type, page_size=0)
|
||||
records = [self.build_record(sub) for sub in self.subs]
|
||||
self.df = DataFrame.from_records(records)
|
||||
self.sheet_name = "Turnaround"
|
||||
|
||||
@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:
|
||||
sub (BasicRun): The run to be processed.
|
||||
sub (BasicRun): The procedure to be processed.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -203,9 +203,9 @@ class ConcentrationMaker(ReportArchetype):
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
# NOTE: Set page size to zero to override limiting query size.
|
||||
self.subs = BasicRun.query(start_date=start_date, end_date=end_date,
|
||||
submission_type_name=submission_type, page_size=0)
|
||||
# self.samples = flatten_list([sub.get_provisional_controls(controls_only=controls_only) for sub in self.runs])
|
||||
self.subs = Run.query(start_date=start_date, end_date=end_date,
|
||||
submissiontype_name=submission_type, page_size=0)
|
||||
# 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.records = [self.build_record(sample) for sample in self.samples]
|
||||
self.df = DataFrame.from_records(self.records)
|
||||
@@ -214,9 +214,9 @@ class ConcentrationMaker(ReportArchetype):
|
||||
@classmethod
|
||||
def build_record(cls, control) -> dict:
|
||||
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"
|
||||
elif control.submitter_id.lower().startswith("en"):
|
||||
elif control.sample_id.lower().startswith("en"):
|
||||
positive = "negative"
|
||||
else:
|
||||
positive = "sample"
|
||||
@@ -224,8 +224,8 @@ class ConcentrationMaker(ReportArchetype):
|
||||
concentration = float(control.concentration)
|
||||
except (TypeError, ValueError):
|
||||
concentration = 0.0
|
||||
return dict(name=control.submitter_id,
|
||||
submission=str(control.submission), concentration=concentration,
|
||||
return dict(name=control.sample_id,
|
||||
submission=str(control.clientsubmission), concentration=concentration,
|
||||
submitted_date=control.submitted_date, positive=positive)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
from copy import copy
|
||||
@@ -8,7 +8,7 @@ from operator import itemgetter
|
||||
from pprint import pformat
|
||||
from typing import List, Generator, Tuple
|
||||
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 io import BytesIO
|
||||
from collections import OrderedDict
|
||||
@@ -24,7 +24,7 @@ class SheetWriter(object):
|
||||
def __init__(self, submission: PydSubmission):
|
||||
"""
|
||||
Args:
|
||||
submission (PydSubmission): Object containing run information.
|
||||
submission (PydSubmission): Object containing procedure information.
|
||||
"""
|
||||
self.sub = OrderedDict(submission.improved_dict())
|
||||
# NOTE: Set values from pydantic object.
|
||||
@@ -32,7 +32,7 @@ class SheetWriter(object):
|
||||
match k:
|
||||
case 'filepath':
|
||||
self.__setattr__(k, v)
|
||||
case 'submission_type':
|
||||
case 'proceduretype':
|
||||
self.sub[k] = v['value']
|
||||
self.submission_type = SubmissionType.query(name=v['value'])
|
||||
self.run_object = BasicRun.find_polymorphic_subclass(
|
||||
@@ -58,7 +58,7 @@ class SheetWriter(object):
|
||||
"""
|
||||
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}
|
||||
writer = InfoWriter(xl=self.xl, submission_type=self.submission_type, info_dict=info_dict)
|
||||
self.xl = writer.write_info()
|
||||
@@ -69,14 +69,14 @@ class SheetWriter(object):
|
||||
"""
|
||||
reagent_list = self.sub['reagents']
|
||||
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()
|
||||
|
||||
def write_samples(self):
|
||||
"""
|
||||
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)
|
||||
self.xl = writer.write_samples()
|
||||
|
||||
@@ -99,22 +99,22 @@ class SheetWriter(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,
|
||||
sub_object: BasicRun | None = None):
|
||||
sub_object: Run | None = None):
|
||||
"""
|
||||
Args:
|
||||
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.
|
||||
sub_object (BasicRun | None, optional): Submission object containing methods. Defaults to None.
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
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.sub_object = sub_object
|
||||
self.xl = xl
|
||||
@@ -196,8 +196,8 @@ class ReagentWriter(object):
|
||||
"""
|
||||
Args:
|
||||
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||
submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.)
|
||||
extraction_kit (KitType | str): Extraction kit used.
|
||||
submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.)
|
||||
extraction_kit (KitType | str): Extraction kittype used.
|
||||
reagent_list (list): List of reagent dicts to be written to excel.
|
||||
"""
|
||||
self.xl = xl
|
||||
@@ -208,7 +208,7 @@ class ReagentWriter(object):
|
||||
extraction_kit = KitType.query(name=extraction_kit)
|
||||
self.kit_object = extraction_kit
|
||||
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()}
|
||||
self.reagents = self.reconcile_map(reagent_list=reagent_list, reagent_map=reagent_map)
|
||||
|
||||
@@ -223,13 +223,13 @@ class ReagentWriter(object):
|
||||
Returns:
|
||||
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():
|
||||
if map_obj not in filled_roles:
|
||||
reagent_list.append(dict(name="Not Applicable", role=map_obj, lot="Not Applicable", expiry="Not Applicable"))
|
||||
for reagent in reagent_list:
|
||||
try:
|
||||
mp_info = reagent_map[reagent['role']]
|
||||
mp_info = reagent_map[reagent['reagentrole']]
|
||||
except KeyError:
|
||||
continue
|
||||
placeholder = copy(reagent)
|
||||
@@ -273,7 +273,7 @@ class SampleWriter(object):
|
||||
"""
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
@@ -281,7 +281,7 @@ class SampleWriter(object):
|
||||
self.submission_type = submission_type
|
||||
self.xl = xl
|
||||
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]
|
||||
self.samples = sorted(samples, key=itemgetter('submission_rank'))
|
||||
self.blank_lookup_table()
|
||||
@@ -322,7 +322,7 @@ class SampleWriter(object):
|
||||
Performs writing operations.
|
||||
|
||||
Returns:
|
||||
Workbook: Workbook with samples written
|
||||
Workbook: Workbook with sample written
|
||||
"""
|
||||
sheet = self.xl[self.sample_map['sheet']]
|
||||
columns = self.sample_map['sample_columns']
|
||||
@@ -351,7 +351,7 @@ class EquipmentWriter(object):
|
||||
"""
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
@@ -376,9 +376,9 @@ class EquipmentWriter(object):
|
||||
return
|
||||
for ii, equipment in enumerate(equipment_list, start=1):
|
||||
try:
|
||||
mp_info = equipment_map[equipment['role']]
|
||||
mp_info = equipment_map[equipment['reagentrole']]
|
||||
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
|
||||
placeholder = copy(equipment)
|
||||
if not mp_info:
|
||||
@@ -433,7 +433,7 @@ class TipWriter(object):
|
||||
"""
|
||||
Args:
|
||||
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.
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging, re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
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 jinja2 import Template
|
||||
from dateutil.parser import parse
|
||||
@@ -25,22 +25,22 @@ class RSLNamer(object):
|
||||
self.submission_type = submission_type
|
||||
if not self.submission_type:
|
||||
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:
|
||||
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(
|
||||
submission_type=submission_type))
|
||||
if not data:
|
||||
data = dict(submission_type=self.submission_type)
|
||||
if "submission_type" not in data.keys():
|
||||
data['submission_type'] = self.submission_type
|
||||
if "proceduretype" not in data.keys():
|
||||
data['proceduretype'] = self.submission_type
|
||||
self.parsed_name = self.sub_object.enforce_name(instr=self.parsed_name, data=data)
|
||||
logger.info(f"Parsed name: {self.parsed_name}")
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
filename (str | Path): filename
|
||||
@@ -49,7 +49,7 @@ class RSLNamer(object):
|
||||
TypeError: Raised if unsupported variable type for filename given.
|
||||
|
||||
Returns:
|
||||
str: parsed run type
|
||||
str: parsed procedure type
|
||||
"""
|
||||
|
||||
def st_from_path(filepath: Path) -> str:
|
||||
@@ -89,7 +89,7 @@ class RSLNamer(object):
|
||||
sub_type = m.lastgroup
|
||||
except AttributeError as e:
|
||||
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
|
||||
|
||||
match filename:
|
||||
@@ -107,8 +107,8 @@ class RSLNamer(object):
|
||||
if "pytest" in sys.modules:
|
||||
raise ValueError("Submission Type came back as None.")
|
||||
from frontend.widgets import ObjectSelector
|
||||
dlg = ObjectSelector(title="Couldn't parse run type.",
|
||||
message="Please select run type from list below.",
|
||||
dlg = ObjectSelector(title="Couldn't parse procedure type.",
|
||||
message="Please select procedure type from list below.",
|
||||
obj_type=SubmissionType)
|
||||
if dlg.exec():
|
||||
submission_type = dlg.parse_form()
|
||||
@@ -118,7 +118,7 @@ class RSLNamer(object):
|
||||
@classmethod
|
||||
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:
|
||||
regex (str): string to construct pattern
|
||||
@@ -145,14 +145,15 @@ class RSLNamer(object):
|
||||
@classmethod
|
||||
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:
|
||||
data (dict): incoming run data
|
||||
data (dict): incoming procedure data
|
||||
|
||||
Returns:
|
||||
str: Output filename
|
||||
"""
|
||||
logger.debug(data)
|
||||
if "submitted_date" in data.keys():
|
||||
if isinstance(data['submitted_date'], dict):
|
||||
if data['submitted_date']['value'] is not None:
|
||||
@@ -163,14 +164,16 @@ class RSLNamer(object):
|
||||
today = data['submitted_date']
|
||||
else:
|
||||
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())
|
||||
except (AttributeError, KeyError):
|
||||
today = datetime.now()
|
||||
if "rsl_plate_num" in data.keys():
|
||||
plate_number = data['rsl_plate_num'].split("-")[-1][0]
|
||||
if isinstance(today, str):
|
||||
today = datetime.strptime(today, "%Y-%m-%d")
|
||||
if "name" in data.keys():
|
||||
plate_number = data['name'].split("-")[-1][0]
|
||||
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
|
||||
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, \
|
||||
PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl, PydProcess, PydElastic, PydClientSubmission
|
||||
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission
|
||||
|
||||
@@ -650,22 +650,22 @@ class OmniProcess(BaseOmni):
|
||||
new_assoc = st.to_sql()
|
||||
except AttributeError:
|
||||
new_assoc = SubmissionType.query(name=st)
|
||||
if new_assoc not in instance.submission_types:
|
||||
instance.submission_types.append(new_assoc)
|
||||
if new_assoc not in instance.proceduretype:
|
||||
instance.proceduretype.append(new_assoc)
|
||||
for er in self.equipment_roles:
|
||||
try:
|
||||
new_assoc = er.to_sql()
|
||||
except AttributeError:
|
||||
new_assoc = EquipmentRole.query(name=er)
|
||||
if new_assoc not in instance.equipment_roles:
|
||||
instance.equipment_roles.append(new_assoc)
|
||||
if new_assoc not in instance.equipmentrole:
|
||||
instance.equipmentrole.append(new_assoc)
|
||||
for tr in self.tip_roles:
|
||||
try:
|
||||
new_assoc = tr.to_sql()
|
||||
except AttributeError:
|
||||
new_assoc = TipRole.query(name=tr)
|
||||
if new_assoc not in instance.tip_roles:
|
||||
instance.tip_roles.append(new_assoc)
|
||||
if new_assoc not in instance.tiprole:
|
||||
instance.tiprole.append(new_assoc)
|
||||
return instance
|
||||
|
||||
@property
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ class CustomFigure(Figure):
|
||||
Creates list of buttons with one for each mode to be used in showing/hiding mode traces.
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -28,12 +28,12 @@ class ConcentrationsChart(CustomFigure):
|
||||
self.df = df
|
||||
try:
|
||||
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)
|
||||
self.df = self.df.reset_index().rename(columns={"index": "idx"})
|
||||
# logger.debug(f"DF after changes:\n{self.df}")
|
||||
scatter = px.scatter(data_frame=self.df, x='run', y="concentration",
|
||||
hover_data=["name", "run", "submitted_date", "concentration"],
|
||||
scatter = px.scatter(data_frame=self.df, x='procedure', y="concentration",
|
||||
hover_data=["name", "procedure", "submitted_date", "concentration"],
|
||||
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}
|
||||
)
|
||||
except (ValueError, AttributeError) as e:
|
||||
@@ -44,11 +44,11 @@ class ConcentrationsChart(CustomFigure):
|
||||
for trace in traces:
|
||||
self.add_trace(trace)
|
||||
try:
|
||||
tickvals = self.df['run'].tolist()
|
||||
tickvals = self.df['procedure'].tolist()
|
||||
except KeyError:
|
||||
tickvals = []
|
||||
try:
|
||||
ticklabels = self.df['run'].tolist()
|
||||
ticklabels = self.df['procedure'].tolist()
|
||||
except KeyError:
|
||||
ticklabels = []
|
||||
self.update_layout(
|
||||
|
||||
@@ -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 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):
|
||||
"""
|
||||
Creates a plotly chart for controls from a pandas dataframe
|
||||
Creates a plotly chart for control from a pandas dataframe
|
||||
|
||||
Args:
|
||||
end_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
|
||||
ytitle (str | None, optional): title on the y-axis. Defaults to None.
|
||||
|
||||
|
||||
@@ -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 . import CustomFigure
|
||||
|
||||
@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from markdown import markdown
|
||||
from pandas import ExcelWriter
|
||||
from backend import Reagent, BasicSample, Organization, KitType, BasicRun
|
||||
from backend import Reagent, Sample, ClientSubmission, KitType, Run
|
||||
from tools import (
|
||||
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
|
||||
under_development
|
||||
@@ -22,7 +22,7 @@ from .date_type_picker import DateTypePicker
|
||||
from .functions import select_save_file
|
||||
from .pop_ups import HTMLPop
|
||||
from .misc import Pagifier
|
||||
from .submission_table import SubmissionsSheet, SubmissionsTree, ClientRunModel
|
||||
from .submission_table import SubmissionsSheet, SubmissionsTree, ClientSubmissionRunModel
|
||||
from .submission_widget import SubmissionFormContainer
|
||||
from .controls_chart import ControlsViewer
|
||||
from .summary import Summary
|
||||
@@ -30,7 +30,7 @@ from .turnaround import TurnaroundTime
|
||||
from .concentrations import Concentrations
|
||||
from .omni_search import SearchBox
|
||||
|
||||
logger = logging.getLogger(f'submissions.{__name__}')
|
||||
logger = logging.getLogger(f'procedure.{__name__}')
|
||||
|
||||
|
||||
class App(QMainWindow):
|
||||
@@ -57,7 +57,7 @@ class App(QMainWindow):
|
||||
# NOTE: insert tabs into main app
|
||||
self.table_widget = AddSubForm(self)
|
||||
self.setCentralWidget(self.table_widget)
|
||||
# NOTE: run initial setups
|
||||
# NOTE: procedure initial setups
|
||||
self._createActions()
|
||||
self._createMenuBar()
|
||||
self._createToolBar()
|
||||
@@ -173,14 +173,14 @@ class App(QMainWindow):
|
||||
|
||||
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()
|
||||
|
||||
@check_authorization
|
||||
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()
|
||||
|
||||
def update_data(self):
|
||||
@@ -239,7 +239,7 @@ class AddSubForm(QWidget):
|
||||
self.tabs.addTab(self.tab3, "PCR Controls")
|
||||
self.tabs.addTab(self.tab4, "Cost Report")
|
||||
self.tabs.addTab(self.tab5, "Turnaround Times")
|
||||
# NOTE: Create run adder form
|
||||
# NOTE: Create procedure adder form
|
||||
self.formwidget = SubmissionFormContainer(self)
|
||||
self.formlayout = QVBoxLayout(self)
|
||||
self.formwidget.setLayout(self.formlayout)
|
||||
@@ -249,12 +249,12 @@ class AddSubForm(QWidget):
|
||||
self.interior.setWidgetResizable(True)
|
||||
self.interior.setFixedWidth(325)
|
||||
self.interior.setWidget(self.formwidget)
|
||||
# NOTE: Create sheet to hold existing submissions
|
||||
# NOTE: Create sheet to hold existing procedure
|
||||
self.sheetwidget = QWidget(self)
|
||||
self.sheetlayout = QVBoxLayout(self)
|
||||
self.sheetwidget.setLayout(self.sheetlayout)
|
||||
# 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.sheetlayout.addWidget(self.sub_wid)
|
||||
self.sheetlayout.addWidget(self.pager)
|
||||
@@ -264,11 +264,13 @@ class AddSubForm(QWidget):
|
||||
self.tab1.layout.addWidget(self.interior)
|
||||
self.tab1.layout.addWidget(self.sheetwidget)
|
||||
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.setLayout(self.tab2.layout)
|
||||
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.setLayout(self.tab3.layout)
|
||||
summary_report = Summary(self)
|
||||
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend import ChartReportMaker
|
||||
from backend.db import ControlType, IridaControl
|
||||
from backend.db import ControlType
|
||||
import logging
|
||||
from tools import Report, report_result
|
||||
from frontend.visualizations import CustomFigure
|
||||
@@ -25,7 +25,7 @@ class ControlsViewer(InfoPane):
|
||||
return
|
||||
# NOTE: set tab2 layout
|
||||
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()]
|
||||
self.control_sub_typer.addItems(con_sub_types)
|
||||
# NOTE: create custom widget to get types of analysis -- disabled by PCR control
|
||||
@@ -52,7 +52,7 @@ class ControlsViewer(InfoPane):
|
||||
@report_result
|
||||
def update_data(self, *args, **kwargs):
|
||||
"""
|
||||
Get controls based on start/end dates
|
||||
Get control based on start/end dates
|
||||
"""
|
||||
super().update_data()
|
||||
# NOTE: mode_sub_type defaults to disabled
|
||||
@@ -70,7 +70,7 @@ class ControlsViewer(InfoPane):
|
||||
sub_types = []
|
||||
# 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:
|
||||
# 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:
|
||||
self.mode_sub_typer.addItems(sub_types)
|
||||
self.mode_sub_typer.setEnabled(True)
|
||||
@@ -83,7 +83,7 @@ class ControlsViewer(InfoPane):
|
||||
@report_result
|
||||
def chart_maker_function(self, *args, **kwargs):
|
||||
"""
|
||||
Create html chart for controls reporting
|
||||
Create html chart for control reporting
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -98,7 +98,7 @@ class ControlsViewer(InfoPane):
|
||||
else:
|
||||
self.mode_sub_type = self.mode_sub_typer.currentText()
|
||||
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(
|
||||
sub_type=self.con_sub_type,
|
||||
start_date=self.start_date,
|
||||
|
||||
@@ -6,7 +6,7 @@ from PyQt6.QtCore import Qt, QSignalBlocker
|
||||
from PyQt6.QtWidgets import (
|
||||
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
|
||||
import logging
|
||||
from typing import Generator
|
||||
@@ -16,13 +16,13 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class EquipmentUsage(QDialog):
|
||||
|
||||
def __init__(self, parent, submission: BasicRun):
|
||||
def __init__(self, parent, procedure: Procedure):
|
||||
super().__init__(parent)
|
||||
self.submission = submission
|
||||
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
|
||||
self.used_equipment = self.submission.used_equipment
|
||||
self.kit = self.submission.extraction_kit
|
||||
self.opt_equipment = submission.submission_type.get_equipment()
|
||||
self.procedure = procedure
|
||||
self.setWindowTitle(f"Equipment Checklist - {procedure.rsl_plate_num}")
|
||||
self.used_equipment = self.procedure.equipment
|
||||
self.kit = self.procedure.kittype
|
||||
self.opt_equipment = procedure.proceduretype.get_equipment()
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.populate_form()
|
||||
@@ -120,7 +120,7 @@ class RoleComboBox(QWidget):
|
||||
|
||||
def update_processes(self):
|
||||
"""
|
||||
Changes processes when equipment is changed
|
||||
Changes process when equipment is changed
|
||||
"""
|
||||
equip = self.box.currentText()
|
||||
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 = Process.query(name=process)
|
||||
if process.tip_roles:
|
||||
for iii, tip_role in enumerate(process.tip_roles):
|
||||
if process.tiprole:
|
||||
for iii, tip_role in enumerate(process.tiprole):
|
||||
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.addItems(tip_choices)
|
||||
widget.setObjectName(f"tips_{tip_role.name}")
|
||||
|
||||
@@ -12,7 +12,7 @@ import logging, numpy as np
|
||||
from pprint import pformat
|
||||
from typing import Tuple, List
|
||||
from pathlib import Path
|
||||
from backend.db.models import BasicRun
|
||||
from backend.db.models import Run
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -20,7 +20,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
# Main window class
|
||||
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)
|
||||
# NOTE: setting title
|
||||
self.setWindowTitle(f"Gel - {img_path}")
|
||||
@@ -135,7 +135,7 @@ class ControlsForm(QWidget):
|
||||
|
||||
def parse_form(self) -> Tuple[List[dict], str]:
|
||||
"""
|
||||
Pulls the controls statuses from the form.
|
||||
Pulls the control statuses from the form.
|
||||
|
||||
Returns:
|
||||
List[dict]: output of values
|
||||
|
||||
@@ -39,7 +39,7 @@ class InfoPane(QWidget):
|
||||
lastmonth = self.datepicker.end_date.date().addDays(-31)
|
||||
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
|
||||
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:
|
||||
self.datepicker.start_date.setDate(lastmonth)
|
||||
self.update_data()
|
||||
|
||||
@@ -19,7 +19,7 @@ env = jinja_template_loading()
|
||||
|
||||
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:
|
||||
|
||||
@@ -71,7 +71,7 @@ class AddEdit(QDialog):
|
||||
# logger.debug(f"We have an elastic model.")
|
||||
parsed['instance'] = self.instance
|
||||
# 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)
|
||||
return model, report
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class ManagerWindow(QDialog):
|
||||
|
||||
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:
|
||||
None
|
||||
|
||||
@@ -100,7 +100,7 @@ class SearchBox(QDialog):
|
||||
|
||||
def update_data(self):
|
||||
"""
|
||||
Shows dataframe of relevant samples.
|
||||
Shows dataframe of relevant sample.
|
||||
"""
|
||||
fields = self.parse_form()
|
||||
sample_list_creator = self.object_type.fuzzy_search(**fields)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
@@ -6,9 +5,12 @@ from PyQt6.QtCore import Qt, pyqtSlot
|
||||
from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
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
|
||||
|
||||
|
||||
env = jinja_template_loading()
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -16,9 +18,13 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
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)
|
||||
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.app = get_application_from_parent(parent)
|
||||
self.webview = QWebEngineView(parent=self)
|
||||
@@ -36,9 +42,10 @@ class SampleChecker(QDialog):
|
||||
css = f.read()
|
||||
try:
|
||||
samples = self.formatted_list
|
||||
except AttributeError:
|
||||
except AttributeError as e:
|
||||
logger.error(f"Problem getting sample list: {e}")
|
||||
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)
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
@@ -51,25 +58,37 @@ class SampleChecker(QDialog):
|
||||
@pyqtSlot(str, str, str)
|
||||
def text_changed(self, submission_rank: str, key: str, new_value: str):
|
||||
logger.debug(f"Name: {submission_rank}, Key: {key}, Value: {new_value}")
|
||||
match key:
|
||||
case "row" | "column":
|
||||
value = [new_value]
|
||||
case _:
|
||||
value = new_value
|
||||
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:
|
||||
logger.error(f"Unable to find sample {submission_rank}")
|
||||
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
|
||||
def formatted_list(self) -> List[dict]:
|
||||
output = []
|
||||
for sample in self.pyd.sample_list:
|
||||
if sample['submitter_id'] in [item['submitter_id'] for item in output]:
|
||||
sample['color'] = "red"
|
||||
for sample in self.samples:
|
||||
logger.debug(sample)
|
||||
s = sample.improved_dict(dictionaries=False)
|
||||
if s['sample_id'] in [item['sample_id'] for item in output]:
|
||||
s['color'] = "red"
|
||||
else:
|
||||
sample['color'] = "black"
|
||||
output.append(sample)
|
||||
s['color'] = "black"
|
||||
output.append(s)
|
||||
return output
|
||||
|
||||
@@ -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,
|
||||
QDialogButtonBox, QTextEdit, QGridLayout)
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtCore import Qt, pyqtSlot
|
||||
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 .functions import select_save_file, save_pdf
|
||||
from pathlib import Path
|
||||
@@ -18,15 +18,15 @@ from pprint import pformat
|
||||
from typing import List
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
|
||||
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)
|
||||
self.app = get_application_from_parent(parent)
|
||||
@@ -51,10 +51,10 @@ class SubmissionDetails(QDialog):
|
||||
self.channel = QWebChannel()
|
||||
self.channel.registerObject('backend', self)
|
||||
match sub:
|
||||
case BasicRun():
|
||||
case Run():
|
||||
self.run_details(run=sub)
|
||||
self.rsl_plate_num = sub.rsl_plate_num
|
||||
case BasicSample():
|
||||
case Sample():
|
||||
self.sample_details(sample=sub)
|
||||
case Reagent():
|
||||
self.reagent_details(reagent=sub)
|
||||
@@ -127,7 +127,7 @@ class SubmissionDetails(QDialog):
|
||||
self.setWindowTitle(f"Process Details - {tips.name}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sample_details(self, sample: str | BasicSample):
|
||||
def sample_details(self, sample: str | Sample):
|
||||
"""
|
||||
Changes details view to summary of Sample
|
||||
|
||||
@@ -136,19 +136,19 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
logger.debug(f"Sample details.")
|
||||
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)
|
||||
exclude = ['submissions', 'excluded', 'colour', 'tooltip']
|
||||
exclude = ['procedure', 'excluded', 'colour', 'tooltip']
|
||||
base_dict['excluded'] = exclude
|
||||
template = sample.details_template
|
||||
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
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)
|
||||
self.webview.setHtml(html)
|
||||
self.setWindowTitle(f"Sample Details - {sample.submitter_id}")
|
||||
self.setWindowTitle(f"Sample Details - {sample.sample_id}")
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
|
||||
@@ -156,7 +156,7 @@ class SubmissionDetails(QDialog):
|
||||
Changes details view to summary of Reagent
|
||||
|
||||
Args:
|
||||
kit (str | KitType): Name of kit.
|
||||
kit (str | KitType): Name of kittype.
|
||||
reagent (str | Reagent): Lot number of the reagent
|
||||
"""
|
||||
logger.debug(f"Reagent details.")
|
||||
@@ -164,7 +164,7 @@ class SubmissionDetails(QDialog):
|
||||
reagent = Reagent.query(lot=reagent)
|
||||
if isinstance(kit, str):
|
||||
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()
|
||||
temp_name = "reagent_details.html"
|
||||
try:
|
||||
@@ -203,7 +203,7 @@ class SubmissionDetails(QDialog):
|
||||
logger.error(f"Reagent with lot {old_lot} not found.")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def run_details(self, run: str | BasicRun):
|
||||
def run_details(self, run: str | Run):
|
||||
"""
|
||||
Sets details view to summary of Submission.
|
||||
|
||||
@@ -212,24 +212,24 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
logger.debug(f"Submission details.")
|
||||
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.base_dict = run.to_dict(full_data=True)
|
||||
# NOTE: don't want id
|
||||
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, 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])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
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.webview.setHtml(self.html)
|
||||
|
||||
@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:
|
||||
run (str | BasicRun): Submission to be completed
|
||||
@@ -239,7 +239,7 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
logger.info(f"Signing off on {run} - ({getuser()})")
|
||||
if isinstance(run, str):
|
||||
run = BasicRun.query(rsl_plate_num=run)
|
||||
run = Run.query(name=run)
|
||||
run.signed_by = getuser()
|
||||
run.completed_date = datetime.now()
|
||||
run.completed_date.replace(tzinfo=timezone)
|
||||
@@ -248,7 +248,7 @@ class SubmissionDetails(QDialog):
|
||||
|
||||
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")
|
||||
save_pdf(obj=self.webview, filename=fname)
|
||||
@@ -256,11 +256,11 @@ class SubmissionDetails(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)
|
||||
self.app = get_application_from_parent(parent)
|
||||
self.submission = submission
|
||||
@@ -282,7 +282,7 @@ class SubmissionComment(QDialog):
|
||||
|
||||
def parse_form(self) -> List[dict]:
|
||||
"""
|
||||
Adds comment to run object.
|
||||
Adds comment to procedure object.
|
||||
"""
|
||||
commenter = getuser()
|
||||
comment = self.txt_editor.toPlainText()
|
||||
|
||||
@@ -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 PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \
|
||||
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
|
||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
|
||||
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor
|
||||
from backend.db.models import BasicRun, ClientSubmission
|
||||
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent
|
||||
|
||||
from backend.db.models import Run, ClientSubmission
|
||||
from tools import Report, Result, report_result
|
||||
from .functions import select_open_file
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
|
||||
class pandasModel(QAbstractTableModel):
|
||||
@@ -63,7 +65,7 @@ class pandasModel(QAbstractTableModel):
|
||||
|
||||
class SubmissionsSheet(QTableView):
|
||||
"""
|
||||
presents run summary to user in tab1
|
||||
presents procedure summary to user in tab1
|
||||
"""
|
||||
|
||||
def __init__(self, parent) -> None:
|
||||
@@ -78,16 +80,16 @@ class SubmissionsSheet(QTableView):
|
||||
self.resizeColumnsToContents()
|
||||
self.resizeRowsToContents()
|
||||
self.setSortingEnabled(True)
|
||||
self.doubleClicked.connect(lambda x: BasicRun.query(id=x.sibling(x.row(), 0).data()).show_details(self))
|
||||
# NOTE: Have to run native query here because mine just returns results?
|
||||
self.total_count = BasicRun.__database_session__.query(BasicRun).count()
|
||||
self.doubleClicked.connect(lambda x: Run.query(id=x.sibling(x.row(), 0).data()).show_details(self))
|
||||
# NOTE: Have to procedure native query here because mine just returns results?
|
||||
self.total_count = Run.__database_session__.query(Run).count()
|
||||
|
||||
def set_data(self, page: int = 1, page_size: int = 250) -> None:
|
||||
"""
|
||||
sets data in model
|
||||
"""
|
||||
# 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:
|
||||
self.data['Id'] = self.data['Id'].apply(str)
|
||||
self.data['Id'] = self.data['Id'].str.zfill(4)
|
||||
@@ -108,7 +110,7 @@ class SubmissionsSheet(QTableView):
|
||||
id = self.selectionModel().currentIndex()
|
||||
# NOTE: Convert to data in id column (i.e. column 0)
|
||||
id = id.sibling(id.row(), 0).data()
|
||||
submission = BasicRun.query(id=id)
|
||||
submission = Run.query(id=id)
|
||||
self.menu = QMenu(self)
|
||||
self.con_actions = submission.custom_context_events()
|
||||
for k in self.con_actions.keys():
|
||||
@@ -140,7 +142,7 @@ class SubmissionsSheet(QTableView):
|
||||
|
||||
def link_extractions_function(self):
|
||||
"""
|
||||
Link extractions from runlogs to imported submissions
|
||||
Link extractions from runlogs to imported procedure
|
||||
|
||||
Args:
|
||||
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
|
||||
for ii in range(6, len(run)):
|
||||
new_run[f"column{str(ii - 5)}_vol"] = run[ii]
|
||||
# NOTE: Lookup imported submissions
|
||||
sub = BasicRun.query(rsl_plate_num=new_run['rsl_plate_num'])
|
||||
# NOTE: If no such run exists, move onto the next run
|
||||
# NOTE: Lookup imported procedure
|
||||
sub = Run.query(name=new_run['name'])
|
||||
# NOTE: If no such procedure exists, move onto the next procedure
|
||||
if sub is None:
|
||||
continue
|
||||
try:
|
||||
@@ -192,7 +194,7 @@ class SubmissionsSheet(QTableView):
|
||||
|
||||
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:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -215,9 +217,9 @@ class SubmissionsSheet(QTableView):
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# NOTE: lookup imported run
|
||||
sub = BasicRun.query(rsl_number=new_run['rsl_plate_num'])
|
||||
# NOTE: if imported run doesn't exist move on to next run
|
||||
# NOTE: lookup imported procedure
|
||||
sub = Run.query(rsl_number=new_run['name'])
|
||||
# NOTE: if imported procedure doesn't exist move on to next procedure
|
||||
if sub is None:
|
||||
continue
|
||||
sub.set_attribute('pcr_info', new_run)
|
||||
@@ -227,9 +229,10 @@ class SubmissionsSheet(QTableView):
|
||||
return report
|
||||
|
||||
|
||||
class RunDelegate(QStyledItemDelegate):
|
||||
class ClientSubmissionDelegate(QStyledItemDelegate):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(RunDelegate, self).__init__(parent)
|
||||
super(ClientSubmissionDelegate, self).__init__(parent)
|
||||
pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
|
||||
icon1 = QWidget().style().standardIcon(pixmapi)
|
||||
pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
|
||||
@@ -238,23 +241,29 @@ class RunDelegate(QStyledItemDelegate):
|
||||
self._minus_icon = icon2
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super(RunDelegate, self).initStyleOption(option, index)
|
||||
super(ClientSubmissionDelegate, self).initStyleOption(option, index)
|
||||
if not index.parent().isValid():
|
||||
is_open = bool(option.state & QStyle.StateFlag.State_Open)
|
||||
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
|
||||
option.icon = self._minus_icon if is_open else self._plus_icon
|
||||
|
||||
|
||||
class RunDelegate(ClientSubmissionDelegate):
|
||||
pass
|
||||
|
||||
|
||||
class SubmissionsTree(QTreeView):
|
||||
"""
|
||||
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):
|
||||
super(SubmissionsTree, self).__init__(parent)
|
||||
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
|
||||
self.setIndentation(0)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.clicked.connect(self.on_clicked)
|
||||
delegate = RunDelegate(self)
|
||||
delegate = ClientSubmissionDelegate(self)
|
||||
self.setItemDelegateForColumn(0, delegate)
|
||||
self.model = model
|
||||
self.setModel(self.model)
|
||||
@@ -263,32 +272,69 @@ class SubmissionsTree(QTreeView):
|
||||
# self.setStyleSheet("background-color: #0D1225;")
|
||||
self.set_data()
|
||||
self.doubleClicked.connect(self.show_details)
|
||||
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
# self.customContextMenuRequested.connect(self.open_menu)
|
||||
|
||||
for ii in range(2):
|
||||
self.resizeColumnToContents(ii)
|
||||
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def on_clicked(self, index):
|
||||
if not index.parent().isValid() and index.column() == 0:
|
||||
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:
|
||||
"""
|
||||
sets data in model
|
||||
"""
|
||||
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 ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
|
||||
logger.debug(pformat(self.data))
|
||||
self.data = [item.to_dict(full_data=True) for item in
|
||||
ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
|
||||
logger.debug(f"setting data:\n {pformat(self.data)}")
|
||||
# sys.exit()
|
||||
for submission in self.data:
|
||||
group_str = f"{submission['submission_type']}-{submission['submitter_plate_number']}-{submission['submitted_date']}"
|
||||
group_item = self.model.add_group(group_str)
|
||||
for run in submission['runs']:
|
||||
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
|
||||
group_item = self.model.add_group(group_str, query_str=submission['submitter_plate_id'])
|
||||
for run in submission['run']:
|
||||
self.model.append_element_to_group(group_item=group_item, element=run)
|
||||
|
||||
|
||||
def clear(self):
|
||||
if self.model != None:
|
||||
# self.model.clear() # works
|
||||
@@ -302,8 +348,7 @@ class SubmissionsTree(QTreeView):
|
||||
id = int(id.data())
|
||||
except ValueError:
|
||||
return
|
||||
BasicRun.query(id=id).show_details(self)
|
||||
|
||||
Run.query(id=id).show_details(self)
|
||||
|
||||
def link_extractions(self):
|
||||
pass
|
||||
@@ -312,62 +357,64 @@ class SubmissionsTree(QTreeView):
|
||||
pass
|
||||
|
||||
|
||||
class ClientRunModel(QStandardItemModel):
|
||||
class ClientSubmissionRunModel(QStandardItemModel):
|
||||
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ClientRunModel, self).__init__(parent)
|
||||
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Technician", "Signed By"]
|
||||
super(ClientSubmissionRunModel, self).__init__(parent)
|
||||
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
|
||||
self.setColumnCount(len(headers))
|
||||
self.setHorizontalHeaderLabels(headers)
|
||||
|
||||
for i in range(self.columnCount()):
|
||||
it = self.horizontalHeaderItem(i)
|
||||
try:
|
||||
logger.debug(it.text())
|
||||
except AttributeError:
|
||||
pass
|
||||
# it.setForeground(QColor("#F2F2F2"))
|
||||
|
||||
def add_group(self, group_name):
|
||||
def add_group(self, item_name, query_str: str):
|
||||
item_root = QStandardItem()
|
||||
item_root.setEditable(False)
|
||||
item = QStandardItem(group_name)
|
||||
item = QStandardItem(item_name)
|
||||
item.setEditable(False)
|
||||
ii = self.invisibleRootItem()
|
||||
i = ii.rowCount()
|
||||
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.setEditable(False)
|
||||
for j in range(self.columnCount()):
|
||||
it = ii.child(i, j)
|
||||
if it is None:
|
||||
# NOTE: Set invisible root child to empty if it is None.
|
||||
it = QStandardItem()
|
||||
ii.setChild(i, j, it)
|
||||
# it.setBackground(QColor("#002842"))
|
||||
# it.setForeground(QColor("#F2F2F2"))
|
||||
item_root.setData(dict(item_type=ClientSubmission, query_str=query_str), 1)
|
||||
return item_root
|
||||
|
||||
def append_element_to_group(self, group_item, element:dict):
|
||||
logger.debug(f"Element: {pformat(element)}")
|
||||
def append_element_to_group(self, group_item, element: dict):
|
||||
# logger.debug(f"Element: {pformat(element)}")
|
||||
j = group_item.rowCount()
|
||||
item_icon = QStandardItem()
|
||||
item_icon.setEditable(False)
|
||||
|
||||
# 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)
|
||||
for i in range(self.columnCount()):
|
||||
it = self.horizontalHeaderItem(i)
|
||||
try:
|
||||
key = it.text().lower().replace(" ", "_")
|
||||
except AttributeError:
|
||||
continue
|
||||
key = None
|
||||
if not key:
|
||||
continue
|
||||
value = str(element[key])
|
||||
item = QStandardItem(value)
|
||||
item.setBackground(QColor("#CFE2F3"))
|
||||
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, 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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Contains all run related frontend functions
|
||||
Contains all procedure related frontend functions
|
||||
"""
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout,
|
||||
@@ -10,11 +10,11 @@ from .functions import select_open_file, select_save_file
|
||||
import logging
|
||||
from pathlib import Path
|
||||
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.validators import PydSubmission, PydReagent
|
||||
from backend.excel import ClientSubmissionParser, SampleParser
|
||||
from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample
|
||||
from backend.db import (
|
||||
Organization, SubmissionType, Reagent,
|
||||
ReagentRole, KitTypeReagentRoleAssociation, BasicRun
|
||||
ClientLab, SubmissionType, Reagent,
|
||||
ReagentRole, KitTypeReagentRoleAssociation, Run
|
||||
)
|
||||
from pprint import pformat
|
||||
from .pop_ups import QuestionAsker, AlertPop
|
||||
@@ -93,7 +93,7 @@ class SubmissionFormContainer(QWidget):
|
||||
@report_result
|
||||
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:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -110,7 +110,7 @@ class SubmissionFormContainer(QWidget):
|
||||
self.form.setParent(None)
|
||||
except AttributeError:
|
||||
pass
|
||||
# NOTE: initialize samples
|
||||
# NOTE: initialize sample
|
||||
self.samples = []
|
||||
self.missing_info = []
|
||||
# NOTE: set file dialog
|
||||
@@ -121,19 +121,28 @@ class SubmissionFormContainer(QWidget):
|
||||
return report
|
||||
# NOTE: create sheetparser using excel sheet and context from gui
|
||||
try:
|
||||
# self.prsr = SheetParser(filepath=fname)
|
||||
self.parser = InfoParser(filepath=fname)
|
||||
self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
|
||||
except PermissionError:
|
||||
logger.error(f"Couldn't get permission to access file: {fname}")
|
||||
return
|
||||
except AttributeError:
|
||||
self.parser = InfoParser(filepath=fname)
|
||||
self.pyd = self.parser.to_pydantic()
|
||||
# logger.debug(f"Samples: {pformat(self.pyd.samples)}")
|
||||
checker = SampleChecker(self, "Sample Checker", self.pyd)
|
||||
self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
|
||||
try:
|
||||
# self.prsr = SheetParser(filepath=fname)
|
||||
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():
|
||||
# logger.debug(pformat(self.pyd.samples))
|
||||
self.form = self.pyd.to_form(parent=self)
|
||||
# logger.debug(pformat(self.pydclientsubmission.sample))
|
||||
self.form = self.pydclientsubmission.to_form(parent=self)
|
||||
self.form.samples = self.pydsamples
|
||||
self.layout().addWidget(self.form)
|
||||
else:
|
||||
message = "Submission cancelled."
|
||||
@@ -150,7 +159,7 @@ class SubmissionFormContainer(QWidget):
|
||||
instance (Reagent | None): Blank reagent instance to be edited and then added.
|
||||
|
||||
Returns:
|
||||
models.Reagent: the constructed reagent object to add to run
|
||||
models.Reagent: the constructed reagent object to add to procedure
|
||||
"""
|
||||
report = Report()
|
||||
if not instance:
|
||||
@@ -167,23 +176,23 @@ class SubmissionFormContainer(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)
|
||||
if disable is None:
|
||||
disable = []
|
||||
self.app = get_application_from_parent(parent)
|
||||
self.pyd = submission
|
||||
self.pyd = pyd
|
||||
self.missing_info = []
|
||||
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
|
||||
basic_submission_class = self.submission_type.submission_class
|
||||
logger.debug(f"Basic run class: {basic_submission_class}")
|
||||
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
|
||||
self.submissiontype = SubmissionType.query(name=self.pyd.submissiontype['value'])
|
||||
# basic_submission_class = self.submission_type.submission_class
|
||||
# logger.debug(f"Basic procedure class: {basic_submission_class}")
|
||||
defaults = Run.get_default_info("form_recover", "form_ignore", submissiontype=self.pyd.submissiontype['value'])
|
||||
self.recover = defaults['form_recover']
|
||||
self.ignore = defaults['form_ignore']
|
||||
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}")
|
||||
if k in self.ignore:
|
||||
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
|
||||
@@ -201,8 +210,8 @@ class SubmissionFormWidget(QWidget):
|
||||
except KeyError:
|
||||
value = dict(value=None, missing=True)
|
||||
logger.debug(f"Pydantic value: {value}")
|
||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
|
||||
run_object=basic_submission_class, disable=check)
|
||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype,
|
||||
run_object=Run(), disable=check)
|
||||
if add_widget is not None:
|
||||
self.layout.addWidget(add_widget)
|
||||
if k in self.__class__.update_reagent_fields:
|
||||
@@ -212,7 +221,7 @@ class SubmissionFormWidget(QWidget):
|
||||
self.layout.addWidget(self.disabler)
|
||||
self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
|
||||
self.setStyleSheet(main_form_style)
|
||||
# self.scrape_reagents(self.extraction_kit)
|
||||
# self.scrape_reagents(self.kittype)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def disable_reagents(self):
|
||||
@@ -223,7 +232,7 @@ class SubmissionFormWidget(QWidget):
|
||||
reagent.flip_check(self.disabler.checkbox.isChecked())
|
||||
|
||||
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":
|
||||
"""
|
||||
Make an InfoItem widget to hold a field
|
||||
@@ -256,14 +265,14 @@ class SubmissionFormWidget(QWidget):
|
||||
return None
|
||||
|
||||
@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
|
||||
form 'extraction_kit' widget is updated.
|
||||
Extracted scrape reagents function that will procedure when
|
||||
form 'kittype' widget is updated.
|
||||
|
||||
Args:
|
||||
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:
|
||||
Tuple[QMainWindow, dict]: Updated application and result
|
||||
@@ -373,7 +382,7 @@ class SubmissionFormWidget(QWidget):
|
||||
return report
|
||||
case _:
|
||||
pass
|
||||
# NOTE: add reagents to run object
|
||||
# NOTE: add reagents to procedure object
|
||||
if base_submission is None:
|
||||
return
|
||||
for reagent in base_submission.reagents:
|
||||
@@ -393,7 +402,7 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
def export_csv_function(self, fname: Path | None = None):
|
||||
"""
|
||||
Save the run's csv file.
|
||||
Save the procedure's csv file.
|
||||
|
||||
Args:
|
||||
fname (Path | None, optional): Input filename. Defaults to None.
|
||||
@@ -405,7 +414,7 @@ class SubmissionFormWidget(QWidget):
|
||||
except PermissionError:
|
||||
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
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:
|
||||
"""
|
||||
@@ -442,11 +451,10 @@ class SubmissionFormWidget(QWidget):
|
||||
report.add_result(report)
|
||||
return report
|
||||
|
||||
|
||||
class InfoItem(QWidget):
|
||||
|
||||
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)
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
@@ -492,7 +500,7 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
def set_widget(self, parent: QWidget, key: str, value: dict,
|
||||
submission_type: str | SubmissionType | None = None,
|
||||
sub_obj: BasicRun | None = None) -> QWidget:
|
||||
sub_obj: Run | None = None) -> QWidget:
|
||||
"""
|
||||
Creates form widget
|
||||
|
||||
@@ -515,16 +523,16 @@ class SubmissionFormWidget(QWidget):
|
||||
pass
|
||||
obj = parent.parent().parent()
|
||||
match key:
|
||||
case 'submitting_lab':
|
||||
case 'clientlab':
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
labs = [item.name for item in Organization.query()]
|
||||
# NOTE: lookup organizations suitable for clientlab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
labs = [item.name for item in ClientLab.query()]
|
||||
if isinstance(value, dict):
|
||||
value = value['value']
|
||||
if isinstance(value, Organization):
|
||||
if isinstance(value, ClientLab):
|
||||
value = value.name
|
||||
try:
|
||||
looked_up_lab = Organization.query(name=value, limit=1)
|
||||
looked_up_lab = ClientLab.query(name=value, limit=1)
|
||||
except AttributeError:
|
||||
looked_up_lab = None
|
||||
if looked_up_lab:
|
||||
@@ -536,28 +544,28 @@ class SubmissionFormWidget(QWidget):
|
||||
# NOTE: set combobox values to lookedup values
|
||||
add_widget.addItems(labs)
|
||||
add_widget.setToolTip("Select submitting lab.")
|
||||
case 'extraction_kit':
|
||||
# NOTE: if extraction kit not available, all other values fail
|
||||
case 'kittype':
|
||||
# NOTE: if extraction kittype not available, all other values fail
|
||||
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")
|
||||
msg.exec()
|
||||
# NOTE: create combobox to hold looked up kits
|
||||
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]
|
||||
obj.uses = uses
|
||||
if check_not_nan(value):
|
||||
try:
|
||||
uses.insert(0, uses.pop(uses.index(value)))
|
||||
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
|
||||
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]
|
||||
add_widget.addItems(uses)
|
||||
add_widget.setToolTip("Select extraction kit.")
|
||||
add_widget.setToolTip("Select extraction kittype.")
|
||||
parent.extraction_kit = add_widget.currentText()
|
||||
case 'submission_category':
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
@@ -568,7 +576,7 @@ class SubmissionFormWidget(QWidget):
|
||||
except ValueError:
|
||||
categories.insert(0, categories.pop(categories.index(submission_type)))
|
||||
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 _:
|
||||
if key in sub_obj.timestamps:
|
||||
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
|
||||
@@ -692,10 +700,10 @@ class SubmissionFormWidget(QWidget):
|
||||
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
|
||||
return wanted_reagent, report
|
||||
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
|
||||
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)
|
||||
if rt is None:
|
||||
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:
|
||||
super().__init__(scrollWidget=scrollWidget)
|
||||
self.setEditable(True)
|
||||
looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.role,
|
||||
looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.equipmentrole,
|
||||
kittype=extraction_kit)
|
||||
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
|
||||
@@ -754,7 +762,8 @@ class SubmissionFormWidget(QWidget):
|
||||
looked_up_reg = None
|
||||
if looked_up_reg:
|
||||
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:
|
||||
logger.error(f"Error reordering relevant reagents: {e}")
|
||||
else:
|
||||
@@ -764,9 +773,9 @@ class SubmissionFormWidget(QWidget):
|
||||
relevant_reagents.insert(0, moved_reag)
|
||||
else:
|
||||
pass
|
||||
self.setObjectName(f"lot_{reagent.role}")
|
||||
self.setObjectName(f"lot_{reagent.equipmentrole}")
|
||||
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):
|
||||
|
||||
@@ -783,16 +792,22 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
class ClientSubmissionFormWidget(SubmissionFormWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
|
||||
super().__init__(parent, submission=submission, disable=disable)
|
||||
self.disabler.setHidden(True)
|
||||
def __init__(self, parent: QWidget, clientsubmission: PydClientSubmission, samples: List = [],
|
||||
disable: list | None = None) -> None:
|
||||
super().__init__(parent, pyd=clientsubmission, disable=disable)
|
||||
try:
|
||||
self.disabler.setHidden(True)
|
||||
except AttributeError:
|
||||
pass
|
||||
# save_btn = QPushButton("Save")
|
||||
self.samples = samples
|
||||
logger.debug(f"Samples: {self.samples}")
|
||||
start_run_btn = QPushButton("Save")
|
||||
# self.layout.addWidget(save_btn)
|
||||
self.layout.addWidget(start_run_btn)
|
||||
start_run_btn.clicked.connect(self.create_new_submission)
|
||||
|
||||
|
||||
@report_result
|
||||
def parse_form(self) -> Report:
|
||||
"""
|
||||
Transforms form info into PydSubmission
|
||||
@@ -801,7 +816,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
|
||||
Report: Report on status of parse.
|
||||
"""
|
||||
report = Report()
|
||||
logger.info(f"Hello from client run form parser!")
|
||||
logger.info(f"Hello from client procedure form parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
for widget in self.findChildren(QWidget):
|
||||
@@ -827,18 +842,20 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
|
||||
report.add_result(report)
|
||||
return report
|
||||
|
||||
@report_result
|
||||
# @report_result
|
||||
def to_pydantic(self, *args):
|
||||
self.parse_form()
|
||||
return self.pyd
|
||||
|
||||
@report_result
|
||||
def create_new_submission(self, *args) -> Report:
|
||||
self.parse_form()
|
||||
sql = self.pyd.to_sql()
|
||||
pyd = self.to_pydantic()
|
||||
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__)
|
||||
sql.save()
|
||||
self.app.table_widget.sub_wid.set_data()
|
||||
self.setParent(None)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Pane to hold information e.g. cost summary.
|
||||
"""
|
||||
from .info_tab import InfoPane
|
||||
from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
|
||||
from backend.db import Organization
|
||||
from backend.db import ClientLab
|
||||
from backend.excel import ReportMaker
|
||||
from .misc import CheckableComboBox
|
||||
import logging
|
||||
@@ -24,7 +24,7 @@ class Summary(InfoPane):
|
||||
self.org_select = CheckableComboBox()
|
||||
self.org_select.setEditable(False)
|
||||
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.model().itemChanged.connect(self.update_data)
|
||||
self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1)
|
||||
|
||||
@@ -9,15 +9,22 @@
|
||||
{% block body %}
|
||||
<h2><u>Sample Checker</u></h2>
|
||||
<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>
|
||||
<br>
|
||||
<form>
|
||||
  Submitter ID              Row           Column<br/>
|
||||
  Submitter ID<br/><!--              Row           Column<br/>-->
|
||||
{% 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'] }}
|
||||
<input type="text" id="{{ sample['submission_rank'] }}_id" name="submitter_id" value="{{ sample['submitter_id'] }}" size="40" style="color:{{ sample['color'] }};">>
|
||||
<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="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'] }}_col" name="column" value="{{ sample['column'] }}" size="5", min="1">-->
|
||||
<br/>
|
||||
{% endfor %}
|
||||
</form>
|
||||
@@ -30,15 +37,23 @@
|
||||
document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){
|
||||
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
|
||||
});
|
||||
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);
|
||||
{% if rsl_plate_num %}
|
||||
document.getElementById("{{ sample['submission_rank'] }}_enabled").addEventListener("input", function(){
|
||||
backend.enable_sample("{{ sample['submission_rank'] }}", this.checked);
|
||||
});
|
||||
{% 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 %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
document.getElementById("rsl_plate_num").addEventListener("input", function(){
|
||||
backend.set_rsl_plate_num(this.value);
|
||||
});
|
||||
{% endblock %}
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@ from functools import wraps
|
||||
|
||||
timezone = tz("America/Winnipeg")
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
logger.info(f"Package dir: {project_path}")
|
||||
|
||||
@@ -41,7 +41,7 @@ else:
|
||||
os_config_dir = ".config"
|
||||
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")
|
||||
LOGDIR = main_aux_dir.joinpath("logs")
|
||||
@@ -343,7 +343,7 @@ class StreamToLogger(object):
|
||||
|
||||
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)
|
||||
self.extra_info = None
|
||||
ch = logging.StreamHandler(stream=sys.stdout)
|
||||
@@ -394,7 +394,7 @@ def setup_logger(verbosity: int = 3):
|
||||
return
|
||||
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
|
||||
|
||||
logger = logging.getLogger("submissions")
|
||||
logger = logging.getLogger("procedure")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
# NOTE: create file handler which logs even debug messages
|
||||
try:
|
||||
@@ -937,7 +937,7 @@ class Settings(BaseSettings, extra="allow"):
|
||||
else:
|
||||
os_config_dir = ".config"
|
||||
# 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
|
||||
def configdir(cls):
|
||||
@@ -955,12 +955,12 @@ class Settings(BaseSettings, extra="allow"):
|
||||
else:
|
||||
settings_path = 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():
|
||||
settings_path = cls.configdir.joinpath("config.yml")
|
||||
# NOTE: Check user .submissions directory
|
||||
elif Path.home().joinpath(".submissions", "config.yml").exists():
|
||||
settings_path = Path.home().joinpath(".submissions", "config.yml")
|
||||
# NOTE: Check user .procedure directory
|
||||
elif Path.home().joinpath(".procedure", "config.yml").exists():
|
||||
settings_path = Path.home().joinpath(".procedure", "config.yml")
|
||||
# NOTE: finally look in the local config
|
||||
else:
|
||||
if check_if_app():
|
||||
@@ -1275,7 +1275,7 @@ class Settings(BaseSettings, extra="allow"):
|
||||
logger.warning(f"Logging directory {self.configdir} already exists.")
|
||||
dicto = {}
|
||||
for k, v in self.__dict__.items():
|
||||
if k in ['package', 'database_session', 'submission_types']:
|
||||
if k in ['package', 'database_session', 'proceduretype']:
|
||||
continue
|
||||
match v:
|
||||
case Path():
|
||||
|
||||
Reference in New Issue
Block a user