Addition of procedure parser in import.
This commit is contained in:
@@ -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
|
||||
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',
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']]
|
||||
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 *
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
22
src/submissions/backend/managers/__init__.py
Normal file
22
src/submissions/backend/managers/__init__.py
Normal 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}")
|
||||
|
||||
44
src/submissions/backend/managers/procedures/__init__.py
Normal file
44
src/submissions/backend/managers/procedures/__init__.py
Normal 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()
|
||||
21
src/submissions/backend/managers/results/__init__.py
Normal file
21
src/submissions/backend/managers/results/__init__.py
Normal 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
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
|
||||
class DefaultResults(object):
|
||||
|
||||
pass
|
||||
|
||||
from .pcr import PCR
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
37
src/submissions/templates/clientsubmission_details.html
Normal file
37
src/submissions/templates/clientsubmission_details.html
Normal 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'] %}
|
||||
<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'] %}
|
||||
<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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
{% if not child %}
|
||||
</html>
|
||||
{% endif %}
|
||||
@@ -28,10 +28,21 @@
|
||||
<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 %}
|
||||
</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%}
|
||||
|
||||
@@ -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'] %}
|
||||
<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'] %}
|
||||
<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>
|
||||
@@ -1,81 +1,41 @@
|
||||
{% extends "details.html" %}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Submission Details for {{ sub['plate_number'] }}</title>
|
||||
<title>Run Details for {{ run['rsl_plate_number'] }}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% block body %}
|
||||
<h2><u>Submission Details for {{ sub['plate_number'] }}</u></h2> {% 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() }}
|
||||
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
|
||||
<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
||||
<p>{% for key, value in run.items() if key not in run['excluded'] %}
|
||||
<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'] %}
|
||||
<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 %}
|
||||
{% if sub['custom'] %}{% for key, value in sub['custom'].items() %}
|
||||
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||
{% endfor %}{% endif %}</p>
|
||||
{% if sub['reagents'] %}
|
||||
<h3><u>Reagents:</u></h3>
|
||||
<p>{% for item in sub['reagents'] %}
|
||||
<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'] %}
|
||||
<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> ') }}</a><br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['tips'] %}
|
||||
<h3><u>Tips:</u></h3>
|
||||
<p>{% for item in sub['tips'] %}
|
||||
<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'] %}
|
||||
<b>{{ item['well'] }}:</b><a class="data-link sample" id="{{ item['submitter_id'] }}">{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% 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 %}
|
||||
<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}uL<br>
|
||||
{% else %}
|
||||
<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'] %}
|
||||
<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 }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% 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 %}
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
</body>
|
||||
|
||||
{% 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(){
|
||||
backend.sign_off("{{ sub['plate_number'] }}");
|
||||
backend.sign_off("{{ run['rsl_plate_num'] }}");
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user