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