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 Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging
import sys, logging, json
from dateutil.parser import parse from dateutil.parser import parse
from pandas import DataFrame from pandas import DataFrame
@@ -565,15 +566,24 @@ class BaseClass(Base):
check = False check = False
if check: if check:
continue continue
try:
value = getattr(self, k) value = getattr(self, k)
except AttributeError:
continue
match value: match value:
case datetime(): case datetime():
value = value.strftime("%Y-%m-%d %H:%M:%S") value = value.strftime("%Y-%m-%d %H:%M:%S")
case _: case _:
pass pass
output[k] = value output[k.strip("_")] = value
return output 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): class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation', tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',

View File

@@ -2,19 +2,16 @@
All kittype and reagent related models All kittype and reagent related models
""" """
from __future__ import annotations from __future__ import annotations
import json, zipfile, yaml, logging, re, sys import zipfile, logging, re
from operator import itemgetter from operator import itemgetter
from pprint import pformat
import numpy as np import numpy as np
from jinja2 import Template, TemplateNotFound
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \ from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
jinja_template_loading, ctx jinja_template_loading
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path from pathlib import Path
@@ -632,7 +629,7 @@ class Reagent(BaseClass, LogMixin):
missing=False missing=False
) )
if full_data: 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['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
output['editable'] = ['lot', 'expiry'] output['editable'] = ['lot', 'expiry']
return output return output
@@ -743,7 +740,7 @@ class Reagent(BaseClass, LogMixin):
role = ReagentRole.query(name=value, limit=1) role = ReagentRole.query(name=value, limit=1)
case _: case _:
return return
if role and role not in self.role: if role and role not in self.reagentrole:
self.reagentrole.append(role) self.reagentrole.append(role)
return return
case "comment": case "comment":
@@ -1097,9 +1094,13 @@ class ProcedureType(BaseClass):
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
name = Column(String(64)) name = Column(String(64))
reagent_map = Column(JSON) reagent_map = Column(JSON)
info_map = Column(JSON)
sample_map = Column(JSON)
equipment_map = Column(JSON)
plate_columns = Column(INTEGER, default=0) plate_columns = Column(INTEGER, default=0)
plate_rows = Column(INTEGER, default=0) plate_rows = Column(INTEGER, default=0)
allowed_result_methods = Column(JSON) allowed_result_methods = Column(JSON)
template_file = Column(BLOB)
procedure = relationship("Procedure", procedure = relationship("Procedure",
back_populates="proceduretype") #: Concrete control of this type. back_populates="proceduretype") #: Concrete control of this type.
@@ -1218,6 +1219,15 @@ class ProcedureType(BaseClass):
plate_columns=self.plate_columns 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): def construct_dummy_procedure(self, run: Run|None=None):
from backend.validators.pydant import PydProcedure from backend.validators.pydant import PydProcedure
if run: if run:
@@ -1277,6 +1287,7 @@ class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
name = Column(String, unique=True) name = Column(String, unique=True)
repeat = Column(INTEGER, nullable=False) repeat = Column(INTEGER, nullable=False)
technician = Column(String(64)) #: name of processing tech(s) technician = Column(String(64)) #: name of processing tech(s)
results = relationship("Results", back_populates="procedure", uselist=True) results = relationship("Results", back_populates="procedure", uselist=True)
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL", 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): def add_results(self, obj, resultstype_name:str):
logger.debug(f"Add Results! {resultstype_name}") logger.debug(f"Add Results! {resultstype_name}")
from frontend.widgets import results from ...managers import results
results_class = getattr(results, resultstype_name) results_class = getattr(results, resultstype_name)
rs = results_class(procedure=self, parent=obj) rs = results_class(procedure=self, parent=obj)
@@ -1412,8 +1423,8 @@ class Procedure(BaseClass):
def add_comment(self, obj): def add_comment(self, obj):
logger.debug("Add Comment!") logger.debug("Add Comment!")
def show_details(self, obj): # def show_details(self, obj):
logger.debug("Show Details!") # logger.debug("Show Details!")
def delete(self, obj): def delete(self, obj):
logger.debug("Delete!") logger.debug("Delete!")
@@ -1423,10 +1434,25 @@ class Procedure(BaseClass):
output['kittype'] = output['kittype'].details_dict() output['kittype'] = output['kittype'].details_dict()
output['proceduretype'] = output['proceduretype'].details_dict() output['proceduretype'] = output['proceduretype'].details_dict()
output['results'] = [result.details_dict() for result in output['results']] 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['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentassociation']]
output['equipment'] = [equipment.details_dict() for equipment in output['procedureequipmentassociation']] output['equipment'] = [equipment.details_dict() for equipment in output['procedureequipmentassociation']]
output['tips'] = [tips.details_dict() for tips in output['proceduretipsassociation']] 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 return output
class ProcedureTypeKitTypeAssociation(BaseClass): class ProcedureTypeKitTypeAssociation(BaseClass):
@@ -1814,7 +1840,7 @@ class ProcedureReagentAssociation(BaseClass):
str: Representation of this RunReagentAssociation str: Representation of this RunReagentAssociation
""" """
try: 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: except AttributeError:
logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!") logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!")
return f"<ProcedureReagentAssociation(Unknown Submission & {self.reagent.lot})>" 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. # 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']} relevant = {k: v for k, v in output.items() if k not in ['reagent']}
output = output['reagent'].details_dict() output = output['reagent'].details_dict()
misc = output['_misc_info'] misc = output['misc_info']
output.update(relevant) output.update(relevant)
output['_misc_info'] = misc output['misc_info'] = misc
output['results'] = [result.details_dict() for result in output['results']] output['results'] = [result.details_dict() for result in output['results']]
return output return output
@@ -2087,9 +2113,9 @@ class Equipment(BaseClass, LogMixin):
asset_number=self.asset_number asset_number=self.asset_number
) )
if full_data: 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) 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] for item in self.equipmentprocedureassociation]
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True) output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True)
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
@@ -2240,6 +2266,11 @@ class EquipmentRole(BaseClass):
from backend.validators.omni_gui_objects import OmniEquipmentRole from backend.validators.omni_gui_objects import OmniEquipmentRole
return OmniEquipmentRole(instance_object=self, name=self.name) 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): class ProcedureEquipmentAssociation(BaseClass):
""" """
@@ -2342,9 +2373,9 @@ class ProcedureEquipmentAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead. # 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']} relevant = {k: v for k, v in output.items() if k not in ['equipment']}
output = output['equipment'].details_dict() output = output['equipment'].details_dict()
misc = output['_misc_info'] misc = output['misc_info']
output.update(relevant) output.update(relevant)
output['_misc_info'] = misc output['misc_info'] = misc
output['process'] = self.process.details_dict() output['process'] = self.process.details_dict()
return output return output
@@ -2530,12 +2561,25 @@ class Process(BaseClass):
name=self.name, name=self.name,
) )
if full_data: 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] submitted_date=sub.run.clientsubmission.submitted_date) for sub in self.procedure]
output['procedure'] = sorted(subs, key=itemgetter("submitted_date"), reverse=True) output['procedure'] = sorted(subs, key=itemgetter("submitted_date"), reverse=True)
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
return output 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 # @classproperty
# def details_template(cls) -> Template: # def details_template(cls) -> Template:
# """ # """
@@ -2591,7 +2635,10 @@ class TipRole(BaseClass):
@classmethod @classmethod
@setup_lookup @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) query = cls.__database_session__.query(cls)
match name: match name:
case str(): case str():
@@ -2707,7 +2754,7 @@ class Tips(BaseClass, LogMixin):
) )
if full_data: if full_data:
subs = [ 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) sub_date=item.procedure.procedure.clientsubmission.submitted_date)
for item in self.tipsprocedureassociation] for item in self.tipsprocedureassociation]
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True) 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. # 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']} relevant = {k: v for k, v in output.items() if k not in ['tips']}
output = output['tips'].details_dict() output = output['tips'].details_dict()
misc = output['_misc_info'] misc = output['misc_info']
output.update(relevant) output.update(relevant)
output['_misc_info'] = misc output['misc_info'] = misc
return output return output
class Results(BaseClass): class Results(BaseClass):
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
result_type = Column(String(32))
result = Column(JSON) result = Column(JSON)
procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL', procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL',
name="fk_RES_procedure_id")) 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 import Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage 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, \ 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 datetime import datetime, date
from typing import List, Any, Tuple, Literal, Generator, Type, TYPE_CHECKING from typing import List, Any, Tuple, Literal, Generator, Type, TYPE_CHECKING
from pathlib import Path from pathlib import Path
@@ -100,7 +100,7 @@ class ClientSubmission(BaseClass, LogMixin):
Args: Args:
submission_type (str | models.SubmissionType | None, optional): Submission type of interest. Defaults to None. 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. 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. 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. 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. 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] samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation]
checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self) checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self)
if checker.exec(): 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] active_samples = [sample for sample in samples if sample.enabled]
logger.debug(active_samples) logger.debug(active_samples)
for sample in active_samples: for sample in active_samples:
@@ -323,8 +323,12 @@ class ClientSubmission(BaseClass, LogMixin):
def add_comment(self, obj): def add_comment(self, obj):
logger.debug("Add Comment") logger.debug("Add Comment")
def show_details(self, obj): # def show_details(self, obj):
logger.debug("Show Details") # 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): def details_dict(self, **kwargs):
output = super().details_dict(**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['run'] = [run.details_dict() for run in output['run']]
output['sample'] = [sample.details_dict() for sample in output['clientsubmissionsampleassociation']] output['sample'] = [sample.details_dict() for sample in output['clientsubmissionsampleassociation']]
output['name'] = self.name 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 return output
@@ -343,7 +352,7 @@ class Run(BaseClass, LogMixin):
""" """
id = Column(INTEGER, primary_key=True) #: primary key 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", clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL",
name="fk_BS_clientsub_id")) #: client lab id from _organizations) name="fk_BS_clientsub_id")) #: client lab id from _organizations)
clientsubmission = relationship("ClientSubmission", back_populates="run") clientsubmission = relationship("ClientSubmission", back_populates="run")
@@ -387,7 +396,7 @@ class Run(BaseClass, LogMixin):
@hybrid_property @hybrid_property
def name(self): def name(self):
return self.rsl_plate_num return self.rsl_plate_number
@classmethod @classmethod
def get_default_info(cls, *args, submissiontype: SubmissionType | None = None) -> dict: def get_default_info(cls, *args, submissiontype: SubmissionType | None = None) -> dict:
@@ -593,8 +602,23 @@ class Run(BaseClass, LogMixin):
def details_dict(self, **kwargs): def details_dict(self, **kwargs):
output = super().details_dict() 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['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 return output
@classmethod @classmethod
@@ -890,10 +914,10 @@ class Run(BaseClass, LogMixin):
field_value = dict(value=self.__getattribute__(key).name, missing=missing) field_value = dict(value=self.__getattribute__(key).name, missing=missing)
case "plate_number": case "plate_number":
key = 'name' 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": case "submitter_plate_number":
key = "submitter_plate_id" 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": case "id":
continue continue
case _: case _:
@@ -1168,8 +1192,8 @@ class Run(BaseClass, LogMixin):
e: SQLIntegrityError or SQLOperationalError if problem with commit. e: SQLIntegrityError or SQLOperationalError if problem with commit.
""" """
from frontend.widgets.pop_ups import QuestionAsker from frontend.widgets.pop_ups import QuestionAsker
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})") 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_num}?\n") msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {self.rsl_plate_number}?\n")
if msg.exec(): if msg.exec():
try: try:
# NOTE: backs up file as xlsx, same as export. # NOTE: backs up file as xlsx, same as export.
@@ -1187,17 +1211,17 @@ class Run(BaseClass, LogMixin):
except AttributeError: except AttributeError:
logger.error("App will not refresh data at this time.") logger.error("App will not refresh data at this time.")
def show_details(self, obj): # def show_details(self, obj):
""" # """
Creates Widget for showing procedure details. # Creates Widget for showing procedure details.
#
Args: # Args:
obj (Widget): Parent widget # obj (Widget): Parent widget
""" # """
from frontend.widgets.submission_details import SubmissionDetails # from frontend.widgets.submission_details import SubmissionDetails
dlg = SubmissionDetails(parent=obj, sub=self) # dlg = SubmissionDetails(parent=obj, sub=self)
if dlg.exec(): # if dlg.exec():
pass # pass
def edit(self, obj): def edit(self, obj):
""" """
@@ -1641,12 +1665,12 @@ class ClientSubmissionSampleAssociation(BaseClass):
output = super().details_dict() output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead. # 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']} 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() output = output['sample'].details_dict()
misc = output['_misc_info'] misc = output['misc_info']
logger.debug(f"Output from sample: {pformat(output)}") # logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant) output.update(relevant)
output['_misc_info'] = misc output['misc_info'] = misc
# output['sample'] = temp # output['sample'] = temp
# output.update(output['sample'].details_dict()) # output.update(output['sample'].details_dict())
return output return output
@@ -1815,7 +1839,7 @@ class ClientSubmissionSampleAssociation(BaseClass):
case ClientSubmission(): case ClientSubmission():
pass pass
case str(): case str():
clientsubmission = ClientSubmission.query(rsl_plate_num=clientsubmission) clientsubmission = ClientSubmission.query(rsl_plate_number=clientsubmission)
case _: case _:
raise ValueError() raise ValueError()
match sample: match sample:
@@ -1879,7 +1903,7 @@ class RunSampleAssociation(BaseClass):
def __repr__(self) -> str: def __repr__(self) -> str:
try: 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: except AttributeError as e:
logger.error(f"Unable to construct __repr__ due to: {e}") logger.error(f"Unable to construct __repr__ due to: {e}")
return super().__repr__() return super().__repr__()
@@ -1901,7 +1925,7 @@ class RunSampleAssociation(BaseClass):
except KeyError as e: except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.") logger.error(f"Unable to find row {self.row} in row_map.")
sample['Well'] = None sample['Well'] = None
sample['plate_name'] = self.run.rsl_plate_num sample['plate_name'] = self.run.rsl_plate_number
sample['positive'] = False sample['positive'] = False
return sample return sample
@@ -1975,7 +1999,7 @@ class RunSampleAssociation(BaseClass):
case Run(): case Run():
query = query.filter(cls.run == run) query = query.filter(cls.run == run)
case str(): case str():
query = query.join(Run).filter(Run.rsl_plate_num == run) query = query.join(Run).filter(Run.rsl_plate_number == run)
case _: case _:
pass pass
match sample: match sample:
@@ -2060,12 +2084,12 @@ class RunSampleAssociation(BaseClass):
output = super().details_dict() output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead. # 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']} 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() output = output['sample'].details_dict()
misc = output['_misc_info'] misc = output['misc_info']
logger.debug(f"Output from sample: {pformat(output)}") # logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant) output.update(relevant)
output['_misc_info'] = misc output['misc_info'] = misc
return output return output
class ProcedureSampleAssociation(BaseClass): class ProcedureSampleAssociation(BaseClass):
@@ -2132,9 +2156,9 @@ class ProcedureSampleAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead. # 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']} relevant = {k: v for k, v in output.items() if k not in ['sample']}
output = output['sample'].details_dict() output = output['sample'].details_dict()
misc = output['_misc_info'] misc = output['misc_info']
output.update(relevant) output.update(relevant)
output['_misc_info'] = misc output['misc_info'] = misc
output['results'] = [result.details_dict() for result in output['results']] output['results'] = [result.details_dict() for result in output['results']]
return output return output

View File

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

View File

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

View File

@@ -1,14 +1,15 @@
""" """
""" """
from __future__ import annotations
import logging, re import logging, re
from pathlib import Path from pathlib import Path
from typing import Generator, Tuple from typing import Generator, Tuple, TYPE_CHECKING
from openpyxl import load_workbook
from pandas import DataFrame from pandas import DataFrame
from backend.validators import pydant from backend.validators import pydant
from backend.db.models import Procedure if TYPE_CHECKING:
from dataclasses import dataclass from backend.db.models import ProcedureType
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -30,7 +31,7 @@ class DefaultParser(object):
return instance 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: Args:
@@ -40,7 +41,7 @@ class DefaultParser(object):
*args (): *args ():
**kwargs (): **kwargs ():
""" """
self.procedure = procedure self.proceduretype = proceduretype
try: try:
self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}") self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}")
except AttributeError: except AttributeError:
@@ -58,6 +59,13 @@ class DefaultParser(object):
data['filepath'] = self.filepath data['filepath'] = self.filepath
return self._pyd_object(**data) 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): class DefaultKEYVALUEParser(DefaultParser):
@@ -90,7 +98,6 @@ class DefaultTABLEParser(DefaultParser):
default_range_dict = [dict( default_range_dict = [dict(
header_row=20, header_row=20,
end_row=116,
sheet="Sample List" sheet="Sample List"
)] )]
@@ -98,15 +105,25 @@ class DefaultTABLEParser(DefaultParser):
def parsed_info(self): def parsed_info(self):
for item in self.range_dict: for item in self.range_dict:
list_worksheet = self.workbook[item['sheet']] list_worksheet = self.workbook[item['sheet']]
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 = DataFrame([item for item in list_worksheet.values][item['header_row'] - 1:])
list_df.columns = list_df.iloc[0] list_df.columns = list_df.iloc[0]
list_df = list_df[1:] list_df = list_df[1:]
list_df = list_df.dropna(axis=1, how='all') list_df = list_df.dropna(axis=1, how='all')
for ii, row in enumerate(list_df.iterrows()): 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 yield output
def to_pydantic(self, **kwargs): def to_pydantic(self, **kwargs):
return [self._pyd_object(**output) for output in self.parsed_info] 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 import logging
from pathlib import Path from pathlib import Path
from string import ascii_lowercase from string import ascii_lowercase
@@ -9,8 +10,9 @@ from typing import Generator
from openpyxl.reader.excel import load_workbook from openpyxl.reader.excel import load_workbook
from tools import row_keys from tools import row_keys
from backend.db.models import SubmissionType # from backend.db.models import SubmissionType
from . import DefaultKEYVALUEParser, DefaultTABLEParser from . import DefaultKEYVALUEParser, DefaultTABLEParser
from backend.managers import procedures as procedure_managers
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -34,6 +36,7 @@ class SubmissionTyperMixin(object):
@classmethod @classmethod
def get_subtype_from_regex(cls, filepath: Path): def get_subtype_from_regex(cls, filepath: Path):
from backend.db.models import SubmissionType
regex = SubmissionType.regex regex = SubmissionType.regex
m = regex.search(filepath.__str__()) m = regex.search(filepath.__str__())
try: try:
@@ -45,7 +48,8 @@ class SubmissionTyperMixin(object):
@classmethod @classmethod
def get_subtype_from_preparse(cls, filepath: Path): 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 = next((value for k, value in parser.parsed_info if k == "submissiontype"), None)
sub_type = SubmissionType.query(name=sub_type) sub_type = SubmissionType.query(name=sub_type)
if isinstance(sub_type, list): if isinstance(sub_type, list):
@@ -54,6 +58,7 @@ class SubmissionTyperMixin(object):
@classmethod @classmethod
def get_subtype_from_properties(cls, filepath: Path): def get_subtype_from_properties(cls, filepath: Path):
from backend.db.models import SubmissionType
wb = load_workbook(filepath) wb = load_workbook(filepath)
# NOTE: Gets first category in the metadata. # NOTE: Gets first category in the metadata.
categories = wb.properties.category.split(";") categories = wb.properties.category.split(";")
@@ -64,7 +69,7 @@ class SubmissionTyperMixin(object):
return sub_type return sub_type
class ClientSubmissionParser(DefaultKEYVALUEParser, SubmissionTyperMixin): class ClientSubmissionInfoParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
""" """
Object for retrieving submitter info from "sample list" sheet 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): def __init__(self, filepath: Path | str, *args, **kwargs):
from frontend.widgets.pop_ups import QuestionAsker
self.submissiontype = self.retrieve_submissiontype(filepath=filepath) self.submissiontype = self.retrieve_submissiontype(filepath=filepath)
if "range_dict" not in kwargs: if "range_dict" not in kwargs:
kwargs['range_dict'] = self.submissiontype.info_map kwargs['range_dict'] = self.submissiontype.info_map
super().__init__(filepath=filepath, **kwargs) 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 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 import logging
from pprint import pformat
from pathlib import Path
from typing import Generator, Tuple
from openpyxl import load_workbook
from backend.db.models import Run, Sample, Procedure, ProcedureSampleAssociation 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__}") logger = logging.getLogger(f"submissions.{__name__}")
# class PCRResultsParser(DefaultParser):
# pass
class PCRInfoParser(DefaultKEYVALUEParser): class PCRInfoParser(DefaultKEYVALUEParser):
default_range_dict = [dict( default_range_dict = [dict(

View File

@@ -191,7 +191,7 @@ class TurnaroundMaker(ReportArchetype):
tat_ok = days <= tat tat_ok = days <= tat
except TypeError: except TypeError:
return {} 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) 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 import logging
from pathlib import Path from pathlib import Path
from backend.validators import PydResults from backend.db.models import Procedure
from backend.db.models import Procedure, Results from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser
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 . import DefaultResults from . import DefaultResults
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -15,25 +12,21 @@ logger = logging.getLogger(f"submissions.{__name__}")
class PCR(DefaultResults): class PCR(DefaultResults):
def __init__(self, procedure: Procedure, parent, fname:Path|str|None=None): def __init__(self, procedure: Procedure, parent, fname:Path|str|None=None):
logger.debug(f"FName before correction: {fname}") super().__init__(procedure=procedure, parent=parent, fname=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}")
self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure) self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure)
self.sample_parser = PCRSampleParser(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() self.build_samples()
def build_procedure(self): def build_info(self):
procedure_info = self.info_parser.to_pydantic() procedure_info = self.info_parser.to_pydantic()
procedure_info.results_type = self.__class__.__name__
procedure_sql = procedure_info.to_sql() procedure_sql = procedure_info.to_sql()
procedure_sql.save() procedure_sql.save()
def build_samples(self): def build_samples(self):
samples = self.sample_parser.to_pydantic() samples = self.sample_parser.to_pydantic()
for sample in samples: for sample in samples:
sample.results_type = self.__class__.__name__
sql = sample.to_sql() sql = sample.to_sql()
sql.save() sql.save()

View File

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

View File

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

View File

@@ -8,8 +8,8 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from tools import jinja_template_loading from tools import jinja_template_loading
import logging import logging
from backend.db import models
from typing import Literal from typing import Literal, Any
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -70,7 +70,8 @@ class ObjectSelector(QDialog):
dialog to input BaseClass type manually 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__() super().__init__()
self.setWindowTitle(title) self.setWindowTitle(title)
self.widget = QComboBox() self.widget = QComboBox()

View File

@@ -29,7 +29,7 @@ class ProcedureCreation(QDialog):
super().__init__(parent) super().__init__(parent)
self.run = run self.run = run
self.proceduretype = proceduretype 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 = self.proceduretype.construct_dummy_procedure(run=self.run)
self.created_procedure.update_kittype_reagentroles(kittype=self.created_procedure.possible_kits[0]) 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) 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", template_name="procedure_creation",
# css_in=['new_context_menu'], # css_in=['new_context_menu'],
js_in=["procedure_form", "grid_drag", "context_menu"], js_in=["procedure_form", "grid_drag", "context_menu"],
proceduretype=self.proceduretype.as_dict, proceduretype=self.proceduretype.details_dict(),
run=self.run.to_dict(), run=self.run.details_dict(),
procedure=self.created_procedure.__dict__, procedure=self.created_procedure.__dict__,
plate_map=self.plate_map 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): def __init__(self, parent, title: str, samples: List[PydSample], clientsubmission: ClientSubmission|None=None):
super().__init__(parent) super().__init__(parent)
if clientsubmission: 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: else:
self.rsl_plate_num = clientsubmission self.rsl_plate_number = clientsubmission
self.samples = samples self.samples = samples
self.setWindowTitle(title) self.setWindowTitle(title)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -45,7 +45,7 @@ class SampleChecker(QDialog):
except AttributeError as e: except AttributeError as e:
logger.error(f"Problem getting sample list: {e}") logger.error(f"Problem getting sample list: {e}")
samples = [] 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) 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)
@@ -76,9 +76,9 @@ class SampleChecker(QDialog):
item.__setattr__("enabled", enabled) item.__setattr__("enabled", enabled)
@pyqtSlot(str) @pyqtSlot(str)
def set_rsl_plate_num(self, rsl_plate_num: str): def set_rsl_plate_number(self, rsl_plate_number: str):
logger.debug(f"RSL plate num: {rsl_plate_num}") logger.debug(f"RSL plate num: {rsl_plate_number}")
self.rsl_plate_num = rsl_plate_num self.rsl_plate_number = rsl_plate_number
@property @property
def formatted_list(self) -> List[dict]: def formatted_list(self) -> List[dict]:

View File

@@ -50,17 +50,33 @@ class SubmissionDetails(QDialog):
# NOTE: setup channel # NOTE: setup channel
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject('backend', self) self.channel.registerObject('backend', self)
match sub: # match sub:
case Run(): # case Run():
self.run_details(run=sub) # self.run_details(run=sub)
self.rsl_plate_num = sub.rsl_plate_num # self.rsl_plate_number = sub.rsl_plate_number
case Sample(): # 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)
# NOTE: Used to maintain javascript functions. # NOTE: Used to maintain javascript functions.
self.object_details(object=sub)
self.webview.page().setWebChannel(self.channel) 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: def activate_export(self) -> None:
""" """
Determines if export pdf should be active. Determines if export pdf should be active.
@@ -213,7 +229,7 @@ class SubmissionDetails(QDialog):
logger.debug(f"Submission details.") logger.debug(f"Submission details.")
if isinstance(run, str): if isinstance(run, str):
run = Run.query(name=run) 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) 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)
@@ -244,7 +260,7 @@ class SubmissionDetails(QDialog):
run.completed_date = datetime.now() run.completed_date = datetime.now()
run.completed_date.replace(tzinfo=timezone) run.completed_date.replace(tzinfo=timezone)
run.save() run.save()
self.run_details(run=self.rsl_plate_num) self.run_details(run=self.rsl_plate_number)
def save_pdf(self): def save_pdf(self):
""" """
@@ -264,7 +280,7 @@ class SubmissionComment(QDialog):
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
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment") self.setWindowTitle(f"{self.submission.rsl_plate_number} Submission Comment")
# NOTE: create text field # NOTE: create text field
self.txt_editor = QTextEdit(self) self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False) self.txt_editor.setReadOnly(False)

View File

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

View File

@@ -10,7 +10,7 @@ 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 ClientSubmissionParser, ClientSampleParser from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser
from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample
from backend.db import ( from backend.db import (
ClientLab, SubmissionType, Reagent, ClientLab, SubmissionType, Reagent,
@@ -121,20 +121,20 @@ 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.clientsubmissionparser = ClientSubmissionParser(filepath=fname) self.clientsubmissionparser = ClientSubmissionInfoParser(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.clientsubmissionparser = ClientSubmissionParser(filepath=fname) self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
try: try:
# self.prsr = SheetParser(filepath=fname) # self.prsr = SheetParser(filepath=fname)
self.sampleparser = ClientSampleParser(filepath=fname) self.sampleparser = ClientSubmissionSampleParser(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.sampleparser = ClientSampleParser(filepath=fname) self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
self.pydclientsubmission = self.clientsubmissionparser.to_pydantic() self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
self.pydsamples = self.sampleparser.to_pydantic() self.pydsamples = self.sampleparser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}") # logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
@@ -368,7 +368,7 @@ class SubmissionFormWidget(QWidget):
pass pass
# NOTE: code 1: ask for overwrite # NOTE: code 1: ask for overwrite
case 1: 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(): if dlg.exec():
# NOTE: Do not add duplicate reagents. # NOTE: Do not add duplicate reagents.
pass 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; 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"> <html lang="en">
<head> <head>
{% block head %} {% block head %}
@@ -14,17 +14,41 @@
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% endif %}
{% block body %} {% block body %}
<!--<button type="button" id="back_btn">Back</button>-->
{% endblock %} {% endblock %}
{% block signing_button %}{% endblock %} {% block signing_button %}{% endblock %}
{% if not child %}
</body> </body>
{% endif %}
{% block script %} {% 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%} {% for j in js%}
<script> <script>
{{ j }} {{ j }}
</script> </script>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% if not child %}
</html> </html>
{% endif %}

View File

@@ -28,10 +28,21 @@
<br><hr><br> <br><hr><br>
{% for key, value in procedure['reagentrole'].items() %} {% for key, value in procedure['reagentrole'].items() %}
<label for="{{ key }}">{{ key }}:</label><br> <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 %} {% for reagent in value %}
<option value="{{ reagent }}">{{ reagent }}</option> <option value="{{ reagent }}">{{ reagent }}</option>
{% endfor %} {% endfor %}
</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> </select>
{% endfor %} {% endfor %}
{% endif%} {% endif%}

View File

@@ -1,4 +1,5 @@
{% extends "details.html" %} {% extends "details.html" %}
{% if not child %}
<head> <head>
{% block head %} {% block head %}
@@ -7,9 +8,31 @@
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% endif %}
{% block body %} {% 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 %} {% endblock %}
</body> </body>

View File

@@ -1,81 +1,41 @@
{% extends "details.html" %} {% extends "details.html" %}
<html>
<head> <head>
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<title>Submission Details for {{ sub['plate_number'] }}</title> <title>Run Details for {{ run['rsl_plate_number'] }}</title>
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% block 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 %} <h2><u>Run Details for {{ run['rsl_plate_number'] }}</u></h2>
{{ super() }} {{ super() }}
<p>{% for key, value in sub.items() if key not in sub['excluded'] %} <p>{% for key, value in run.items() if key not in run['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> &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 %} {% endfor %}
{% if sub['custom'] %}{% for key, value in sub['custom'].items() %} </div>
&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 %}
{% 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 %}
{% 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 %} {% endif %}
{% endblock %} {% endblock %}
{% block signing_button %} {% block signing_button %}
<button type="button" id="sign_btn" {% if permission and not sub['signed_by'] %}{% else %}hidden{% endif %}>Sign Off</button> <button type="button" id="sign_btn" {% if run['permission'] and not run['signed_by'] %}{% else %}hidden{% endif %}>Sign Off</button>
{% endblock %} {% endblock %}
<br> <br>
<br>
<br>
</body> </body>
{% block script %} {% block script %}
@@ -90,46 +50,10 @@
}) })
} }
var reagentSelection = document.getElementsByClassName('reagent');
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(){ document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}"); backend.sign_off("{{ run['rsl_plate_num'] }}");
}); });
</script> </script>
{% endblock %} {% endblock %}
</html>

View File

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