Addition of procedure parser in import.
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 *
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 *
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
@@ -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
|
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(
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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
|
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()
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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):
|
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]:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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;
|
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">
|
<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 %}
|
||||||
@@ -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%}
|
||||||
|
|||||||
@@ -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'] %}
|
||||||
|
<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 %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
@@ -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> {% 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'] %}
|
||||||
<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
<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 %}
|
{% endfor %}
|
||||||
{% if sub['custom'] %}{% for key, value in sub['custom'].items() %}
|
</div>
|
||||||
<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 }}">
|
|
||||||
{% 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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user