Addition of procedure parser in import.

This commit is contained in:
lwark
2025-06-17 15:09:51 -05:00
parent 0233bc3ac2
commit d8c3f3bbb2
31 changed files with 688 additions and 304 deletions

View File

@@ -2,7 +2,8 @@
Contains all models for sqlalchemy
"""
from __future__ import annotations
import sys, logging
import sys, logging, json
from dateutil.parser import parse
from pandas import DataFrame
@@ -565,15 +566,24 @@ class BaseClass(Base):
check = False
if check:
continue
value = getattr(self, k)
try:
value = getattr(self, k)
except AttributeError:
continue
match value:
case datetime():
value = value.strftime("%Y-%m-%d %H:%M:%S")
case _:
pass
output[k] = value
output[k.strip("_")] = value
return output
def show_details(self, obj):
logger.debug("Show Details")
from frontend.widgets.submission_details import SubmissionDetails
dlg = SubmissionDetails(parent=obj, sub=self)
if dlg.exec():
pass
class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',

View File

@@ -2,19 +2,16 @@
All kittype and reagent related models
"""
from __future__ import annotations
import json, zipfile, yaml, logging, re, sys
import zipfile, logging, re
from operator import itemgetter
from pprint import pformat
import numpy as np
from jinja2 import Template, TemplateNotFound
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \
jinja_template_loading, ctx
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
jinja_template_loading
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from pandas import ExcelFile
from pathlib import Path
@@ -632,7 +629,7 @@ class Reagent(BaseClass, LogMixin):
missing=False
)
if full_data:
output['procedure'] = [sub.rsl_plate_num for sub in self.procedures]
output['procedure'] = [sub.rsl_plate_number for sub in self.procedures]
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
output['editable'] = ['lot', 'expiry']
return output
@@ -743,7 +740,7 @@ class Reagent(BaseClass, LogMixin):
role = ReagentRole.query(name=value, limit=1)
case _:
return
if role and role not in self.role:
if role and role not in self.reagentrole:
self.reagentrole.append(role)
return
case "comment":
@@ -1097,9 +1094,13 @@ class ProcedureType(BaseClass):
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
reagent_map = Column(JSON)
info_map = Column(JSON)
sample_map = Column(JSON)
equipment_map = Column(JSON)
plate_columns = Column(INTEGER, default=0)
plate_rows = Column(INTEGER, default=0)
allowed_result_methods = Column(JSON)
template_file = Column(BLOB)
procedure = relationship("Procedure",
back_populates="proceduretype") #: Concrete control of this type.
@@ -1218,6 +1219,15 @@ class ProcedureType(BaseClass):
plate_columns=self.plate_columns
)
def details_dict(self, **kwargs):
output = super().details_dict(**kwargs)
output['kittype'] = [item.details_dict() for item in output['kittype']]
# output['process'] = [item.details_dict() for item in output['process']]
output['equipment'] = [item.details_dict() for item in output['equipment']]
return output
def construct_dummy_procedure(self, run: Run|None=None):
from backend.validators.pydant import PydProcedure
if run:
@@ -1277,6 +1287,7 @@ class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True)
name = Column(String, unique=True)
repeat = Column(INTEGER, nullable=False)
technician = Column(String(64)) #: name of processing tech(s)
results = relationship("Results", back_populates="procedure", uselist=True)
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL",
@@ -1372,7 +1383,7 @@ class Procedure(BaseClass):
def add_results(self, obj, resultstype_name:str):
logger.debug(f"Add Results! {resultstype_name}")
from frontend.widgets import results
from ...managers import results
results_class = getattr(results, resultstype_name)
rs = results_class(procedure=self, parent=obj)
@@ -1412,8 +1423,8 @@ class Procedure(BaseClass):
def add_comment(self, obj):
logger.debug("Add Comment!")
def show_details(self, obj):
logger.debug("Show Details!")
# def show_details(self, obj):
# logger.debug("Show Details!")
def delete(self, obj):
logger.debug("Delete!")
@@ -1423,10 +1434,25 @@ class Procedure(BaseClass):
output['kittype'] = output['kittype'].details_dict()
output['proceduretype'] = output['proceduretype'].details_dict()
output['results'] = [result.details_dict() for result in output['results']]
output['sample'] = [sample.details_dict() for sample in output['sample']]
run_samples = [sample for sample in self.run.sample]
active_samples = [sample.details_dict() for sample in output['proceduresampleassociation']
if sample.sample.sample_id in [s.sample_id for s in run_samples]]
for sample in active_samples:
sample['active'] = True
inactive_samples = [sample.details_dict() for sample in run_samples if sample.name not in [s['sample_id'] for s in active_samples]]
# logger.debug(f"Inactive samples:{pformat(inactive_samples)}")
for sample in inactive_samples:
sample['active'] = False
# output['sample'] = [sample.details_dict() for sample in output['runsampleassociation']]
output['sample'] = active_samples + inactive_samples
# output['sample'] = [sample.details_dict() for sample in output['sample']]
output['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentassociation']]
output['equipment'] = [equipment.details_dict() for equipment in output['procedureequipmentassociation']]
output['tips'] = [tips.details_dict() for tips in output['proceduretipsassociation']]
output['repeat'] = bool(output['repeat'])
output['excluded'] = ['id', "results", "proceduresampleassociation", "sample", "procedurereagentassociation",
"procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", "tips",
"excluded"]
return output
class ProcedureTypeKitTypeAssociation(BaseClass):
@@ -1814,7 +1840,7 @@ class ProcedureReagentAssociation(BaseClass):
str: Representation of this RunReagentAssociation
"""
try:
return f"<ProcedureReagentAssociation({self.procedure.procedure.rsl_plate_num} & {self.reagent.lot})>"
return f"<ProcedureReagentAssociation({self.procedure.procedure.rsl_plate_number} & {self.reagent.lot})>"
except AttributeError:
logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!")
return f"<ProcedureReagentAssociation(Unknown Submission & {self.reagent.lot})>"
@@ -1887,9 +1913,9 @@ class ProcedureReagentAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['reagent']}
output = output['reagent'].details_dict()
misc = output['_misc_info']
misc = output['misc_info']
output.update(relevant)
output['_misc_info'] = misc
output['misc_info'] = misc
output['results'] = [result.details_dict() for result in output['results']]
return output
@@ -2087,9 +2113,9 @@ class Equipment(BaseClass, LogMixin):
asset_number=self.asset_number
)
if full_data:
subs = [dict(plate=item.procedure.procedure.rsl_plate_num, process=item.process.name,
subs = [dict(plate=item.procedure.procedure.rsl_plate_number, process=item.process.name,
sub_date=item.procedure.procedure.start_date)
if item.process else dict(plate=item.procedure.procedure.rsl_plate_num, process="NA")
if item.process else dict(plate=item.procedure.procedure.rsl_plate_number, process="NA")
for item in self.equipmentprocedureassociation]
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True)
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
@@ -2240,6 +2266,11 @@ class EquipmentRole(BaseClass):
from backend.validators.omni_gui_objects import OmniEquipmentRole
return OmniEquipmentRole(instance_object=self, name=self.name)
def details_dict(self, **kwargs):
output = super().details_dict(**kwargs)
output['equipment'] = [item.details_dict() for item in output['equipment']]
output['process'] = [item.details_dict() for item in output['process']]
return output
class ProcedureEquipmentAssociation(BaseClass):
"""
@@ -2342,9 +2373,9 @@ class ProcedureEquipmentAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['equipment']}
output = output['equipment'].details_dict()
misc = output['_misc_info']
misc = output['misc_info']
output.update(relevant)
output['_misc_info'] = misc
output['misc_info'] = misc
output['process'] = self.process.details_dict()
return output
@@ -2530,12 +2561,25 @@ class Process(BaseClass):
name=self.name,
)
if full_data:
subs = [dict(plate=sub.run.rsl_plate_num, equipment=sub.equipment.name,
subs = [dict(plate=sub.run.rsl_plate_number, equipment=sub.equipment.name,
submitted_date=sub.run.clientsubmission.submitted_date) for sub in self.procedure]
output['procedure'] = sorted(subs, key=itemgetter("submitted_date"), reverse=True)
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
return output
def to_pydantic(self):
from backend.validators.pydant import PydProcess
output = {}
for k, v in self.details_dict().items():
if isinstance(v, list):
output[k] = [item.name for item in v]
elif issubclass(v.__class__, BaseClass):
output[k] = v.name
else:
output[k] = v
return PydProcess(**output)
# @classproperty
# def details_template(cls) -> Template:
# """
@@ -2591,7 +2635,10 @@ class TipRole(BaseClass):
@classmethod
@setup_lookup
def query(cls, name: str | None = None, limit: int = 0, **kwargs) -> TipRole | List[TipRole]:
def query(cls,
name: str | None = None,
limit: int = 0,
**kwargs) -> TipRole | List[TipRole]:
query = cls.__database_session__.query(cls)
match name:
case str():
@@ -2707,7 +2754,7 @@ class Tips(BaseClass, LogMixin):
)
if full_data:
subs = [
dict(plate=item.procedure.procedure.rsl_plate_num, role=item.role_name,
dict(plate=item.procedure.procedure.rsl_plate_number, role=item.role_name,
sub_date=item.procedure.procedure.clientsubmission.submitted_date)
for item in self.tipsprocedureassociation]
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True)
@@ -2819,15 +2866,16 @@ class ProcedureTipsAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['tips']}
output = output['tips'].details_dict()
misc = output['_misc_info']
misc = output['misc_info']
output.update(relevant)
output['_misc_info'] = misc
output['misc_info'] = misc
return output
class Results(BaseClass):
id = Column(INTEGER, primary_key=True)
result_type = Column(String(32))
result = Column(JSON)
procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL',
name="fk_RES_procedure_id"))

View File

@@ -26,7 +26,7 @@ from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as S
from openpyxl import Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
report_result, create_holidays_for_year, check_dictionary_inclusion_equality
report_result, create_holidays_for_year, check_dictionary_inclusion_equality, is_power_user
from datetime import datetime, date
from typing import List, Any, Tuple, Literal, Generator, Type, TYPE_CHECKING
from pathlib import Path
@@ -100,7 +100,7 @@ class ClientSubmission(BaseClass, LogMixin):
Args:
submission_type (str | models.SubmissionType | None, optional): Submission type of interest. Defaults to None.
id (int | str | None, optional): Submission id in the database (limits results to 1). Defaults to None.
rsl_plate_num (str | None, optional): Submission name in the database (limits results to 1). Defaults to None.
rsl_plate_number (str | None, optional): Submission name in the database (limits results to 1). Defaults to None.
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None.
end_date (date | str | int | None, optional): Ending date to search by. Defaults to None.
reagent (models.Reagent | str | None, optional): A reagent used in the procedure. Defaults to None.
@@ -304,7 +304,7 @@ class ClientSubmission(BaseClass, LogMixin):
samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation]
checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self)
if checker.exec():
run = Run(clientsubmission=self, rsl_plate_num=checker.rsl_plate_num)
run = Run(clientsubmission=self, rsl_plate_number=checker.rsl_plate_number)
active_samples = [sample for sample in samples if sample.enabled]
logger.debug(active_samples)
for sample in active_samples:
@@ -323,8 +323,12 @@ class ClientSubmission(BaseClass, LogMixin):
def add_comment(self, obj):
logger.debug("Add Comment")
def show_details(self, obj):
logger.debug("Show Details")
# def show_details(self, obj):
# logger.debug("Show Details")
# from frontend.widgets.submission_details import SubmissionDetails
# dlg = SubmissionDetails(parent=obj, sub=self)
# if dlg.exec():
# pass
def details_dict(self, **kwargs):
output = super().details_dict(**kwargs)
@@ -334,6 +338,11 @@ class ClientSubmission(BaseClass, LogMixin):
output['run'] = [run.details_dict() for run in output['run']]
output['sample'] = [sample.details_dict() for sample in output['clientsubmissionsampleassociation']]
output['name'] = self.name
output['client_lab'] = output['clientlab']
output['submission_type'] = output['submissiontype']
output['excluded'] = ['run', "sample", "clientsubmissionsampleassociation", "excluded",
"expanded", 'clientlab', 'submissiontype', 'id']
output['expanded'] = ["clientlab", "contact", "submissiontype"]
return output
@@ -343,7 +352,7 @@ class Run(BaseClass, LogMixin):
"""
id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
rsl_plate_number = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL",
name="fk_BS_clientsub_id")) #: client lab id from _organizations)
clientsubmission = relationship("ClientSubmission", back_populates="run")
@@ -387,7 +396,7 @@ class Run(BaseClass, LogMixin):
@hybrid_property
def name(self):
return self.rsl_plate_num
return self.rsl_plate_number
@classmethod
def get_default_info(cls, *args, submissiontype: SubmissionType | None = None) -> dict:
@@ -593,8 +602,23 @@ class Run(BaseClass, LogMixin):
def details_dict(self, **kwargs):
output = super().details_dict()
output['sample'] = [sample.details_dict() for sample in output['runsampleassociation']]
submission_samples = [sample for sample in self.clientsubmission.sample]
# logger.debug(f"Submission samples:{pformat(submission_samples)}")
active_samples = [sample.details_dict() for sample in output['runsampleassociation']
if sample.sample.sample_id in [s.sample_id for s in submission_samples]]
# logger.debug(f"Active samples:{pformat(active_samples)}")
for sample in active_samples:
sample['active'] = True
inactive_samples = [sample.details_dict() for sample in submission_samples if sample.name not in [s['sample_id'] for s in active_samples]]
# logger.debug(f"Inactive samples:{pformat(inactive_samples)}")
for sample in inactive_samples:
sample['active'] = False
# output['sample'] = [sample.details_dict() for sample in output['runsampleassociation']]
output['sample'] = active_samples + inactive_samples
output['procedure'] = [procedure.details_dict() for procedure in output['procedure']]
output['permission'] = is_power_user()
output['excluded'] = ['procedure', "runsampleassociation", 'excluded', 'expanded', 'sample', 'id', 'custom', 'permission']
return output
@classmethod
@@ -890,10 +914,10 @@ class Run(BaseClass, LogMixin):
field_value = dict(value=self.__getattribute__(key).name, missing=missing)
case "plate_number":
key = 'name'
field_value = dict(value=self.rsl_plate_num, missing=missing)
field_value = dict(value=self.rsl_plate_number, missing=missing)
case "submitter_plate_number":
key = "submitter_plate_id"
field_value = dict(value=self.submitter_plate_num, missing=missing)
field_value = dict(value=self.submitter_plate_number, missing=missing)
case "id":
continue
case _:
@@ -1168,8 +1192,8 @@ class Run(BaseClass, LogMixin):
e: SQLIntegrityError or SQLOperationalError if problem with commit.
"""
from frontend.widgets.pop_ups import QuestionAsker
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {self.rsl_plate_num}?\n")
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_number}-backup({date.today().strftime('%Y%m%d')})")
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {self.rsl_plate_number}?\n")
if msg.exec():
try:
# NOTE: backs up file as xlsx, same as export.
@@ -1187,17 +1211,17 @@ class Run(BaseClass, LogMixin):
except AttributeError:
logger.error("App will not refresh data at this time.")
def show_details(self, obj):
"""
Creates Widget for showing procedure details.
Args:
obj (Widget): Parent widget
"""
from frontend.widgets.submission_details import SubmissionDetails
dlg = SubmissionDetails(parent=obj, sub=self)
if dlg.exec():
pass
# def show_details(self, obj):
# """
# Creates Widget for showing procedure details.
#
# Args:
# obj (Widget): Parent widget
# """
# from frontend.widgets.submission_details import SubmissionDetails
# dlg = SubmissionDetails(parent=obj, sub=self)
# if dlg.exec():
# pass
def edit(self, obj):
"""
@@ -1641,12 +1665,12 @@ class ClientSubmissionSampleAssociation(BaseClass):
output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['sample']}
logger.debug(f"Relevant info from assoc output: {pformat(relevant)}")
# logger.debug(f"Relevant info from assoc output: {pformat(relevant)}")
output = output['sample'].details_dict()
misc = output['_misc_info']
logger.debug(f"Output from sample: {pformat(output)}")
misc = output['misc_info']
# logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant)
output['_misc_info'] = misc
output['misc_info'] = misc
# output['sample'] = temp
# output.update(output['sample'].details_dict())
return output
@@ -1815,7 +1839,7 @@ class ClientSubmissionSampleAssociation(BaseClass):
case ClientSubmission():
pass
case str():
clientsubmission = ClientSubmission.query(rsl_plate_num=clientsubmission)
clientsubmission = ClientSubmission.query(rsl_plate_number=clientsubmission)
case _:
raise ValueError()
match sample:
@@ -1879,7 +1903,7 @@ class RunSampleAssociation(BaseClass):
def __repr__(self) -> str:
try:
return f"<{self.__class__.__name__}({self.run.rsl_plate_num} & {self.sample.sample_id})"
return f"<{self.__class__.__name__}({self.run.rsl_plate_number} & {self.sample.sample_id})"
except AttributeError as e:
logger.error(f"Unable to construct __repr__ due to: {e}")
return super().__repr__()
@@ -1901,7 +1925,7 @@ class RunSampleAssociation(BaseClass):
except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.")
sample['Well'] = None
sample['plate_name'] = self.run.rsl_plate_num
sample['plate_name'] = self.run.rsl_plate_number
sample['positive'] = False
return sample
@@ -1975,7 +1999,7 @@ class RunSampleAssociation(BaseClass):
case Run():
query = query.filter(cls.run == run)
case str():
query = query.join(Run).filter(Run.rsl_plate_num == run)
query = query.join(Run).filter(Run.rsl_plate_number == run)
case _:
pass
match sample:
@@ -2060,12 +2084,12 @@ class RunSampleAssociation(BaseClass):
output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['sample']}
logger.debug(f"Relevant info from assoc output: {pformat(relevant)}")
# logger.debug(f"Relevant info from assoc output: {pformat(relevant)}")
output = output['sample'].details_dict()
misc = output['_misc_info']
logger.debug(f"Output from sample: {pformat(output)}")
misc = output['misc_info']
# logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant)
output['_misc_info'] = misc
output['misc_info'] = misc
return output
class ProcedureSampleAssociation(BaseClass):
@@ -2132,9 +2156,9 @@ class ProcedureSampleAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['sample']}
output = output['sample'].details_dict()
misc = output['_misc_info']
misc = output['misc_info']
output.update(relevant)
output['_misc_info'] = misc
output['misc_info'] = misc
output['results'] = [result.details_dict() for result in output['results']]
return output

View File

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

View File

@@ -630,12 +630,12 @@ class PCRParser(object):
return None
if submission is None:
self.submission_obj = Wastewater
rsl_plate_num = None
rsl_plate_number = None
else:
self.submission_obj = submission
rsl_plate_num = self.submission_obj.rsl_plate_num
self.samples = self.submission_obj.parse_pcr(xl=self.xl, rsl_plate_num=rsl_plate_num)
self.controls = self.submission_obj.parse_pcr_controls(xl=self.xl, rsl_plate_num=rsl_plate_num)
rsl_plate_number = self.submission_obj.rsl_plate_number
self.samples = self.submission_obj.parse_pcr(xl=self.xl, rsl_plate_number=rsl_plate_number)
self.controls = self.submission_obj.parse_pcr_controls(xl=self.xl, rsl_plate_number=rsl_plate_number)
@property
def pcr_info(self) -> dict:
@@ -675,11 +675,11 @@ class ConcentrationParser(object):
return None
if run is None:
self.submission_obj = Run()
rsl_plate_num = None
rsl_plate_number = None
else:
self.submission_obj = run
rsl_plate_num = self.submission_obj.rsl_plate_num
self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_num=rsl_plate_num)
rsl_plate_number = self.submission_obj.rsl_plate_number
self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_number=rsl_plate_number)
# NOTE: Generified parsers below

View File

@@ -1,14 +1,15 @@
"""
"""
from __future__ import annotations
import logging, re
from pathlib import Path
from typing import Generator, Tuple
from openpyxl import load_workbook
from typing import Generator, Tuple, TYPE_CHECKING
from pandas import DataFrame
from backend.validators import pydant
from backend.db.models import Procedure
from dataclasses import dataclass
if TYPE_CHECKING:
from backend.db.models import ProcedureType
logger = logging.getLogger(f"submissions.{__name__}")
@@ -30,7 +31,7 @@ class DefaultParser(object):
return instance
def __init__(self, filepath: Path | str, procedure: Procedure|None=None, range_dict: dict | None = None, *args, **kwargs):
def __init__(self, filepath: Path | str, proceduretype: ProcedureType|None=None, range_dict: dict | None = None, *args, **kwargs):
"""
Args:
@@ -40,7 +41,7 @@ class DefaultParser(object):
*args ():
**kwargs ():
"""
self.procedure = procedure
self.proceduretype = proceduretype
try:
self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}")
except AttributeError:
@@ -58,6 +59,13 @@ class DefaultParser(object):
data['filepath'] = self.filepath
return self._pyd_object(**data)
@classmethod
def correct_procedure_type(cls, proceduretype: str | "ProcedureType"):
from backend.db.models import ProcedureType
if isinstance(proceduretype, str):
proceduretype = ProcedureType.query(name=proceduretype)
return proceduretype
class DefaultKEYVALUEParser(DefaultParser):
@@ -90,7 +98,6 @@ class DefaultTABLEParser(DefaultParser):
default_range_dict = [dict(
header_row=20,
end_row=116,
sheet="Sample List"
)]
@@ -98,15 +105,25 @@ class DefaultTABLEParser(DefaultParser):
def parsed_info(self):
for item in self.range_dict:
list_worksheet = self.workbook[item['sheet']]
list_df = DataFrame([item for item in list_worksheet.values][item['header_row'] - 1:])
if "end_row" in item.keys():
list_df = DataFrame([item for item in list_worksheet.values][item['header_row'] - 1:item['end_row']-1])
else:
list_df = DataFrame([item for item in list_worksheet.values][item['header_row'] - 1:])
list_df.columns = list_df.iloc[0]
list_df = list_df[1:]
list_df = list_df.dropna(axis=1, how='all')
for ii, row in enumerate(list_df.iterrows()):
output = {key.lower().replace(" ", "_"): value for key, value in row[1].to_dict().items()}
output = {}
for key, value in row[1].to_dict().items():
if isinstance(key, str):
key = key.lower().replace(" ", "_")
key = re.sub(r"_(\(.*\)|#)", "", key)
logger.debug(f"Row {ii} values: {key}: {value}")
output[key] = value
yield output
def to_pydantic(self, **kwargs):
return [self._pyd_object(**output) for output in self.parsed_info]
from .submission_parser import *
from .clientsubmission_parser import *
from backend.excel.parsers.results_parsers.pcr_results_parser import *

View File

@@ -1,6 +1,7 @@
"""
"""
from __future__ import annotations
import logging
from pathlib import Path
from string import ascii_lowercase
@@ -9,8 +10,9 @@ from typing import Generator
from openpyxl.reader.excel import load_workbook
from tools import row_keys
from backend.db.models import SubmissionType
# from backend.db.models import SubmissionType
from . import DefaultKEYVALUEParser, DefaultTABLEParser
from backend.managers import procedures as procedure_managers
logger = logging.getLogger(f"submissions.{__name__}")
@@ -34,6 +36,7 @@ class SubmissionTyperMixin(object):
@classmethod
def get_subtype_from_regex(cls, filepath: Path):
from backend.db.models import SubmissionType
regex = SubmissionType.regex
m = regex.search(filepath.__str__())
try:
@@ -45,7 +48,8 @@ class SubmissionTyperMixin(object):
@classmethod
def get_subtype_from_preparse(cls, filepath: Path):
parser = ClientSubmissionParser(filepath)
from backend.db.models import SubmissionType
parser = ClientSubmissionInfoParser(filepath)
sub_type = next((value for k, value in parser.parsed_info if k == "submissiontype"), None)
sub_type = SubmissionType.query(name=sub_type)
if isinstance(sub_type, list):
@@ -54,6 +58,7 @@ class SubmissionTyperMixin(object):
@classmethod
def get_subtype_from_properties(cls, filepath: Path):
from backend.db.models import SubmissionType
wb = load_workbook(filepath)
# NOTE: Gets first category in the metadata.
categories = wb.properties.category.split(";")
@@ -64,7 +69,7 @@ class SubmissionTyperMixin(object):
return sub_type
class ClientSubmissionParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
class ClientSubmissionInfoParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
"""
Object for retrieving submitter info from "sample list" sheet
"""
@@ -78,13 +83,29 @@ class ClientSubmissionParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
)]
def __init__(self, filepath: Path | str, *args, **kwargs):
from frontend.widgets.pop_ups import QuestionAsker
self.submissiontype = self.retrieve_submissiontype(filepath=filepath)
if "range_dict" not in kwargs:
kwargs['range_dict'] = self.submissiontype.info_map
super().__init__(filepath=filepath, **kwargs)
allowed_procedure_types = [item.name for item in self.submissiontype.proceduretype]
for name in allowed_procedure_types:
if name in self.workbook.sheetnames:
# TODO: check if run with name already exists
add_run = QuestionAsker(title="Add Run?", message="We've detected a sheet corresponding to an associated procedure type.\nWould you like to add a new run?")
if add_run.accepted:
class ClientSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
# NOTE: recruit parser.
try:
manager = getattr(procedure_managers, name)
except AttributeError:
manager = procedure_managers.DefaultManager
self.manager = manager(proceduretype=name)
pass
class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
"""
Object for retrieving submitter samples from "sample list" sheet
"""

View File

@@ -0,0 +1,119 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from backend.excel.parsers import DefaultTABLEParser, DefaultKEYVALUEParser
if TYPE_CHECKING:
from backend.db.models import ProcedureType
class DefaultInfoParser(DefaultKEYVALUEParser):
default_range_dict = [dict(
start_row=1,
end_row=14,
key_column=1,
value_column=2,
sheet=""
)]
def __init__(self, filepath: Path | str, proceduretype: "ProcedureType"|None=None, range_dict: dict | None = None, *args, **kwargs):
from backend.validators.pydant import PydProcedure
proceduretype = self.correct_procedure_type(proceduretype)
if not range_dict:
range_dict = proceduretype.info_map
if not range_dict:
range_dict = self.__class__.default_range_dict
for item in range_dict:
item['sheet'] = proceduretype.name
super().__init__(filepath=filepath, proceduretype=proceduretype, range_dict=range_dict, *args, **kwargs)
self._pyd_object = PydProcedure
class DefaultSampleParser(DefaultTABLEParser):
default_range_dict = [dict(
header_row=41,
sheet=""
)]
def __init__(self, filepath: Path | str, proceduretype: "ProcedureType"|None=None, range_dict: dict | None = None, *args, **kwargs):
from backend.validators.pydant import PydSample
proceduretype = self.correct_procedure_type(proceduretype)
if not range_dict:
range_dict = proceduretype.sample_map
if not range_dict:
range_dict = self.__class__.default_range_dict
for item in range_dict:
item['sheet'] = proceduretype.name
super().__init__(filepath=filepath, procedure=proceduretype, range_dict=range_dict, *args, **kwargs)
self._pyd_object = PydSample
class DefaultReagentParser(DefaultTABLEParser):
default_range_dict = [dict(
header_row=17,
end_row=29,
sheet=""
)]
def __init__(self, filepath: Path | str, proceduretype: "ProcedureType"|None=None, range_dict: dict | None = None, *args, **kwargs):
from backend.validators.pydant import PydReagent
proceduretype = self.correct_procedure_type(proceduretype)
if not range_dict:
range_dict = proceduretype.sample_map
if not range_dict:
range_dict = self.__class__.default_range_dict
for item in range_dict:
item['sheet'] = proceduretype.name
super().__init__(filepath=filepath, proceduretype=proceduretype, range_dict=range_dict, *args, **kwargs)
self._pyd_object = PydReagent
@property
def parsed_info(self):
output = super().parsed_info
for item in output:
if not item['lot']:
continue
item['reagentrole'] = item['reagent_role']
yield item
class DefaultEquipmentParser(DefaultTABLEParser):
default_range_dict = [dict(
header_row=32,
end_row=39,
sheet=""
)]
def __init__(self, filepath: Path | str, proceduretype: "ProcedureType"|None=None, range_dict: dict | None = None, *args, **kwargs):
from backend.validators.pydant import PydEquipment
proceduretype = self.correct_procedure_type(proceduretype)
if not range_dict:
range_dict = proceduretype.sample_map
if not range_dict:
range_dict = self.__class__.default_range_dict
for item in range_dict:
item['sheet'] = proceduretype.name
super().__init__(filepath=filepath, proceduretype=proceduretype, range_dict=range_dict, *args, **kwargs)
self._pyd_object = PydEquipment
@property
def parsed_info(self):
output = super().parsed_info
for item in output:
if not item['name']:
continue
from backend.db.models import Equipment, Process
from backend.validators.pydant import PydTips, PydProcess
eq = Equipment.query(name=item['name'])
item['asset_number'] = eq.asset_number
item['nickname'] = eq.nickname
process = Process.query(name=item['process'])
if item['tips']:
item['tips'] = [PydTips(name=item['tips'], tiprole=process.tiprole[0].name)]
item['equipmentrole'] = item['equipment_role']
yield item

View File

@@ -1,19 +1,14 @@
"""
"""
import logging, re, sys
from pprint import pformat
from pathlib import Path
from typing import Generator, Tuple
from openpyxl import load_workbook
import logging
from backend.db.models import Run, Sample, Procedure, ProcedureSampleAssociation
from . import DefaultKEYVALUEParser, DefaultTABLEParser
from backend.excel.parsers import DefaultKEYVALUEParser, DefaultTABLEParser
logger = logging.getLogger(f"submissions.{__name__}")
# class PCRResultsParser(DefaultParser):
# pass
class PCRInfoParser(DefaultKEYVALUEParser):
default_range_dict = [dict(

View File

@@ -191,7 +191,7 @@ class TurnaroundMaker(ReportArchetype):
tat_ok = days <= tat
except TypeError:
return {}
return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date,
return dict(name=str(sub.rsl_plate_number), days=days, submitted_date=sub.submitted_date,
completed_date=sub.completed_date, acceptable=tat_ok)

View File

@@ -0,0 +1,22 @@
import logging
from pathlib import Path
from backend.db.models import ProcedureType
from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent
logger = logging.getLogger(f"submissions.{__name__}")
class DefaultManager(object):
def __init__(self, proceduretype: ProcedureType, parent, fname: Path | str | None = None):
logger.debug(f"FName before correction: {fname}")
if isinstance(proceduretype, str):
proceduretype = ProcedureType.query(name=proceduretype)
self.proceduretype = proceduretype
if fname != "no_file":
if not fname:
self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent))
elif isinstance(fname, str):
self.fname = Path(fname)
logger.debug(f"FName after correction: {fname}")

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import logging
from backend.managers import DefaultManager
from typing import TYPE_CHECKING
from pathlib import Path
from backend.excel.parsers import procedure_parsers
if TYPE_CHECKING:
from backend.db.models import ProcedureType
logger = logging.getLogger(f"submissions.{__name__}")
class DefaultProcedure(DefaultManager):
def __init__(self, proceduretype: "ProcedureType"|str, parent, fname: Path | str | None = None):
super().__init__(proceduretype=proceduretype, parent=parent, fname=fname)
try:
info_parser = getattr(procedure_parsers, f"{self.proceduretype.name.replace(' ', '')}InfoParser")
except AttributeError:
info_parser = procedure_parsers.DefaultInfoParser
self.info_parser = info_parser(filepath=fname, proceduretype=proceduretype)
try:
reagent_parser = getattr(procedure_parsers, f"{self.proceduretype.name.replace(' ', '')}ReagentParser")
except AttributeError:
reagent_parser = procedure_parsers.DefaultReagentParser
self.reagent_parser = reagent_parser(filepath=fname, proceduretype=proceduretype)
try:
sample_parser = getattr(procedure_parsers, f"{self.proceduretype.name.replace(' ', '')}SampleParser")
except AttributeError:
sample_parser = procedure_parsers.DefaultSampleParser
self.sample_parser = sample_parser(filepath=fname, proceduretype=proceduretype)
try:
equipment_parser = getattr(procedure_parsers, f"{self.proceduretype.name.replace(' ', '')}EquipmentParser")
except AttributeError:
equipment_parser = procedure_parsers.DefaultEquipmentParser
self.equipment_parser = equipment_parser(filepath=fname, proceduretype=proceduretype)
self.to_pydantic()
def to_pydantic(self):
self.procedure = self.info_parser.to_pydantic()
self.reagents = self.reagent_parser.to_pydantic()
self.samples = self.sample_parser.to_pydantic()
self.equipment = self.equipment_parser.to_pydantic()

View File

@@ -0,0 +1,21 @@
import logging
from .. import DefaultManager
from backend.db.models import Procedure
from pathlib import Path
from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent
logger = logging.getLogger(f"submission.{__name__}")
class DefaultResults(DefaultManager):
def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None):
logger.debug(f"FName before correction: {fname}")
self.procedure = procedure
if not fname:
self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent))
elif isinstance(fname, str):
self.fname = Path(fname)
logger.debug(f"FName after correction: {fname}")
from .pcr_results_manager import PCR

View File

@@ -3,11 +3,8 @@
"""
import logging
from pathlib import Path
from backend.validators import PydResults
from backend.db.models import Procedure, Results
from backend.excel.parsers.pcr_parser import PCRSampleParser, PCRInfoParser
from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent
from backend.db.models import Procedure
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser
from . import DefaultResults
logger = logging.getLogger(f"submissions.{__name__}")
@@ -15,25 +12,21 @@ logger = logging.getLogger(f"submissions.{__name__}")
class PCR(DefaultResults):
def __init__(self, procedure: Procedure, parent, fname:Path|str|None=None):
logger.debug(f"FName before correction: {fname}")
self.procedure = procedure
if not fname:
self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent))
elif isinstance(fname, str):
self.fname = Path(fname)
logger.debug(f"FName after correction: {fname}")
super().__init__(procedure=procedure, parent=parent, fname=fname)
self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure)
self.sample_parser = PCRSampleParser(filepath=self.fname, procedure=self.procedure)
self.build_procedure()
self.build_info()
self.build_samples()
def build_procedure(self):
def build_info(self):
procedure_info = self.info_parser.to_pydantic()
procedure_info.results_type = self.__class__.__name__
procedure_sql = procedure_info.to_sql()
procedure_sql.save()
def build_samples(self):
samples = self.sample_parser.to_pydantic()
for sample in samples:
sample.results_type = self.__class__.__name__
sql = sample.to_sql()
sql.save()

View File

@@ -59,8 +59,8 @@ class ClientSubmissionNamer(DefaultNamer):
def get_subtype_from_preparse(self):
from backend.excel.parsers.submission_parser import ClientSubmissionParser
parser = ClientSubmissionParser(self.filepath)
from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser
parser = ClientSubmissionInfoParser(self.filepath)
sub_type = next((value for k, value in parser.parsed_info if k == "submissiontype"), None)
sub_type = SubmissionType.query(name=sub_type)
if isinstance(sub_type, list):

View File

@@ -262,6 +262,12 @@ class PydSample(PydBaseClass):
pass
return value
@field_validator("row", mode="before")
@classmethod
def str_to_int(cls, value):
if isinstance(value, str):
value = row_keys[value]
return value
class PydTips(BaseModel):
name: str
@@ -298,7 +304,7 @@ class PydEquipment(BaseModel, extra='ignore'):
asset_number: str
name: str
nickname: str | None
processes: List[str] | None
process: List[str] | None
equipmentrole: str | None
tips: List[PydTips] | None = Field(default=None)
@@ -309,7 +315,7 @@ class PydEquipment(BaseModel, extra='ignore'):
value = value.name
return value
@field_validator('processes', mode='before')
@field_validator('process', mode='before')
@classmethod
def make_empty_list(cls, value):
# if isinstance(value, dict):
@@ -397,7 +403,7 @@ class PydSubmission(BaseModel, extra='allow'):
filepath: Path
submissiontype: dict | None
submitter_plate_id: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
rsl_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
rsl_plate_number: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
submitted_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True)
clientlab: dict | None
sample_count: dict | None
@@ -517,14 +523,14 @@ class PydSubmission(BaseModel, extra='allow'):
value['value'] = None
return value
@field_validator("rsl_plate_num", mode='before')
@field_validator("rsl_plate_number", mode='before')
@classmethod
def rescue_rsl_number(cls, value):
if value is None:
return dict(value=None, missing=True)
return value
@field_validator("rsl_plate_num")
@field_validator("rsl_plate_number")
@classmethod
def rsl_from_file(cls, value, values):
sub_type = values.data['proceduretype']['value']
@@ -689,7 +695,7 @@ class PydSubmission(BaseModel, extra='allow'):
# NOTE: this could also be done with default_factory
self.submission_object = Run.find_polymorphic_subclass(
polymorphic_identity=self.submission_type['value'])
self.namer = RSLNamer(self.rsl_plate_num['value'], submission_type=self.submission_type['value'])
self.namer = RSLNamer(self.rsl_plate_number['value'], submission_type=self.submission_type['value'])
if run_custom:
self.submission_object.custom_validation(pyd=self)
@@ -796,7 +802,7 @@ class PydSubmission(BaseModel, extra='allow'):
# logger.debug(f"Pydantic procedure type: {self.proceduretype['value']}")
# logger.debug(f"Pydantic improved_dict: {pformat(dicto)}")
instance, result = Run.query_or_create(submissiontype=self.submission_type['value'],
rsl_plate_num=self.rsl_plate_num['value'])
rsl_plate_number=self.rsl_plate_number['value'])
# logger.debug(f"Created or queried instance: {instance}")
if instance is None:
report.add_result(Result(msg="Overwrite Cancelled."))
@@ -1266,13 +1272,13 @@ class PydEquipmentRole(BaseModel):
class PydProcess(BaseModel, extra="allow"):
name: str
version: str = Field(default="1")
submissiontype: List[str]
proceduretype: List[str]
equipment: List[str]
equipmentrole: List[str]
kittype: List[str]
tiprole: List[str]
@field_validator("submissiontype", "equipment", "equipmentrole", "kittype", "tiprole", mode="before")
@field_validator("proceduretype", "equipment", "equipmentrole", "kittype", "tiprole", mode="before")
@classmethod
def enforce_list(cls, value):
if not isinstance(value, list):
@@ -1361,10 +1367,10 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
else:
procedure_type = None
if values.data['run']:
run = values.data['run'].rsl_plate_num
run = values.data['run'].rsl_plate_number
else:
run = None
value['value'] = f"{procedure_type}-{run}"
value['value'] = f"{run}-{procedure_type}"
value['missing'] = True
return value
@@ -1391,7 +1397,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
if not value:
if values.data['kittype']['value'] != cls.model_fields['kittype'].default['value']:
kittype = KitType.query(name=values.data['kittype']['value'])
value = {item.name: item.reagents for item in kittype.reagentrole}
value = {item.name: item.reagent for item in kittype.reagentrole}
return value
def update_kittype_reagentroles(self, kittype: str | KitType):
@@ -1545,11 +1551,13 @@ class PydClientSubmission(PydBaseClass):
class PydResults(PydBaseClass, arbitrary_types_allowed=True):
results: dict = Field(default={})
img: None = Field(default=None)
results_type: str = Field(default="NA")
img: None | bytes = Field(default=None)
parent: Procedure|ProcedureSampleAssociation|None = Field(default=None)
def to_sql(self):
sql = Results(result=self.results)
sql = Results(results_type=self.results_type, result=self.results)
sql.image = self.img
match self.parent:
case ProcedureSampleAssociation():
sql.sampleprocedureassociation = self.parent

View File

@@ -8,8 +8,8 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView
from tools import jinja_template_loading
import logging
from backend.db import models
from typing import Literal
from typing import Literal, Any
logger = logging.getLogger(f"submissions.{__name__}")
@@ -70,7 +70,8 @@ class ObjectSelector(QDialog):
dialog to input BaseClass type manually
"""
def __init__(self, title: str, message: str, obj_type: str | type[models.BaseClass], values: list | None = None):
def __init__(self, title: str, message: str, obj_type: str | Any, values: list | None = None):
from backend.db import models
super().__init__()
self.setWindowTitle(title)
self.widget = QComboBox()

View File

@@ -29,7 +29,7 @@ class ProcedureCreation(QDialog):
super().__init__(parent)
self.run = run
self.proceduretype = proceduretype
self.setWindowTitle(f"New {proceduretype.name} for { run.rsl_plate_num }")
self.setWindowTitle(f"New {proceduretype.name} for { run.rsl_plate_number }")
self.created_procedure = self.proceduretype.construct_dummy_procedure(run=self.run)
self.created_procedure.update_kittype_reagentroles(kittype=self.created_procedure.possible_kits[0])
self.created_procedure.samples = self.run.constuct_sample_dicts_for_proceduretype(proceduretype=self.proceduretype)
@@ -65,8 +65,8 @@ class ProcedureCreation(QDialog):
template_name="procedure_creation",
# css_in=['new_context_menu'],
js_in=["procedure_form", "grid_drag", "context_menu"],
proceduretype=self.proceduretype.as_dict,
run=self.run.to_dict(),
proceduretype=self.proceduretype.details_dict(),
run=self.run.details_dict(),
procedure=self.created_procedure.__dict__,
plate_map=self.plate_map
)

View File

@@ -1,7 +0,0 @@
class DefaultResults(object):
pass
from .pcr import PCR

View File

@@ -21,9 +21,9 @@ class SampleChecker(QDialog):
def __init__(self, parent, title: str, samples: List[PydSample], clientsubmission: ClientSubmission|None=None):
super().__init__(parent)
if clientsubmission:
self.rsl_plate_num = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
self.rsl_plate_number = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
else:
self.rsl_plate_num = clientsubmission
self.rsl_plate_number = clientsubmission
self.samples = samples
self.setWindowTitle(title)
self.app = get_application_from_parent(parent)
@@ -45,7 +45,7 @@ class SampleChecker(QDialog):
except AttributeError as e:
logger.error(f"Problem getting sample list: {e}")
samples = []
html = template.render(samples=samples, css=css, rsl_plate_num=self.rsl_plate_num)
html = template.render(samples=samples, css=css, rsl_plate_number=self.rsl_plate_number)
self.webview.setHtml(html)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
@@ -76,9 +76,9 @@ class SampleChecker(QDialog):
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
def set_rsl_plate_number(self, rsl_plate_number: str):
logger.debug(f"RSL plate num: {rsl_plate_number}")
self.rsl_plate_number = rsl_plate_number
@property
def formatted_list(self) -> List[dict]:

View File

@@ -50,17 +50,33 @@ class SubmissionDetails(QDialog):
# NOTE: setup channel
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
match sub:
case Run():
self.run_details(run=sub)
self.rsl_plate_num = sub.rsl_plate_num
case Sample():
self.sample_details(sample=sub)
case Reagent():
self.reagent_details(reagent=sub)
# match sub:
# case Run():
# self.run_details(run=sub)
# self.rsl_plate_number = sub.rsl_plate_number
# case Sample():
# self.sample_details(sample=sub)
# case Reagent():
# self.reagent_details(reagent=sub)
# NOTE: Used to maintain javascript functions.
self.object_details(object=sub)
self.webview.page().setWebChannel(self.channel)
def object_details(self, object):
details = object.details_dict()
template = object.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
key = object.__class__.__name__.lower()
d = {key: details}
logger.debug(f"Using details: {d}")
html = template.render(**d, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}")
def activate_export(self) -> None:
"""
Determines if export pdf should be active.
@@ -213,7 +229,7 @@ class SubmissionDetails(QDialog):
logger.debug(f"Submission details.")
if isinstance(run, str):
run = Run.query(name=run)
self.rsl_plate_num = run.rsl_plate_num
self.rsl_plate_number = run.rsl_plate_number
self.base_dict = run.to_dict(full_data=True)
# NOTE: don't want id
self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked)
@@ -244,7 +260,7 @@ class SubmissionDetails(QDialog):
run.completed_date = datetime.now()
run.completed_date.replace(tzinfo=timezone)
run.save()
self.run_details(run=self.rsl_plate_num)
self.run_details(run=self.rsl_plate_number)
def save_pdf(self):
"""
@@ -264,7 +280,7 @@ class SubmissionComment(QDialog):
super().__init__(parent)
self.app = get_application_from_parent(parent)
self.submission = submission
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
self.setWindowTitle(f"{self.submission.rsl_plate_number} Submission Comment")
# NOTE: create text field
self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False)

View File

@@ -161,7 +161,7 @@ class SubmissionsSheet(QTableView):
for run in runs:
new_run = dict(
start_time=run[0].strip(),
rsl_plate_num=run[1].strip(),
rsl_plate_number=run[1].strip(),
sample_count=run[2].strip(),
status=run[3].strip(),
experiment_name=run[4].strip(),
@@ -213,7 +213,7 @@ class SubmissionsSheet(QTableView):
for run in runs:
new_run = dict(
start_time=run[0].strip(),
rsl_plate_num=run[1].strip(),
rsl_plate_number=run[1].strip(),
biomek_status=run[2].strip(),
quant_status=run[3].strip(),
experiment_name=run[4].strip(),
@@ -379,7 +379,7 @@ class SubmissionsTree(QTreeView):
query_str=submission['submitter_plate_id'],
item_type=ClientSubmission
))
logger.debug(f"Added {submission_item}")
# logger.debug(f"Added {submission_item}")
for run in submission['run']:
# self.model.append_element_to_group(group_item=group_item, element=run)
run_item = self.model.add_child(parent=submission_item, child=dict(
@@ -387,14 +387,14 @@ class SubmissionsTree(QTreeView):
query_str=run['plate_number'],
item_type=Run
))
logger.debug(f"Added {run_item}")
# logger.debug(f"Added {run_item}")
for procedure in run['procedures']:
procedure_item = self.model.add_child(parent=run_item, child=dict(
name=procedure['name'],
query_str=procedure['name'],
item_type=Procedure
))
logger.debug(f"Added {procedure_item}")
# logger.debug(f"Added {procedure_item}")
def _populateTree(self, children, parent):
for child in children:
@@ -415,7 +415,6 @@ class SubmissionsTree(QTreeView):
# id = id.sibling(id.row(), 1)
indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1)))
logger.debug(dicto)
# try:
# id = int(id.data())
# except ValueError:
@@ -423,6 +422,7 @@ class SubmissionsTree(QTreeView):
# Run.query(id=id).show_details(self)
obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(obj)
obj.show_details(obj)
def link_extractions(self):
pass

View File

@@ -10,7 +10,7 @@ from .functions import select_open_file, select_save_file
import logging
from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.excel import ClientSubmissionParser, ClientSampleParser
from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser
from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample
from backend.db import (
ClientLab, SubmissionType, Reagent,
@@ -121,20 +121,20 @@ class SubmissionFormContainer(QWidget):
return report
# NOTE: create sheetparser using excel sheet and context from gui
try:
self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}")
return
except AttributeError:
self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
try:
# self.prsr = SheetParser(filepath=fname)
self.sampleparser = ClientSampleParser(filepath=fname)
self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}")
return
except AttributeError:
self.sampleparser = ClientSampleParser(filepath=fname)
self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
self.pydsamples = self.sampleparser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
@@ -368,7 +368,7 @@ class SubmissionFormWidget(QWidget):
pass
# NOTE: code 1: ask for overwrite
case 1:
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=trigger.msg)
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_number}?", message=trigger.msg)
if dlg.exec():
# NOTE: Do not add duplicate reagents.
pass

View File

@@ -0,0 +1,37 @@
{% extends "details.html"%}
<head>
{% block head %}
{{ super() }}
<title>ClientSubmission Details for {{ clientsubmission['name'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Submission Details for {{ clientsubmission['name'] }}</u></h2>
{{ super() }}
<p>{% for key, value in clientsubmission.items() if key not in clientsubmission['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Id", "ID") }}: </b>{% if key=='cost' %}{% if clientsubmission['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
{% endfor %}
</p>
{% if clientsubmission['sample'] %}
<button type="button" class="collapsible"><h3><u>Client Submitted Samples:</u></h3></button>
<div class="nested">
<p>{% for sample in clientsubmission['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="data-link sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p>
</div>
{% endif %}
{% if clientsubmission['run'] %}
<button type="button" class="collapsible"><h3><u>Runs:</u></h3></button>
<div class="nested">
{% for run in clientsubmission['run'] %}
{% with run=run, child=True %}
{% include "run_details.html" %}
{% endwith %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
</body>

View File

@@ -146,3 +146,36 @@ ul.no-bullets {
background-color: pink;
}
/* */
.nested {
margin-left: 50px;
padding: 0 18px;
display: none;
overflow: hidden;
background-color: #f1f1f1;
}
/* Style the button that is used to open and close the collapsible content */
.collapsible {
background-color: #eee;
color: #444;
cursor: pointer;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .collapsible:hover {
background-color: #ccc;
}
.unused {
color: red;
text-decoration-line: line-through;
text-decoration-color: red;
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
{% if not child %}
<html lang="en">
<head>
{% block head %}
@@ -14,17 +14,41 @@
{% endblock %}
</head>
<body>
{% endif %}
{% block body %}
<!--<button type="button" id="back_btn">Back</button>-->
{% endblock %}
{% block signing_button %}{% endblock %}
{% if not child %}
</body>
{% endif %}
{% block script %}
{% if not child %}
<script>
var coll = document.getElementsByClassName("collapsible");
var i;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
} else {
content.style.display = "block";
}
});
}
</script>
{% endif %}
{% for j in js%}
<script>
{{ j }}
</script>
{% endfor %}
{% endblock %}
</html>
{% if not child %}
</html>
{% endif %}

View File

@@ -28,13 +28,24 @@
<br><hr><br>
{% for key, value in procedure['reagentrole'].items() %}
<label for="{{ key }}">{{ key }}:</label><br>
<select class="reagentrole dropdown" id="{{ key }}" name="{{ reagentrole }}"><br>
<datalist class="reagentrole dropdown" id="{{ key }}" name="{{ reagentrole }}"><br>
{% for reagent in value %}
<option value="{{ reagent }}">{{ reagent }}</option>
{% endfor %}
</select>
</datalist>
{% endfor %}
{% endif %}
{% if proceduretype['equipment'] %}
<br><hr><br>
{% for equipmentrole in proceduretype['equipment'] %}
<label for="{{ equipmentrole['name'] }}">{{ equipmentrole['name'] }}:</label><br>
<select class="equipmentrole dropdown" id="{{ equipmentrole['name'] }}" name="{{ equipmentrole['name'] }}"><br>
{% for equipment in equipmentrole['equipment'] %}
<option value="{{ equipment['name'] }}">{{ equipment['name'] }}</option>
{% endfor %}
</select>
{% endfor %}
{% endif%}
</form>
</div>
<div class="right">

View File

@@ -1,4 +1,5 @@
{% extends "details.html" %}
{% if not child %}
<head>
{% block head %}
@@ -7,9 +8,31 @@
{% endblock %}
</head>
<body>
{% endif %}
{% block body %}
<h2><u>Procedure Details for {{ procedure['name'] }}</u></h2>
{{ super() }}
<p>{% for key, value in procedure.items() if key not in procedure['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: {{ value }}</b><br>
{% endfor %}</p>
{% if procedure['results'] %}
<button type="button" class="collapsible"><h3><u>Results:</u></h3></button>
<div class="nested">
{% for result in procedure['results'] %}
<p>{% for k, v in result['result'].items() %}
<b>{{ key | replace("_", " ") | title | replace("Rsl", "RSL") }}:</b> {{ value }}<br>
{% endfor %}</p>
{% endfor %}
</div>
{% endif %}
{% if procedure['sample'] %}
<button type="button" class="collapsible"><h3><u>Procedure Samples:</u></h3></button>
<div class="nested">
<p>{% for sample in procedure['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p>
</div>
{% endif %}
{% endblock %}
</body>
</body>

View File

@@ -1,135 +1,59 @@
{% extends "details.html" %}
<html>
<head>
{% block head %}
{{ super() }}
<title>Submission Details for {{ sub['plate_number'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Submission Details for {{ sub['plate_number'] }}</u></h2>&nbsp;&nbsp;&nbsp;{% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
{{ super() }}
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
{% endfor %}
{% if sub['custom'] %}{% for key, value in sub['custom'].items() %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
{% endfor %}{% endif %}</p>
{% if sub['reagents'] %}
<h3><u>Reagents:</u></h3>
<p>{% for item in sub['reagents'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> <a class="data-link reagent" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
{% endfor %}</p>
{% endif %}
{% if sub['equipment'] %}
<h3><u>Equipment:</u></h3>
<p>{% for item in sub['equipment'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> <a class="data-link equipment" id="{{ item['name'] }}"> {{ item['name'] }} ({{ item['asset_number'] }})</a>: <a class="data-link process" id="{{ item['processes'][0]|replace('\n\t', '') }}">{{ item['processes'][0]|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}</a><br>
{% endfor %}</p>
{% endif %}
{% if sub['tips'] %}
<h3><u>Tips:</u></h3>
<p>{% for item in sub['tips'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> <a class="data-link tips" id="{{ item['lot'] }}">{{ item['name'] }} ({{ item['lot'] }})</a><br>
{% endfor %}</p>
{% endif %}
{% if sub['samples'] %}
<h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b><a class="data-link sample" id="{{ item['submitter_id'] }}">{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}</a><br>
{% endfor %}</p>
{% endif %}
<head>
{% block head %}
{{ super() }}
<title>Run Details for {{ run['rsl_plate_number'] }}</title>
{% endblock %}
</head>
<body>
{% if sub['ext_info'] %}
{% for entry in sub['ext_info'] %}
<h3><u>Extraction Status:</u></h3>
<p>{% for key, value in entry.items() %}
{% if "column" in key %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}uL<br>
{% else %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br>
{% endif %}
{% endfor %}</p>
{% endfor %}
{% endif %}
{% block body %}
<h2><u>Run Details for {{ run['rsl_plate_number'] }}</u></h2>
{{ super() }}
<p>{% for key, value in run.items() if key not in run['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Rsl", "RSL") }}:</b> {{ value }}<br>
{% endfor %}</p>
{% if run['sample'] %}
<button type="button" class="collapsible"><h3><u>Run Samples:</u></h3></button>
<p>{% for sample in run['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p>
{% endif %}
{% if run['procedure'] %}
<button type="button" class="collapsible"><h3><u>Procedures:</u></h3></button>
<div class="nested">
{% for procedure in run['procedure'] %}
{% with procedure=procedure, child=True %}
{% include "procedure_details.html" %}
{% endwith %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% block signing_button %}
<button type="button" id="sign_btn" {% if run['permission'] and not run['signed_by'] %}{% else %}hidden{% endif %}>Sign Off</button>
{% endblock %}
<br>
{% if sub['comment'] %}
<h3><u>Comments:</u></h3>
<p>{% for entry in sub['comment'] %}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
{% endfor %}</p>
{% endif %}
{% if sub['platemap'] %}
<h3><u>Plate map:</u></h3>
{{ sub['platemap'] }}
{% endif %}
{% if sub['export_map'] %}
<h3><u>Plate map:</u></h3>
<img height="600px" width="1300px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}">
{% endif %}
{% endblock %}
{% block signing_button %}
<button type="button" id="sign_btn" {% if permission and not sub['signed_by'] %}{% else %}hidden{% endif %}>Sign Off</button>
{% endblock %}
<br>
<br>
<br>
</body>
</body>
{% block script %}
{{ super() }}
<script>
var sampleSelection = document.getElementsByClassName('sample');
{% block script %}
{{ super() }}
<script>
var sampleSelection = document.getElementsByClassName('sample');
for(let i = 0; i < sampleSelection.length; i++) {
sampleSelection[i].addEventListener("click", function() {
console.log(sampleSelection[i].id);
backend.sample_details(sampleSelection[i].id);
})
}
for(let i = 0; i < sampleSelection.length; i++) {
sampleSelection[i].addEventListener("click", function() {
console.log(sampleSelection[i].id);
backend.sample_details(sampleSelection[i].id);
})
}
var reagentSelection = document.getElementsByClassName('reagent');
document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ run['rsl_plate_num'] }}");
});
</script>
{% endblock %}
for(let i = 0; i < reagentSelection.length; i++) {
reagentSelection[i].addEventListener("click", function() {
console.log(reagentSelection[i].id);
backend.reagent_details(reagentSelection[i].id, "{{ sub['extraction_kit'] }}");
})
}
var equipmentSelection = document.getElementsByClassName('equipment');
for(let i = 0; i < equipmentSelection.length; i++) {
equipmentSelection[i].addEventListener("click", function() {
console.log(equipmentSelection[i].id);
backend.equipment_details(equipmentSelection[i].id);
})
}
var processSelection = document.getElementsByClassName('process');
for(let i = 0; i < processSelection.length; i++) {
processSelection[i].addEventListener("click", function() {
console.log(processSelection[i].id);
backend.process_details(processSelection[i].id);
})
}
var tipsSelection = document.getElementsByClassName('tips');
for(let i = 0; i < tipsSelection.length; i++) {
tipsSelection[i].addEventListener("click", function() {
console.log(tipsSelection[i].id);
backend.tips_details(tipsSelection[i].id);
})
}
document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}");
});
</script>
{% endblock %}
</html>

View File

@@ -10,8 +10,8 @@
<h2><u>Sample Checker</u></h2>
<br>
{% if rsl_plate_num %}
<label for="rsl_plate_num">RSL Plate Number:</label><br>
<input type="text" id="rsl_plate_num" name="sample_id" value="{{ rsl_plate_num }}" size="40">
<label for="rsl_plate_number">RSL Plate Number:</label><br>
<input type="text" id="rsl_plate_number" name="sample_id" value="{{ rsl_plate_number }}" size="40">
{% endif %}
<br>