Code cleanup and documentation

This commit is contained in:
Landon Wark
2024-02-09 14:03:35 -06:00
parent eda62fba5a
commit a534d229a8
30 changed files with 1558 additions and 1347 deletions

View File

@@ -4,7 +4,7 @@ from pathlib import Path
# Version of the realpython-reader package
__project__ = "submissions"
__version__ = "202401.4b"
__version__ = "202402.1b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2024, Government of Canada"

View File

@@ -2,7 +2,7 @@
Contains all models for sqlalchemy
'''
import sys
from sqlalchemy.orm import DeclarativeMeta, declarative_base
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query
from sqlalchemy.ext.declarative import declared_attr
if 'pytest' in sys.modules:
from pathlib import Path
@@ -23,10 +23,16 @@ class BaseClass(Base):
@declared_attr
def __tablename__(cls):
"""
Set tablename to lowercase class name
"""
return f"_{cls.__name__.lower()}"
@declared_attr
def __database_session__(cls):
"""
Pull db session from ctx
"""
if not 'pytest' in sys.modules:
from tools import ctx
else:
@@ -35,6 +41,9 @@ class BaseClass(Base):
@declared_attr
def __directory_path__(cls):
"""
Pull submission directory from ctx
"""
if not 'pytest' in sys.modules:
from tools import ctx
else:
@@ -43,14 +52,39 @@ class BaseClass(Base):
@declared_attr
def __backup_path__(cls):
"""
Pull backup directory from ctx
"""
if not 'pytest' in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.backup_path
def query_return(query:Query, limit:int=0):
"""
Execute sqlalchemy query.
Args:
query (Query): Query object
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
_type_: Query result.
"""
with query.session.no_autoflush:
match limit:
case 0:
return query.all()
case 1:
return query.first()
case _:
return query.limit(limit).all()
def save(self):
# logger.debug(f"Saving {self}")
"""
Add the object to the database and commit
"""
try:
self.__database_session__.add(self)
self.__database_session__.commit()

View File

@@ -7,7 +7,7 @@ from sqlalchemy.orm import relationship, Query
import logging, json
from operator import itemgetter
from . import BaseClass
from tools import setup_lookup, query_return
from tools import setup_lookup
from datetime import date, datetime
from typing import List
from dateutil.parser import parse
@@ -18,7 +18,6 @@ class ControlType(BaseClass):
"""
Base class of a control archetype.
"""
# __tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -48,7 +47,7 @@ class ControlType(BaseClass):
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
return cls.query_return(query=query, limit=limit)
def get_subtypes(self, mode:str) -> List[str]:
"""
@@ -60,10 +59,13 @@ class ControlType(BaseClass):
Returns:
List[str]: list of subtypes available
"""
# Get first instance since all should have same subtypes
outs = self.instances[0]
# Get mode of instance
jsoner = json.loads(getattr(outs, mode))
logger.debug(f"JSON out: {jsoner.keys()}")
try:
# Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0]
except IndexError:
return []
@@ -74,8 +76,6 @@ class Control(BaseClass):
"""
Base class of a control sample.
"""
# __tablename__ = '_control_samples'
id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_controltype.id", name="fk_control_parent_id")) #: primary key of control type
@@ -90,10 +90,14 @@ class Control(BaseClass):
refseq_version = Column(String(16)) #: version of refseq used in fastq parsing
kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing
kraken2_db_version = Column(String(32)) #: folder name of kraken2 db
sample = relationship("BacterialCultureSample", back_populates="control")
sample_id = Column(INTEGER, ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id"))
sample = relationship("BacterialCultureSample", back_populates="control") #: This control's submission sample
sample_id = Column(INTEGER, ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
def __repr__(self) -> str:
"""
Returns:
str: Representation of self
"""
return f"<Control({self.name})>"
def to_sub_dict(self) -> dict:
@@ -103,25 +107,25 @@ class Control(BaseClass):
Returns:
dict: output dictionary containing: Name, Type, Targets, Top Kraken results
"""
# load json string into dict
# logger.debug("loading json string into dict")
try:
kraken = json.loads(self.kraken)
except TypeError:
kraken = {}
# calculate kraken count total to use in percentage
# logger.debug("calculating kraken count total to use in percentage")
kraken_cnt_total = sum([kraken[item]['kraken_count'] for item in kraken])
new_kraken = []
for item in kraken:
# calculate kraken percent (overwrites what's already been scraped)
# logger.debug("calculating kraken percent (overwrites what's already been scraped)")
kraken_percent = kraken[item]['kraken_count'] / kraken_cnt_total
new_kraken.append({'name': item, 'kraken_count':kraken[item]['kraken_count'], 'kraken_percent':"{0:.0%}".format(kraken_percent)})
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
# set targets
# logger.debug("setting targets")
if self.controltype.targets == []:
targets = ["None"]
else:
targets = self.controltype.targets
# construct output dictionary
# logger.debug("constructing output dictionary")
output = {
"name" : self.name,
"type" : self.controltype.name,
@@ -141,49 +145,28 @@ class Control(BaseClass):
list[dict]: list of records
"""
output = []
# load json string for mode (i.e. contains, matches, kraken2)
# logger.debug("load json string for mode (i.e. contains, matches, kraken2)")
try:
data = json.loads(getattr(self, mode))
except TypeError:
data = {}
logger.debug(f"Length of data: {len(data)}")
# dict keys are genera of bacteria, e.g. 'Streptococcus'
# logger.debug("dict keys are genera of bacteria, e.g. 'Streptococcus'")
for genus in data:
_dict = {}
_dict['name'] = self.name
_dict['submitted_date'] = self.submitted_date
_dict['genus'] = genus
# get Target or Off-target of genus
# logger.debug("get Target or Off-target of genus")
_dict['target'] = 'Target' if genus.strip("*") in self.controltype.targets else "Off-target"
# set 'contains_hashes', etc for genus,
# logger.debug("set 'contains_hashes', etc for genus")
for key in data[genus]:
_dict[key] = data[genus][key]
output.append(_dict)
# Have to triage kraken data to keep program from getting overwhelmed
# logger.debug("Have to triage kraken data to keep program from getting overwhelmed")
if "kraken" in mode:
output = sorted(output, key=lambda d: d[f"{mode}_count"], reverse=True)[:49]
return output
def create_dummy_data(self, mode:str) -> dict:
"""
Create non-zero length data to maintain entry of zero length 'contains' (depreciated)
Args:
mode (str): analysis type, 'contains', etc
Returns:
dict: dictionary of 'Nothing' genus
"""
match mode:
case "contains":
data = {"Nothing": {"contains_hashes":"0/400", "contains_ratio":0.0}}
case "matches":
data = {"Nothing": {"matches_hashes":"0/400", "matches_ratio":0.0}}
case "kraken":
data = {"Nothing": {"kraken_percent":0.0, "kraken_count":0}}
case _:
data = {}
return data
@classmethod
def get_modes(cls) -> List[str]:
@@ -194,6 +177,7 @@ class Control(BaseClass):
List[str]: List of control mode names.
"""
try:
# logger.debug("Creating a list of JSON columns in _controls table")
cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
except AttributeError as e:
logger.error(f"Failed to get available modes from db: {e}")
@@ -243,25 +227,32 @@ class Control(BaseClass):
if start_date != None:
match start_date:
case date():
# logger.debug(f"Lookup control by start date({start_date})")
start_date = start_date.strftime("%Y-%m-%d")
case int():
# logger.debug(f"Lookup control by ordinal start date {start_date}")
start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _:
# logger.debug(f"Lookup control with parsed start date {start_date}")
start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date:
case date():
# logger.debug(f"Lookup control by end date({end_date})")
end_date = end_date.strftime("%Y-%m-%d")
case int():
# logger.debug(f"Lookup control by ordinal end date {end_date}")
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
case _:
# logger.debug(f"Lookup control with parsed end date {end_date}")
end_date = parse(end_date).strftime("%Y-%m-%d")
# logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
query = query.filter(cls.submitted_date.between(start_date, end_date))
match control_name:
case str():
# logger.debug(f"Lookup control by name {control_name}")
query = query.filter(cls.name.startswith(control_name))
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
return cls.query_return(query=query, limit=limit)

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship, Query
from . import Base, BaseClass
from tools import check_authorization, setup_lookup, query_return, Settings
from tools import check_authorization, setup_lookup
from typing import List
import logging
@@ -25,8 +25,7 @@ class Organization(BaseClass):
"""
Base of organization
"""
# __tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name
submissions = relationship("BasicSubmission", back_populates="submitting_lab") #: submissions this organization has submitted
@@ -34,11 +33,12 @@ class Organization(BaseClass):
contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) #: contacts involved with this org
def __repr__(self) -> str:
"""
Returns:
str: Representation of this Organization
"""
return f"<Organization({self.name})>"
def set_attribute(self, name:str, value):
setattr(self, name, value)
@classmethod
@setup_lookup
def query(cls,
@@ -63,24 +63,17 @@ class Organization(BaseClass):
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
return cls.query_return(query=query, limit=limit)
@check_authorization
def save(self, ctx:Settings):
"""
Adds this instance to the database and commits
Args:
ctx (Settings): Settings object passed down from GUI. Necessary to check authorization
"""
def save(self):
super().save()
class Contact(BaseClass):
"""
Base of Contact
"""
# __tablename__ = "_contacts"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name
email = Column(String(64)) #: contact email
@@ -88,6 +81,10 @@ class Contact(BaseClass):
organization = relationship("Organization", back_populates="contacts", uselist=True, secondary=orgs_contacts) #: relationship to joined organization
def __repr__(self) -> str:
"""
Returns:
str: Representation of this Contact
"""
return f"<Contact({self.name})>"
@classmethod
@@ -133,5 +130,5 @@ class Contact(BaseClass):
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
return cls.query_return(query=query, limit=limit)

File diff suppressed because it is too large Load Diff

View File

@@ -13,23 +13,21 @@ import logging, re
from collections import OrderedDict
from datetime import date
from dateutil.parser import parse, ParserError
from tools import check_not_nan, convert_nans_to_nones, Settings, is_missing
from tools import check_not_nan, convert_nans_to_nones, is_missing, row_map
logger = logging.getLogger(f"submissions.{__name__}")
row_keys = dict(A=1, B=2, C=3, D=4, E=5, F=6, G=7, H=8)
row_keys = {v:k for k,v in row_map.items()}
class SheetParser(object):
"""
object to pull and contain data from excel file
"""
def __init__(self, ctx:Settings, filepath:Path|None = None):
def __init__(self, filepath:Path|None = None):
"""
Args:
ctx (Settings): Settings object passed down from gui. Necessary for Bacterial to get directory path.
filepath (Path | None, optional): file path to excel sheet. Defaults to None.
"""
self.ctx = ctx
logger.debug(f"\n\nParsing {filepath.__str__()}\n\n")
match filepath:
case Path():
@@ -46,7 +44,7 @@ class SheetParser(object):
raise FileNotFoundError(f"Couldn't parse file {self.filepath}")
self.sub = OrderedDict()
# make decision about type of sample we have
self.sub['submission_type'] = dict(value=RSLNamer.retrieve_submission_type(instr=self.filepath), missing=True)
self.sub['submission_type'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath), missing=True)
# # grab the info map from the submission type in database
self.parse_info()
self.import_kit_validation_check()
@@ -144,7 +142,6 @@ class InfoParser(object):
def __init__(self, xl:pd.ExcelFile, submission_type:str):
logger.info(f"\n\Hello from InfoParser!\n\n")
# self.ctx = ctx
self.map = self.fetch_submission_info_map(submission_type=submission_type)
self.xl = xl
logger.debug(f"Info map for InfoParser: {pformat(self.map)}")
@@ -209,7 +206,6 @@ class ReagentParser(object):
def __init__(self, xl:pd.ExcelFile, submission_type:str, extraction_kit:str):
logger.debug("\n\nHello from ReagentParser!\n\n")
# self.ctx = ctx
self.map = self.fetch_kit_info_map(extraction_kit=extraction_kit, submission_type=submission_type)
logger.debug(f"Reagent Parser map: {self.map}")
self.xl = xl
@@ -227,7 +223,6 @@ class ReagentParser(object):
"""
if isinstance(extraction_kit, dict):
extraction_kit = extraction_kit['value']
# kit = lookup_kit_types(ctx=self.ctx, name=extraction_kit)
kit = KitType.query(name=extraction_kit)
if isinstance(submission_type, dict):
submission_type = submission_type['value']
@@ -272,7 +267,6 @@ class ReagentParser(object):
lot = str(lot)
logger.debug(f"Going into pydantic: name: {name}, lot: {lot}, expiry: {expiry}, type: {item.strip()}, comment: {comment}")
listo.append(PydReagent(type=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, missing=missing))
# logger.debug(f"Returning listo: {listo}")
return listo
class SampleParser(object):
@@ -290,7 +284,6 @@ class SampleParser(object):
"""
logger.debug("\n\nHello from SampleParser!\n\n")
self.samples = []
# self.ctx = ctx
self.xl = xl
self.submission_type = submission_type
sample_info_map = self.fetch_sample_info_map(submission_type=submission_type)
@@ -316,11 +309,9 @@ class SampleParser(object):
dict: Info locations.
"""
logger.debug(f"Looking up submission type: {submission_type}")
# submission_type = lookup_submission_type(ctx=self.ctx, name=submission_type)
submission_type = SubmissionType.query(name=submission_type)
logger.debug(f"info_map: {pformat(submission_type.info_map)}")
sample_info_map = submission_type.info_map['samples']
# self.custom_parser = get_polymorphic_subclass(models.BasicSubmission, submission_type.name).parse_samples
self.custom_sub_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name).parse_samples
self.custom_sample_parser = BasicSample.find_polymorphic_subclass(polymorphic_identity=f"{submission_type.name} Sample").parse_sample
return sample_info_map
@@ -341,7 +332,6 @@ class SampleParser(object):
df = pd.DataFrame(df.values[1:], columns=df.iloc[0])
df = df.set_index(df.columns[0])
logger.debug(f"Vanilla platemap: {df}")
# custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type)
custom_mapper = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
df = custom_mapper.custom_platemap(self.xl, df)
logger.debug(f"Custom platemap:\n{df}")
@@ -402,7 +392,6 @@ class SampleParser(object):
else:
return input_str
for sample in self.samples:
# addition = self.lookup_table[self.lookup_table.isin([sample['submitter_id']]).any(axis=1)].squeeze().to_dict()
addition = self.lookup_table[self.lookup_table.isin([sample['submitter_id']]).any(axis=1)].squeeze()
# logger.debug(addition)
if isinstance(addition, pd.DataFrame) and not addition.empty:
@@ -433,25 +422,17 @@ class SampleParser(object):
# logger.debug(f"Output sample dict: {sample}")
logger.debug(f"Final lookup_table: \n\n {self.lookup_table}")
def parse_samples(self, generate:bool=True) -> List[dict]|List[BasicSample]:
def parse_samples(self) -> List[dict]|List[BasicSample]:
"""
Parse merged platemap\lookup info into dicts/samples
Args:
generate (bool, optional): Indicates if sample objects to be generated from dicts. Defaults to True.
Returns:
List[dict]|List[models.BasicSample]: List of samples
"""
result = None
new_samples = []
logger.debug(f"Starting samples: {pformat(self.samples)}")
for ii, sample in enumerate(self.samples):
# try:
# if sample['submitter_id'] in [check_sample['sample'].submitter_id for check_sample in new_samples]:
# sample['submitter_id'] = f"{sample['submitter_id']}-{ii}"
# except KeyError as e:
# logger.error(f"Sample obj: {sample}, error: {e}")
for sample in self.samples:
translated_dict = {}
for k, v in sample.items():
match v:
@@ -483,7 +464,7 @@ class SampleParser(object):
for plate in self.plates:
df = self.xl.parse(plate['sheet'], header=None)
if isinstance(df.iat[plate['row']-1, plate['column']-1], str):
output = RSLNamer.retrieve_rsl_number(instr=df.iat[plate['row']-1, plate['column']-1])
output = RSLNamer.retrieve_rsl_number(filename=df.iat[plate['row']-1, plate['column']-1])
else:
continue
plates.append(output)
@@ -495,25 +476,43 @@ class EquipmentParser(object):
self.submission_type = submission_type
self.xl = xl
self.map = self.fetch_equipment_map()
# self.equipment = self.parse_equipment()
def fetch_equipment_map(self) -> List[dict]:
"""
Gets the map of equipment locations in the submission type's spreadsheet
Returns:
List[dict]: List of locations
"""
submission_type = SubmissionType.query(name=self.submission_type)
return submission_type.construct_equipment_map()
def get_asset_number(self, input:str) -> str:
"""
Pulls asset number from string.
Args:
input (str): String to be scraped
Returns:
str: asset number
"""
regex = Equipment.get_regex()
logger.debug(f"Using equipment regex: {regex} on {input}")
try:
return regex.search(input).group().strip("-")
except AttributeError:
return input
def parse_equipment(self):
def parse_equipment(self) -> List[PydEquipment]:
"""
Scrapes equipment from xl sheet
Returns:
List[PydEquipment]: list of equipment
"""
logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
output = []
# sheets = list(set([item['sheet'] for item in self.map]))
# logger.debug(f"Sheets: {sheets}")
for sheet in self.xl.sheet_names:
df = self.xl.parse(sheet, header=None, dtype=object)
@@ -550,7 +549,6 @@ class PCRParser(object):
Args:
filepath (Path | None, optional): file to parse. Defaults to None.
"""
# self.ctx = ctx
logger.debug(f"Parsing {filepath.__str__()}")
if filepath == None:
logger.error(f"No filepath given.")
@@ -564,9 +562,8 @@ class PCRParser(object):
except PermissionError:
logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.")
return
# self.pcr = OrderedDict()
self.parse_general(sheet_name="Results")
namer = RSLNamer(instr=filepath.__str__())
namer = RSLNamer(filename=filepath.__str__())
self.plate_num = namer.parsed_name
self.submission_type = namer.submission_type
logger.debug(f"Set plate number to {self.plate_num} and type to {self.submission_type}")

View File

@@ -219,7 +219,7 @@ def drop_reruns_from_df(ctx:Settings, df: DataFrame) -> DataFrame:
def make_hitpicks(input:List[dict]) -> DataFrame:
"""
Converts lsit of dictionaries constructed by hitpicking to dataframe
Converts list of dictionaries constructed by hitpicking to dataframe
Args:
input (List[dict]): list of hitpicked dictionaries

View File

@@ -2,8 +2,8 @@ import logging, re
from pathlib import Path
from openpyxl import load_workbook
from backend.db.models import BasicSubmission, SubmissionType
from datetime import date
from tools import jinja_template_loading
from jinja2 import Template
logger = logging.getLogger(f"submissions.{__name__}")
@@ -11,14 +11,16 @@ class RSLNamer(object):
"""
Object that will enforce proper formatting on RSL plate names.
"""
def __init__(self, instr:str, sub_type:str|None=None, data:dict|None=None):
def __init__(self, filename:str, sub_type:str|None=None, data:dict|None=None):
self.submission_type = sub_type
if self.submission_type == None:
self.submission_type = self.retrieve_submission_type(instr=instr)
# logger.debug("Creating submission type because none exists")
self.submission_type = self.retrieve_submission_type(filename=filename)
logger.debug(f"got submission type: {self.submission_type}")
if self.submission_type != None:
# logger.debug("Retrieving BasicSubmission subclass")
enforcer = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.parsed_name = self.retrieve_rsl_number(instr=instr, regex=enforcer.get_regex())
self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=enforcer.get_regex())
if data == None:
data = dict(submission_type=self.submission_type)
if "submission_type" not in data.keys():
@@ -26,26 +28,25 @@ class RSLNamer(object):
self.parsed_name = enforcer.enforce_name(instr=self.parsed_name, data=data)
@classmethod
def retrieve_submission_type(cls, instr:str|Path) -> str:
def retrieve_submission_type(cls, filename:str|Path) -> str:
"""
Gets submission type from excel file properties or sheet names or regex pattern match or user input
Args:
instr (str | Path): filename
filename (str | Path): filename
Returns:
str: parsed submission type
"""
match instr:
match filename:
case Path():
logger.debug(f"Using path method for {instr}.")
if instr.exists():
wb = load_workbook(instr)
logger.debug(f"Using path method for {filename}.")
if filename.exists():
wb = load_workbook(filename)
try:
submission_type = [item.strip().title() for item in wb.properties.category.split(";")][0]
except AttributeError:
try:
# sts = {item.name:item.info_map['all_sheets'] for item in SubmissionType.query(key="all_sheets")}
sts = {item.name:item.get_template_file_sheets() for item in SubmissionType.query()}
for k,v in sts.items():
# This gets the *first* submission type that matches the sheet names in the workbook
@@ -54,13 +55,13 @@ class RSLNamer(object):
break
except:
# On failure recurse using filename as string for string method
submission_type = cls.retrieve_submission_type(instr=instr.stem.__str__())
submission_type = cls.retrieve_submission_type(filename=filename.stem.__str__())
else:
submission_type = cls.retrieve_submission_type(instr=instr.stem.__str__())
submission_type = cls.retrieve_submission_type(filename=filename.stem.__str__())
case str():
regex = BasicSubmission.construct_regex()
logger.debug(f"Using string method for {instr}.")
m = regex.search(instr)
logger.debug(f"Using string method for {filename}.")
m = regex.search(filename)
try:
submission_type = m.lastgroup
except AttributeError as e:
@@ -72,6 +73,7 @@ class RSLNamer(object):
except UnboundLocalError:
check = True
if check:
# logger.debug("Final option, ask the user for submission type")
from frontend.widgets import SubmissionTypeSelector
dlg = SubmissionTypeSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.")
if dlg.exec():
@@ -80,25 +82,25 @@ class RSLNamer(object):
return submission_type
@classmethod
def retrieve_rsl_number(cls, instr:str|Path, regex:str|None=None):
def retrieve_rsl_number(cls, filename:str|Path, regex:str|None=None):
"""
Uses regex to retrieve the plate number and submission type from an input string
Args:
in_str (str): string to be parsed
"""
logger.debug(f"Input string to be parsed: {instr}")
logger.debug(f"Input string to be parsed: {filename}")
if regex == None:
regex = BasicSubmission.construct_regex()
else:
regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE)
logger.debug(f"Using regex: {regex}")
match instr:
match filename:
case Path():
m = regex.search(instr.stem)
m = regex.search(filename.stem)
case str():
logger.debug(f"Using string method.")
m = regex.search(instr)
m = regex.search(filename)
case _:
pass
if m != None:
@@ -113,6 +115,15 @@ class RSLNamer(object):
@classmethod
def construct_new_plate_name(cls, data:dict) -> str:
"""
Make a brand new plate name from submission data.
Args:
data (dict): incoming submission data
Returns:
str: Output filename
"""
if "submitted_date" in data.keys():
if isinstance(data['submitted_date'], dict):
if data['submitted_date']['value'] != None:
@@ -135,12 +146,20 @@ class RSLNamer(object):
return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
@classmethod
def construct_export_name(cls, template, **kwargs):
def construct_export_name(cls, template:Template, **kwargs) -> str:
"""
Make export file name from jinja template. (currently unused)
Args:
template (jinja2.Template): Template stored in BasicSubmission
Returns:
str: output file name.
"""
logger.debug(f"Kwargs: {kwargs}")
logger.debug(f"Template: {template}")
environment = jinja_template_loading()
template = environment.from_string(template)
return template.render(**kwargs)
from .pydant import *
from .pydant import *

View File

@@ -11,17 +11,17 @@ from dateutil.parser._parser import ParserError
from typing import List, Tuple
from . import RSLNamer
from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, jinja_template_loading, Report, Result, row_map
from tools import check_not_nan, convert_nans_to_nones, Report, Result, row_map
from backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError
from PyQt6.QtWidgets import QComboBox, QWidget
# from pprint import pformat
from openpyxl import load_workbook, Workbook
from io import BytesIO
logger = logging.getLogger(f"submissions.{__name__}")
class PydReagent(BaseModel):
lot: str|None
type: str|None
expiry: date|None
@@ -103,6 +103,7 @@ class PydReagent(BaseModel):
Tuple[Reagent, Report]: Reagent instance and result of function
"""
report = Report()
# logger.debug("Adding extra fields.")
if self.model_extra != None:
self.__dict__.update(self.model_extra)
logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
@@ -118,16 +119,17 @@ class PydReagent(BaseModel):
match key:
case "lot":
reagent.lot = value.upper()
case "expiry":
reagent.expiry = value
case "type":
reagent_type = ReagentType.query(name=value)
if reagent_type != None:
reagent.type.append(reagent_type)
case "name":
reagent.name = value
case "comment":
continue
case _:
try:
reagent.__setattr__(key, value)
except AttributeError:
logger.error(f"Couldn't set {key} to {value}")
if submission != None:
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
assoc.comments = self.comment
@@ -190,7 +192,8 @@ class PydSample(BaseModel, extra='allow'):
case "row" | "column":
continue
case _:
instance.set_attribute(name=key, value=value)
# instance.set_attribute(name=key, value=value)
instance.__setattr__(key, value)
out_associations = []
if submission != None:
assoc_type = self.sample_type.replace("Sample", "").strip()
@@ -228,11 +231,16 @@ class PydEquipment(BaseModel, extra='ignore'):
value=['']
return value
# def toForm(self, parent):
# from frontend.widgets.equipment_usage import EquipmentCheckBox
# return EquipmentCheckBox(parent=parent, equipment=self)
def toSQL(self, submission:BasicSubmission|str=None):
def toSQL(self, submission:BasicSubmission|str=None) -> Tuple[Equipment, SubmissionEquipmentAssociation]:
"""
Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment
Args:
submission ( BasicSubmission | str ): BasicSubmission of interest
Returns:
Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects
"""
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_number=submission)
equipment = Equipment.query(asset_number=self.asset_number)
@@ -242,6 +250,7 @@ class PydEquipment(BaseModel, extra='ignore'):
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
process = Process.query(name=self.processes[0])
if process == None:
# logger.debug("Adding in unknown process.")
from frontend.widgets.pop_ups import QuestionAsker
dlg = QuestionAsker(title="Add Process?", message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
if dlg.exec():
@@ -254,8 +263,6 @@ class PydEquipment(BaseModel, extra='ignore'):
process.save()
assoc.process = process
assoc.role = self.role
# equipment.equipment_submission_associations.append(assoc)
# equipment.equipment_submission_associations.append(assoc)
else:
assoc = None
return equipment, assoc
@@ -357,7 +364,7 @@ class PydSubmission(BaseModel, extra='allow'):
if check_not_nan(value['value']):
return value
else:
output = RSLNamer(instr=values.data['filepath'].__str__(), sub_type=sub_type, data=values.data).parsed_name
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type, data=values.data).parsed_name
return dict(value=output, missing=True)
@field_validator("technician", mode="before")
@@ -407,9 +414,10 @@ class PydSubmission(BaseModel, extra='allow'):
return dict(value=value, missing=False)
else:
# return dict(value=RSLNamer(instr=values.data['filepath'].__str__()).submission_type.title(), missing=True)
return dict(value=RSLNamer.retrieve_submission_type(instr=values.data['filepath']).title(), missing=True)
return dict(value=RSLNamer.retrieve_submission_type(filename=values.data['filepath']).title(), missing=True)
@field_validator("submission_category", mode="before")
@classmethod
def create_category(cls, value):
if not isinstance(value, dict):
return dict(value=value, missing=True)
@@ -423,6 +431,7 @@ class PydSubmission(BaseModel, extra='allow'):
return value
@field_validator("samples")
@classmethod
def assign_ids(cls, value, values):
starting_id = SubmissionSampleAssociation.autoincrement_id()
output = []
@@ -431,7 +440,6 @@ class PydSubmission(BaseModel, extra='allow'):
output.append(sample)
return output
def handle_duplicate_samples(self):
"""
Collapses multiple samples with same submitter id into one with lists for rows, columns.
@@ -439,7 +447,7 @@ class PydSubmission(BaseModel, extra='allow'):
"""
submitter_ids = list(set([sample.submitter_id for sample in self.samples]))
output = []
for iii, id in enumerate(submitter_ids, start=1):
for id in submitter_ids:
relevants = [item for item in self.samples if item.submitter_id==id]
if len(relevants) <= 1:
output += relevants
@@ -447,9 +455,6 @@ class PydSubmission(BaseModel, extra='allow'):
rows = [item.row[0] for item in relevants]
columns = [item.column[0] for item in relevants]
ids = [item.assoc_id[0] for item in relevants]
# for jjj, rel in enumerate(relevants, start=1):
# starting_id += jjj
# ids.append(starting_id)
dummy = relevants[0]
dummy.assoc_id = ids
dummy.row = rows
@@ -471,6 +476,7 @@ class PydSubmission(BaseModel, extra='allow'):
if dictionaries:
output = {k:getattr(self, k) for k in fields}
else:
# logger.debug("Extracting 'value' from attributes")
output = {k:(getattr(self, k) if not isinstance(getattr(self, k), dict) else getattr(self, k)['value']) for k in fields}
return output
@@ -493,12 +499,14 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object
"""
self.__dict__.update(self.model_extra)
# self.__dict__.update(self.model_extra)
dicto = self.improved_dict()
instance, code, msg = BasicSubmission.query_or_create(submission_type=self.submission_type['value'], rsl_plate_num=self.rsl_plate_num['value'])
result = Result(msg=msg, code=code)
self.handle_duplicate_samples()
logger.debug(f"Here's our list of duplicate removed samples: {self.samples}")
for key, value in self.__dict__.items():
# for key, value in self.__dict__.items():
for key, value in dicto.items():
if isinstance(value, dict):
value = value['value']
logger.debug(f"Setting {key} to {value}")
@@ -600,6 +608,7 @@ class PydSubmission(BaseModel, extra='allow'):
info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
reagents = self.reagents
if len(reagents + list(info.keys())) == 0:
# logger.warning("No info to fill in, returning")
return None
logger.debug(f"We have blank info and/or reagents in the excel sheet.\n\tLet's try to fill them in.")
# extraction_kit = lookup_kit_types(ctx=self.ctx, name=self.extraction_kit['value'])
@@ -610,6 +619,7 @@ class PydSubmission(BaseModel, extra='allow'):
# logger.debug(f"Missing reagents going into autofile: {pformat(reagents)}")
# logger.debug(f"Missing info going into autofile: {pformat(info)}")
new_reagents = []
# logger.debug("Constructing reagent map and values")
for reagent in reagents:
new_reagent = {}
new_reagent['type'] = reagent.type
@@ -626,6 +636,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.error(f"Couldn't get name due to {e}")
new_reagents.append(new_reagent)
new_info = []
# logger.debug("Constructing info map and values")
for k,v in info.items():
try:
new_item = {}
@@ -678,6 +689,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Sample info: {pformat(sample_info)}")
logger.debug(f"Workbook sheets: {workbook.sheetnames}")
worksheet = workbook[sample_info["lookup_table"]['sheet']]
# logger.debug("Sorting samples by row/column")
samples = sorted(self.samples, key=attrgetter('column', 'row'))
submission_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
samples = submission_obj.adjust_autofill_samples(samples=samples)
@@ -704,6 +716,15 @@ class PydSubmission(BaseModel, extra='allow'):
return workbook
def autofill_equipment(self, workbook:Workbook) -> Workbook:
"""
Fill in equipment on the excel sheet
Args:
workbook (Workbook): Input excel workbook
Returns:
Workbook: Updated excel workbook
"""
equipment_map = SubmissionType.query(name=self.submission_type['value']).construct_equipment_map()
logger.debug(f"Equipment map: {equipment_map}")
# See if all equipment has a location map
@@ -712,6 +733,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.warning("Creating 'Equipment' sheet to hold unmapped equipment")
workbook.create_sheet("Equipment")
equipment = []
# logger.debug("Contructing equipment info map/values")
for ii, equip in enumerate(self.equipment, start=1):
loc = [item for item in equipment_map if item['role'] == equip.role][0]
try:
@@ -746,12 +768,10 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
str: Output filename
"""
env = jinja_template_loading()
template = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type).filename_template()
logger.debug(f"Using template string: {template}")
template = env.from_string(template)
render = template.render(**self.improved_dict(dictionaries=False)).replace("/", "")
logger.debug(f"Template rendered as: {render}")
# logger.debug(f"Using template string: {template}")
render = RSLNamer.construct_export_name(template=template, **self.improved_dict(dictionaries=False)).replace("/", "")
# logger.debug(f"Template rendered as: {render}")
return render
def check_kit_integrity(self, reagenttypes:list=[]) -> Report:
@@ -785,6 +805,7 @@ class PydSubmission(BaseModel, extra='allow'):
return report
class PydContact(BaseModel):
name: str
phone: str|None
email: str|None
@@ -818,7 +839,8 @@ class PydOrganization(BaseModel):
value = [item.toSQL() for item in getattr(self, field)]
case _:
value = getattr(self, field)
instance.set_attribute(name=field, value=value)
# instance.set_attribute(name=field, value=value)
instance.__setattr__(name=field, value=value)
return instance
class PydReagentType(BaseModel):
@@ -845,19 +867,16 @@ class PydReagentType(BaseModel):
Returns:
ReagentType: ReagentType instance
"""
# instance: ReagentType = lookup_reagent_types(ctx=ctx, name=self.name)
instance: ReagentType = ReagentType.query(name=self.name)
if instance == None:
instance = ReagentType(name=self.name, eol_ext=self.eol_ext)
logger.debug(f"This is the reagent type instance: {instance.__dict__}")
try:
# assoc = lookup_reagenttype_kittype_association(ctx=ctx, reagent_type=instance, kit_type=kit)
assoc = KitTypeReagentTypeAssociation.query(reagent_type=instance, kit_type=kit)
except StatementError:
assoc = None
if assoc == None:
assoc = KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=instance, uses=self.uses, required=self.required)
# kit.kit_reagenttype_associations.append(assoc)
return instance
class PydKit(BaseModel):
@@ -872,13 +891,10 @@ class PydKit(BaseModel):
Returns:
Tuple[KitType, Report]: KitType instance and report of results.
"""
# result = dict(message=None, status='Information')
report = Report()
# instance = lookup_kit_types(ctx=ctx, name=self.name)
instance = KitType.query(name=self.name)
if instance == None:
instance = KitType(name=self.name)
# instance.reagent_types = [item.toSQL(ctx, instance) for item in self.reagent_types]
[item.toSQL(instance) for item in self.reagent_types]
return instance, report
@@ -888,7 +904,17 @@ class PydEquipmentRole(BaseModel):
equipment: List[PydEquipment]
processes: List[str]|None
def toForm(self, parent, submission_type, used):
def toForm(self, parent, used:list) -> "RoleComboBox":
"""
Creates a widget for user input into this class.
Args:
parent (_type_): parent widget
used (list): list of equipment already added to submission
Returns:
RoleComboBox: widget
"""
from frontend.widgets.equipment_usage import RoleComboBox
return RoleComboBox(parent=parent, role=self, submission_type=submission_type, used=used)
return RoleComboBox(parent=parent, role=self, used=used)

View File

@@ -2,5 +2,3 @@
Contains all operations for creating charts, graphs and visual effects.
'''
from .control_charts import *
from .barcode import *
from .plate_map import *

View File

@@ -1,19 +0,0 @@
from reportlab.graphics.barcode import createBarcodeImageInMemory
from reportlab.graphics.shapes import Drawing
from reportlab.lib.units import mm
def make_plate_barcode(text:str, width:int=100, height:int=25) -> Drawing:
"""
Creates a barcode image for a given str.
Args:
text (str): Input string
width (int, optional): Width (pixels) of image. Defaults to 100.
height (int, optional): Height (pixels) of image. Defaults to 25.
Returns:
Drawing: image object
"""
# return createBarcodeDrawing('Code128', value=text, width=200, height=50, humanReadable=True)
return createBarcodeImageInMemory('Code128', value=text, width=width*mm, height=height*mm, humanReadable=True, format="png")

View File

@@ -12,7 +12,6 @@ from frontend.widgets.functions import select_save_file
logger = logging.getLogger(f"submissions.{__name__}")
def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
"""
Constructs figures based on parsed pandas dataframe.
@@ -40,7 +39,6 @@ def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure
genera.append("")
df['genus'] = df['genus'].replace({'\*':''}, regex=True).replace({"NaN":"Unknown"})
df['genera'] = genera
# df = df.dropna()
# remove original runs, using reruns if applicable
df = drop_reruns_from_df(ctx=ctx, df=df)
# sort by and exclude from
@@ -224,4 +222,4 @@ def construct_html(figure:Figure) -> str:
else:
html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>'
return html
return html

View File

@@ -1,121 +0,0 @@
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from tools import check_if_app, jinja_template_loading
import logging, sys
logger = logging.getLogger(f"submissions.{__name__}")
def make_plate_map(sample_list:list) -> Image:
"""
Makes a pillow image of a plate from hitpicks
Args:
sample_list (list): list of sample dictionaries from the hitpicks
Returns:
Image: Image of the 96 well plate with positive samples in red.
"""
# If we can't get a plate number, do nothing
try:
plate_num = sample_list[0]['plate_name']
except IndexError as e:
logger.error(f"Couldn't get a plate number. Will not make plate.")
return None
except TypeError as e:
logger.error(f"No samples for this plate. Nothing to do.")
return None
# Make an 8 row, 12 column, 3 color ints array, filled with white by default
grid = np.full((8,12,3),255, dtype=np.uint8)
# Go through samples and change its row/column to red if positive, else blue
for sample in sample_list:
logger.debug(f"sample keys: {list(sample.keys())}")
# set color of square
if sample['positive']:
colour = [255,0,0]
else:
if 'colour' in sample.keys():
colour = sample['colour']
else:
colour = [0,0,255]
grid[int(sample['row'])-1][int(sample['column'])-1] = colour
# Create pixel image from the grid and enlarge
img = Image.fromarray(grid).resize((1200, 800), resample=Image.NEAREST)
# create a drawer over the image
draw = ImageDraw.Draw(img)
# draw grid over the image
y_start = 0
y_end = img.height
step_size = int(img.width / 12)
for x in range(0, img.width, step_size):
line = ((x, y_start), (x, y_end))
draw.line(line, fill=128)
x_start = 0
x_end = img.width
step_size = int(img.height / 8)
for y in range(0, img.height, step_size):
line = ((x_start, y), (x_end, y))
draw.line(line, fill=128)
del draw
old_size = img.size
new_size = (1300, 900)
# create a new, larger white image to hold the annotations
new_img = Image.new("RGB", new_size, "White")
box = tuple((n - o) // 2 for n, o in zip(new_size, old_size))
# paste plate map into the new image
new_img.paste(img, box)
# create drawer over the new image
draw = ImageDraw.Draw(new_img)
if check_if_app():
font_path = Path(sys._MEIPASS).joinpath("files", "resources")
else:
font_path = Path(__file__).parents[2].joinpath('resources').absolute()
logger.debug(f"Font path: {font_path}")
font = ImageFont.truetype(font_path.joinpath('arial.ttf').__str__(), 32)
row_dict = ["A", "B", "C", "D", "E", "F", "G", "H"]
# write the plate number on the image
draw.text((100, 850),plate_num,(0,0,0),font=font)
# write column numbers
for num in range(1,13):
x = (num * 100) - 10
draw.text((x, 0), str(num), (0,0,0),font=font)
# write row letters
for num in range(1,9):
letter = row_dict[num-1]
y = (num * 100) - 10
draw.text((10, y), letter, (0,0,0),font=font)
return new_img
def make_plate_map_html(sample_list:list, plate_rows:int=8, plate_columns=12) -> str:
"""
Constructs an html based plate map.
Args:
sample_list (list): List of submission samples
plate_rows (int, optional): Number of rows in the plate. Defaults to 8.
plate_columns (int, optional): Number of columns in the plate. Defaults to 12.
Returns:
str: html output string.
"""
for sample in sample_list:
if sample['positive']:
sample['background_color'] = "#f10f07"
else:
if "colour" in sample.keys():
sample['background_color'] = "#69d84f"
else:
sample['background_color'] = "#80cbc4"
output_samples = []
for column in range(1, plate_columns+1):
for row in range(1, plate_rows+1):
try:
well = [item for item in sample_list if item['row'] == row and item['column']==column][0]
except IndexError:
well = dict(name="", row=row, column=column, background_color="#ffffff")
output_samples.append(well)
env = jinja_template_loading()
template = env.get_template("plate_map.html")
html = template.render(samples=output_samples, PLATE_ROWS=plate_rows, PLATE_COLUMNS=plate_columns)
return html

View File

@@ -52,7 +52,6 @@ class App(QMainWindow):
self._createMenuBar()
self._createToolBar()
self._connectActions()
# self._controls_getter()
self.show()
self.statusBar().showMessage('Ready', 5000)
@@ -114,14 +113,10 @@ class App(QMainWindow):
self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results)
self.addReagentAction.triggered.connect(self.add_reagent)
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
# self.addKitAction.triggered.connect(self.add_kit)
# self.addOrgAction.triggered.connect(self.add_org)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
self.helpAction.triggered.connect(self.showAbout)
self.docsAction.triggered.connect(self.openDocs)
# self.constructFS.triggered.connect(self.construct_first_strand)
# self.table_widget.formwidget.import_drag.connect(self.importSubmission)
self.searchLog.triggered.connect(self.runSearch)
def showAbout(self):

View File

@@ -4,9 +4,9 @@ from PyQt6.QtWidgets import (
QDateEdit, QLabel, QSizePolicy
)
from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control#, get_control_subtypes
from backend.db import ControlType, Control
from PyQt6.QtCore import QDate, QSize
import logging, sys
import logging
from tools import Report, Result
from backend.excel.reports import convert_data_list_to_df
from frontend.visualizations.control_charts import create_charts, construct_html
@@ -88,9 +88,7 @@ class ControlsViewer(QWidget):
self.mode = self.mode_typer.currentText()
self.sub_typer.clear()
# lookup subtypes
# sub_types = get_control_subtypes(type=self.con_type, mode=self.mode)
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
# sub_types = lookup_controls(ctx=obj.ctx, control_type=obj.con_type)
if sub_types != []:
# block signal that will rerun controls getter and update sub_typer
with QSignalBlocker(self.sub_typer) as blocker:
@@ -103,7 +101,6 @@ class ControlsViewer(QWidget):
self.chart_maker()
self.report.add_result(report)
def chart_maker_function(self):
"""
Create html chart for controls reporting

View File

@@ -2,9 +2,10 @@ from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QHBoxLayout,
QVBoxLayout, QDialogButtonBox)
from backend.db.models import SubmissionType, Equipment, BasicSubmission
from backend.db.models import Equipment, BasicSubmission
from backend.validators.pydant import PydEquipment, PydEquipmentRole
import logging
from typing import List
logger = logging.getLogger(f"submissions.{__name__}")
@@ -24,19 +25,29 @@ class EquipmentUsage(QDialog):
self.populate_form()
def populate_form(self):
"""
Create form widgets
"""
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
label = self.LabelRow(parent=self)
self.layout.addWidget(label)
# logger.debug("Creating widgets for equipment")
for eq in self.opt_equipment:
widg = eq.toForm(parent=self, submission_type=self.submission.submission_type, used=self.used_equipment)
widg = eq.toForm(parent=self, used=self.used_equipment)
self.layout.addWidget(widg)
widg.update_processes()
self.layout.addWidget(self.buttonBox)
def parse_form(self):
def parse_form(self) -> List[PydEquipment]:
"""
Pull info from all RoleComboBox widgets
Returns:
List[PydEquipment]: All equipment pulled from widgets
"""
output = []
for widget in self.findChildren(QWidget):
match widget:
@@ -63,43 +74,18 @@ class EquipmentUsage(QDialog):
self.setLayout(self.layout)
def check_all(self):
"""
Toggles all checkboxes in the form
"""
for object in self.parent().findChildren(QCheckBox):
object.setChecked(self.check.isChecked())
class EquipmentCheckBox(QWidget):
def __init__(self, parent, equipment:PydEquipment) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
self.label = QLabel()
self.label.setMaximumWidth(125)
self.label.setMinimumWidth(125)
self.check = QCheckBox()
if equipment.static:
self.check.setChecked(True)
if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})"
else:
text = equipment.name
self.setObjectName(equipment.name)
self.label.setText(text)
self.layout.addWidget(self.label)
self.layout.addWidget(self.check)
self.setLayout(self.layout)
def parse_form(self) -> str|None:
if self.check.isChecked():
return self.objectName()
else:
return None
# TODO: Figure out how this is working again
class RoleComboBox(QWidget):
def __init__(self, parent, role:PydEquipmentRole, submission_type:SubmissionType, used:list) -> None:
def __init__(self, parent, role:PydEquipmentRole, used:list) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
# label = QLabel()
# label.setText(pool.name)
self.role = role
self.check = QCheckBox()
if role.name in used:
@@ -111,14 +97,10 @@ class RoleComboBox(QWidget):
self.box.setMinimumWidth(200)
self.box.addItems([item.name for item in role.equipment])
self.box.currentTextChanged.connect(self.update_processes)
# self.check = QCheckBox()
# self.layout.addWidget(label)
self.process = QComboBox()
self.process.setMaximumWidth(200)
self.process.setMinimumWidth(200)
self.process.setEditable(True)
# self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
# self.process.addItems(role.processes)
self.layout.addWidget(self.check)
label = QLabel(f"{role.name}:")
label.setMinimumWidth(200)
@@ -127,11 +109,12 @@ class RoleComboBox(QWidget):
self.layout.addWidget(label)
self.layout.addWidget(self.box)
self.layout.addWidget(self.process)
# self.layout.addWidget(self.check)
self.setLayout(self.layout)
# self.update_processes()
def update_processes(self):
"""
Changes processes when equipment is changed
"""
equip = self.box.currentText()
logger.debug(f"Updating equipment: {equip}")
equip2 = [item for item in self.role.equipment if item.name==equip][0]
@@ -139,10 +122,16 @@ class RoleComboBox(QWidget):
self.process.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.processes])
def parse_form(self) -> str|None:
def parse_form(self) -> PydEquipment|None:
"""
Creates PydEquipment for values in form
Returns:
PydEquipment|None: PydEquipment matching form
"""
eq = Equipment.query(name=self.box.currentText())
# if self.check.isChecked():
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
# else:
# return None
try:
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
except Exception as e:
logger.error(f"Could create PydEquipment due to: {e}")

View File

@@ -1,7 +1,7 @@
# import required modules
# from PyQt6.QtCore import Qt
"""
Gel box for artic quality control
"""
from PyQt6.QtWidgets import *
# import sys
from PyQt6.QtWidgets import QWidget
import numpy as np
import pyqtgraph as pg
@@ -9,11 +9,17 @@ from PyQt6.QtGui import *
from PyQt6.QtCore import *
from PIL import Image
import numpy as np
import logging
from pprint import pformat
from typing import Tuple, List
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
# Main window class
class GelBox(QDialog):
def __init__(self, parent, img_path):
def __init__(self, parent, img_path:str|Path):
super().__init__(parent)
# setting title
self.setWindowTitle("PyQtGraph")
@@ -27,11 +33,12 @@ class GelBox(QDialog):
# calling method
self.UiComponents()
# showing all the widgets
# self.show()
# method for components
def UiComponents(self):
# widget = QWidget()
"""
Create widgets in ui
"""
# setting configuration options
pg.setConfigOptions(antialias=True)
# creating image view object
@@ -39,33 +46,41 @@ class GelBox(QDialog):
img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0]))
layout = QGridLayout()
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
self.core_number = QLineEdit()
layout.addWidget(self.core_number, 0,2)
# setting this layout to the widget
# widget.setLayout(layout)
# plot window goes on right side, spanning 3 rows
layout.addWidget(self.imv, 0, 0,20,20)
layout.addWidget(self.imv, 1, 1,20,20)
# setting this widget as central widget of the main window
self.form = ControlsForm(parent=self)
layout.addWidget(self.form,21,1,1,4)
layout.addWidget(self.form,22,1,1,4)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 21, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
# self.buttonBox.clicked.connect(self.submit)
layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
self.setLayout(layout)
def parse_form(self):
return self.img_path, self.form.parse_form()
def parse_form(self) -> Tuple[str, str|Path, list]:
"""
Get relevant values from self/form
Returns:
Tuple[str, str|Path, list]: output values
"""
dna_core_submission_number = self.core_number.text()
return dna_core_submission_number, self.img_path, self.form.parse_form()
class ControlsForm(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QGridLayout()
columns = []
rows = []
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
label = QLabel(item)
self.layout.addWidget(label, 0, iii,1,1)
if iii > 1:
@@ -85,11 +100,22 @@ class ControlsForm(QWidget):
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
self.setLayout(self.layout)
def parse_form(self):
dicto = {}
def parse_form(self) -> List[dict]:
"""
Pulls the controls statuses from the form.
Returns:
List[dict]: output of values
"""
output = []
for le in self.findChildren(QLineEdit):
label = [item.strip() for item in le.objectName().split(" : ")]
if label[0] not in dicto.keys():
dicto[label[0]] = {}
dicto[label[0]][label[1]] = le.text()
return dicto
try:
dicto = [item for item in output if item['name']==label[0]][0]
except IndexError:
dicto = dict(name=label[0], values=[])
dicto['values'].append(dict(name=label[1], value=le.text()))
if label[0] not in [item['name'] for item in output]:
output.append(dicto)
logger.debug(pformat(output))
return output

View File

@@ -79,12 +79,10 @@ class KitAdder(QWidget):
"""
insert new reagent type row
"""
print(self.app)
# get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
self.grid.addWidget(reg_form, maxrow,0,1,4)
def submit(self) -> None:
@@ -118,6 +116,12 @@ class KitAdder(QWidget):
self.__init__(self.parent())
def parse_form(self) -> Tuple[dict, list]:
"""
Pulls reagent and general info from form
Returns:
Tuple[dict, list]: dict=info, list=reagents
"""
logger.debug(f"Hello from {self.__class__} parser!")
info = {}
reagents = []
@@ -188,10 +192,19 @@ class ReagentTypeForm(QWidget):
]
def remove(self):
"""
Destroys this row of reagenttype from the form
"""
self.setParent(None)
self.destroy()
def parse_form(self) -> dict:
"""
Pulls ReagentType info from the form.
Returns:
dict: _description_
"""
logger.debug(f"Hello from {self.__class__} parser!")
info = {}
info['eol'] = self.eol.value()

View File

@@ -25,7 +25,6 @@ class AddReagentForm(QDialog):
"""
def __init__(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, reagent_name:str|None=None) -> None:
super().__init__()
# self.ctx = ctx
if reagent_lot == None:
reagent_lot = reagent_type
@@ -41,7 +40,6 @@ class AddReagentForm(QDialog):
self.name_input.setObjectName("name")
self.name_input.setEditable(True)
self.name_input.setCurrentText(reagent_name)
# self.name_input.setText(reagent_name)
self.lot_input = QLineEdit()
self.lot_input.setObjectName("lot")
self.lot_input.setText(reagent_lot)
@@ -56,7 +54,6 @@ class AddReagentForm(QDialog):
# widget to get reagent type info
self.type_input = QComboBox()
self.type_input.setObjectName('type')
# self.type_input.addItems([item.name for item in lookup_reagent_types(ctx=ctx)])
self.type_input.addItems([item.name for item in ReagentType.query()])
logger.debug(f"Trying to find index of {reagent_type}")
# convert input to user friendly string?
@@ -169,7 +166,13 @@ class FirstStrandSalvage(QDialog):
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
def parse_form(self) -> dict:
"""
Pulls first strand info from form.
Returns:
dict: Output info
"""
return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}")
class LogParser(QDialog):
@@ -193,9 +196,15 @@ class LogParser(QDialog):
def filelookup(self):
"""
Select file to search
"""
self.fname = select_open_file(self, "tabular")
def runsearch(self):
"""
Gets total/percent occurences of string in tabular file.
"""
count: int = 0
total: int = 0
logger.debug(f"Current search term: {self.phrase_looker.currentText()}")

View File

@@ -47,7 +47,7 @@ class AlertPop(QMessageBox):
class KitSelector(QDialog):
"""
dialog to ask yes/no questions
dialog to input KitType manually
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
@@ -69,12 +69,18 @@ class KitSelector(QDialog):
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def getValues(self):
def getValues(self) -> str:
"""
Get KitType(str) from widget
Returns:
str: KitType as str
"""
return self.widget.currentText()
class SubmissionTypeSelector(QDialog):
"""
dialog to ask yes/no questions
dialog to input SubmissionType manually
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
@@ -97,5 +103,11 @@ class SubmissionTypeSelector(QDialog):
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
def parse_form(self) -> str:
"""
Pulls SubmissionType(str) from widget
Returns:
str: SubmissionType as str
"""
return self.widget.currentText()

View File

@@ -1,25 +1,25 @@
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
QLabel, QDialogButtonBox, QToolBar, QTextEdit)
from PyQt6.QtGui import QAction, QPixmap
QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt
from PyQt6 import QtPrintSupport
from backend.db.models import BasicSubmission
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from tools import check_if_app, jinja_template_loading
from tools import check_if_app
from .functions import select_save_file
from io import BytesIO
from tempfile import TemporaryFile, TemporaryDirectory
from pathlib import Path
from xhtml2pdf import pisa
import logging, base64
from getpass import getuser
from datetime import datetime
from pprint import pformat
from html2image import Html2Image
from PIL import Image
from typing import List
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
@@ -27,7 +27,6 @@ class SubmissionDetails(QDialog):
def __init__(self, parent, sub:BasicSubmission) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError:
@@ -36,19 +35,16 @@ class SubmissionDetails(QDialog):
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# sub = BasicSubmission.query(id=id)
self.base_dict = sub.to_dict(full_data=True)
logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k != 'samples'})}")
# don't want id
del self.base_dict['id']
logger.debug(f"Creating barcode.")
if not check_if_app():
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
logger.debug(f"Hitpicking plate...")
self.plate_dicto = sub.hitpick_plate()
self.base_dict['barcode'] = base64.b64encode(sub.make_plate_barcode(width=120, height=30)).decode('utf-8')
logger.debug(f"Making platemap...")
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
self.template = env.get_template("submission_details.html")
self.base_dict['platemap'] = sub.make_plate_map()
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView()
webview.setMinimumSize(900, 500)
@@ -63,21 +59,29 @@ class SubmissionDetails(QDialog):
btn.setParent(self)
btn.setFixedWidth(900)
btn.clicked.connect(self.export)
def export(self):
"""
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
del self.base_dict['platemap']
export_map = make_plate_map(self.plate_dicto)
image_io = BytesIO()
temp_dir = Path(TemporaryDirectory().name)
hti = Html2Image(output_path=temp_dir, size=(1200, 750))
temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name)
screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
export_map = Image.open(screenshot[0])
export_map = export_map.convert('RGB')
try:
export_map.save(image_io, 'JPEG')
except AttributeError:
logger.error(f"No plate map found")
self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
del self.base_dict['platemap']
self.html2 = self.template.render(sub=self.base_dict)
with open("test.html", "w") as fw:
fw.write(self.html2)
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(self.html2, dest=f)
@@ -88,73 +92,6 @@ class SubmissionDetails(QDialog):
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()
class BarcodeWindow(QDialog):
def __init__(self, rsl_num:str):
super().__init__()
# set the title
self.setWindowTitle("Image")
self.layout = QVBoxLayout()
# setting the geometry of window
self.setGeometry(0, 0, 400, 300)
# creating label
self.label = QLabel()
self.img = make_plate_barcode(rsl_num)
self.pixmap = QPixmap()
self.pixmap.loadFromData(self.img)
# adding image to label
self.label.setPixmap(self.pixmap)
# show all the widgets]
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.label)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self._createActions()
self._createToolBar()
self._connectActions()
def _createToolBar(self):
"""
adds items to menu bar
"""
toolbar = QToolBar("My main toolbar")
toolbar.addAction(self.printAction)
def _createActions(self):
"""
creates actions
"""
self.printAction = QAction("&Print", self)
def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
self.printAction.triggered.connect(self.print_barcode)
def print_barcode(self):
"""
Sends barcode image to printer.
"""
printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec():
self.handle_paint_request(printer, self.pixmap.toImage())
def handle_paint_request(self, printer:QtPrintSupport.QPrinter, im):
logger.debug(f"Hello from print handler.")
painter = QPainter(printer)
image = QPixmap.fromImage(im)
painter.drawPixmap(120, -20, image)
painter.end()
class SubmissionComment(QDialog):
"""
@@ -163,7 +100,6 @@ class SubmissionComment(QDialog):
def __init__(self, parent, submission:BasicSubmission) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent
print(f"App: {self.app}")
@@ -185,7 +121,7 @@ class SubmissionComment(QDialog):
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def parse_form(self):
def parse_form(self) -> List[dict]:
"""
Adds comment to submission object.
"""

View File

@@ -1,37 +1,22 @@
'''
Contains widgets specific to the submission summary and submission details.
'''
import base64, logging, json
from datetime import datetime
from io import BytesIO
import logging, json
from pprint import pformat
from PyQt6 import QtPrintSupport
from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea,
QMessageBox, QMenu, QLabel,
QDialogButtonBox, QToolBar
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QTableView, QMenu
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.models import BasicSubmission, Equipment
from PyQt6.QtGui import QAction, QCursor
from backend.db.models import BasicSubmission
from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from tools import Report, Result, get_first_blank_df_row, row_map
from xhtml2pdf import pisa
from .pop_ups import QuestionAsker
from .equipment_usage import EquipmentUsage
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker
import pandas as pd
from openpyxl.worksheet.worksheet import Worksheet
from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
@@ -89,20 +74,17 @@ class SubmissionsSheet(QTableView):
"""
super().__init__(parent)
self.app = self.parent()
# self.ctx = ctx
self.report = Report()
self.setData()
self.resizeColumnsToContents()
self.resizeRowsToContents()
self.setSortingEnabled(True)
# self.doubleClicked.connect(self.show_details)
self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
def setData(self) -> None:
"""
sets data in model
"""
# self.data = submissions_to_df()
self.data = BasicSubmission.submissions_to_df()
try:
self.data['id'] = self.data['id'].apply(str)
@@ -114,39 +96,6 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
# def show_details(self, submission:BasicSubmission) -> None:
# """
# creates detailed data to show in seperate window
# """
# logger.debug(f"Sheet.app: {self.app}")
# # index = (self.selectionModel().currentIndex())
# # value = index.sibling(index.row(),0).data()
# dlg = SubmissionDetails(parent=self, sub=submission)
# if dlg.exec():
# pass
def create_barcode(self) -> None:
"""
Generates a window for displaying barcode
"""
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),1).data()
logger.debug(f"Selected value: {value}")
dlg = BarcodeWindow(value)
if dlg.exec():
dlg.print_barcode()
def add_comment(self) -> None:
"""
Generates a text editor window.
"""
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),1).data()
logger.debug(f"Selected value: {value}")
dlg = SubmissionComment(parent=self, rsl=value)
if dlg.exec():
dlg.add_comment()
def contextMenuEvent(self, event):
"""
Creates actions for right click menu events.
@@ -158,21 +107,6 @@ class SubmissionsSheet(QTableView):
id = id.sibling(id.row(),0).data()
submission = BasicSubmission.query(id=id)
self.menu = QMenu(self)
# renameAction = QAction('Delete', self)
# detailsAction = QAction('Details', self)
# commentAction = QAction("Add Comment", self)
# equipAction = QAction("Add Equipment", self)
# backupAction = QAction("Export", self)
# renameAction.triggered.connect(lambda: self.delete_item(submission))
# detailsAction.triggered.connect(lambda: self.show_details(submission))
# commentAction.triggered.connect(lambda: self.add_comment(submission))
# backupAction.triggered.connect(lambda: self.regenerate_submission_form(submission))
# equipAction.triggered.connect(lambda: self.add_equipment(submission))
# self.menu.addAction(detailsAction)
# self.menu.addAction(renameAction)
# self.menu.addAction(commentAction)
# self.menu.addAction(backupAction)
# self.menu.addAction(equipAction)
self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys():
logger.debug(f"Adding {k}")
@@ -183,57 +117,21 @@ class SubmissionsSheet(QTableView):
self.menu.popup(QCursor.pos())
def triggered_action(self, action_name:str):
"""
Calls the triggered action from the context menu
Args:
action_name (str): name of the action from the menu
"""
logger.debug(f"Action: {action_name}")
logger.debug(f"Responding with {self.con_actions[action_name]}")
func = self.con_actions[action_name]
func(obj=self)
def add_equipment(self):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value)
def add_equipment_function(self, submission:BasicSubmission):
# submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission)
if dlg.exec():
equipment = dlg.parse_form()
logger.debug(f"We've got equipment: {equipment}")
for equip in equipment:
e = Equipment.query(name=equip.name)
# assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# process = Process.query(name=equip.processes)
# assoc.process = process
# assoc.role = equip.role
_, assoc = equip.toSQL(submission=submission)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
else:
pass
def delete_item(self, submission:BasicSubmission):
"""
Confirms user deletion and sends id to backend for deletion.
Args:
event (_type_): the item of interest
"""
# index = (self.selectionModel().currentIndex())
# value = index.sibling(index.row(),0).data()
# logger.debug(index)
# msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {submission.rsl_plate_num}?\n")
if msg.exec():
# delete_submission(id=value)
submission.delete()
else:
return
self.setData()
def link_extractions(self):
"""
Pull extraction logs into the db
"""
self.link_extractions_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -306,6 +204,9 @@ class SubmissionsSheet(QTableView):
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def link_pcr(self):
"""
Pull pcr logs into the db
"""
self.link_pcr_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -376,6 +277,9 @@ class SubmissionsSheet(QTableView):
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def generate_report(self):
"""
Make a report
"""
self.generate_report_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -436,12 +340,3 @@ class SubmissionsSheet(QTableView):
cell.style = 'Currency'
writer.close()
self.report.add_result(report)
def regenerate_submission_form(self, submission:BasicSubmission):
# index = (self.selectionModel().currentIndex())
# value = index.sibling(index.row(),0).data()
# logger.debug(index)
# sub = BasicSubmission.query(id=value)
fname = select_save_file(self, default_name=submission.to_pydantic().construct_filename(), extension="xlsx")
submission.backup(fname=fname, full_backup=False)

View File

@@ -2,21 +2,14 @@ from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QDateEdit
QLineEdit, QSpinBox
)
from sqlalchemy import FLOAT, INTEGER
from sqlalchemy.orm.attributes import InstrumentedAttribute
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentRoleAssociation, BasicSubmission
from backend.validators import PydReagentType, PydKit
from backend.db import SubmissionType, BasicSubmission
import logging
from pprint import pformat
from tools import Report
from typing import Tuple
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionTypeAdder(QWidget):
@@ -46,35 +39,21 @@ class SubmissionTypeAdder(QWidget):
self.grid.addWidget(template_selector,3,1)
self.template_label = QLabel("None")
self.grid.addWidget(self.template_label,3,2)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost']
self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)}
self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude}
for iii, key in enumerate(self.columns):
idx = iii + 4
# convert field name to human readable.
# field_name = key
# self.grid.addWidget(QLabel(field_name),idx,0)
# print(self.columns[key].type)
# match self.columns[key].type:
# case FLOAT():
# add_widget = QDoubleSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case INTEGER():
# add_widget = QSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case _:
# add_widget = QLineEdit()
# add_widget.setObjectName(key)
self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
scroll.setWidget(scrollContent)
self.submit_btn.clicked.connect(self.submit)
template_selector.clicked.connect(self.get_template_path)
def submit(self):
"""
Create SubmissionType and send to db
"""
info = self.parse_form()
ST = SubmissionType(name=self.st_name.text(), info_map=info)
try:
@@ -84,11 +63,20 @@ class SubmissionTypeAdder(QWidget):
logger.error(f"Could not find template file: {self.template_path}")
ST.save(ctx=self.app.ctx)
def parse_form(self):
def parse_form(self) -> dict:
"""
Pulls info from form
Returns:
dict: information from form
"""
widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
return {widget.objectName():widget.parse_form() for widget in widgets}
def get_template_path(self):
"""
Sets path for loading a submission form template
"""
self.template_path = select_open_file(obj=self, file_extension="xlsx")
self.template_label.setText(self.template_path.__str__())
@@ -113,7 +101,13 @@ class InfoWidget(QWidget):
self.column.setObjectName("column")
grid.addWidget(self.column,2,3)
def parse_form(self):
def parse_form(self) -> dict:
"""
Pulls info from the Info form.
Returns:
dict: sheets, row, column
"""
return dict(
sheets = self.sheet.text().split(","),
row = self.row.value(),

View File

@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import pyqtSignal
from pathlib import Path
from . import select_open_file, select_save_file
import logging, difflib, inspect, json, sys
import logging, difflib, inspect, json
from pathlib import Path
from tools import Report, Result, check_not_nan
from backend.excel.parser import SheetParser, PCRParser
@@ -16,7 +16,6 @@ from backend.db import (
)
from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop
# from .misc import ReagentFormWidget
from typing import List, Tuple
from datetime import date
@@ -24,6 +23,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionFormContainer(QWidget):
# A signal carrying a path
import_drag = pyqtSignal(Path)
def __init__(self, parent: QWidget) -> None:
@@ -31,19 +31,24 @@ class SubmissionFormContainer(QWidget):
super().__init__(parent)
self.app = self.parent().parent()
self.report = Report()
# self.parent = parent
self.setAcceptDrops(True)
# if import_drag is emitted, importSubmission will fire
self.import_drag.connect(self.importSubmission)
def dragEnterEvent(self, event):
"""
Allow drag if file.
"""
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Sets filename when file dropped
"""
fname = Path([u.toLocalFile() for u in event.mimeData().urls()][0])
logger.debug(f"App: {self.app}")
self.app.last_dir = fname.parent
self.import_drag.emit(fname)
@@ -52,7 +57,6 @@ class SubmissionFormContainer(QWidget):
"""
import submission from excel sheet into form
"""
# from .main_window_functions import import_submission_function
self.app.raise_()
self.app.activateWindow()
self.import_submission_function(fname)
@@ -62,6 +66,9 @@ class SubmissionFormContainer(QWidget):
self.app.result_reporter()
def scrape_reagents(self, *args, **kwargs):
"""
Called when a reagent is changed.
"""
caller = inspect.stack()[1].function.__repr__().replace("'", "")
logger.debug(f"Args: {args}, kwargs: {kwargs}")
self.scrape_reagents_function(args[0], caller=caller)
@@ -80,7 +87,6 @@ class SubmissionFormContainer(QWidget):
NOTE: this will not change self.reagents which should be fine
since it's only used when looking up
"""
# from .main_window_functions import kit_integrity_completion_function
self.kit_integrity_completion_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -94,14 +100,12 @@ class SubmissionFormContainer(QWidget):
"""
Attempt to add sample to database when 'submit' button clicked
"""
# from .main_window_functions import submit_new_sample_function
self.submit_new_sample_function()
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
def export_csv(self, fname:Path|None=None):
# from .main_window_functions import export_csv_function
self.export_csv_function(fname)
def import_submission_function(self, fname:Path|None=None):
@@ -116,12 +120,11 @@ class SubmissionFormContainer(QWidget):
"""
logger.debug(f"\n\nStarting Import...\n\n")
report = Report()
# logger.debug(obj.ctx)
# initialize samples
try:
self.form.setParent(None)
except AttributeError:
pass
# initialize samples
self.samples = []
self.missing_info = []
# set file dialog
@@ -129,7 +132,6 @@ class SubmissionFormContainer(QWidget):
fname = select_open_file(self, file_extension="xlsx")
logger.debug(f"Attempting to parse file: {fname}")
if not fname.exists():
# result = dict(message=f"File {fname.__str__()} not found.", status="critical")
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
self.report.add_result(report)
return
@@ -141,14 +143,9 @@ class SubmissionFormContainer(QWidget):
return
except AttributeError:
self.prsr = SheetParser(ctx=self.app.ctx, filepath=fname)
# try:
logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}")
self.pyd = self.prsr.to_pydantic()
logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n")
# except Exception as e:
# report.add_result(Result(msg=f"Problem creating pydantic model:\n\n{e}", status="Critical"))
# self.report.add_result(report)
# return
self.form = self.pyd.toForm(parent=self)
self.layout().addWidget(self.form)
kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input
@@ -176,11 +173,8 @@ class SubmissionFormContainer(QWidget):
"""
self.form.reagents = []
logger.debug(f"\n\n{caller}\n\n")
# assert caller == "import_submission_function"
report = Report()
logger.debug(f"Extraction kit: {extraction_kit}")
# obj.reagents = []
# obj.missing_reagents = []
# Remove previous reagent widgets
try:
old_reagents = self.form.find_widgets()
@@ -191,14 +185,6 @@ class SubmissionFormContainer(QWidget):
for reagent in old_reagents:
if isinstance(reagent, ReagentFormWidget) or isinstance(reagent, QPushButton):
reagent.setParent(None)
# reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
# logger.debug(f"Got reagents: {reagents}")
# for reagent in obj.prsr.sub['reagents']:
# # create label
# if reagent.parsed:
# obj.reagents.append(reagent)
# else:
# obj.missing_reagents.append(reagent)
match caller:
case "import_submission_function":
self.form.reagents = self.prsr.sub['reagents']
@@ -231,11 +217,9 @@ class SubmissionFormContainer(QWidget):
logger.debug(f"Kit selector: {kit_widget}")
# get current kit being used
self.ext_kit = kit_widget.currentText()
# for reagent in obj.pyd.reagents:
for reagent in self.form.reagents:
logger.debug(f"Creating widget for {reagent}")
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
# add_widget.setParent(sub_form_container.form)
self.form.layout().addWidget(add_widget)
if reagent.missing:
missing_reagents.append(reagent)
@@ -275,7 +259,6 @@ class SubmissionFormContainer(QWidget):
self.pyd: PydSubmission = self.form.parse_form()
logger.debug(f"Submission: {pformat(self.pyd)}")
logger.debug("Checking kit integrity...")
# result = check_kit_integrity(sub=self.pyd)
result = self.pyd.check_kit_integrity()
report.add_result(result)
if len(result.results) > 0:
@@ -283,7 +266,6 @@ class SubmissionFormContainer(QWidget):
return
base_submission, result = self.pyd.toSQL()
# logger.debug(f"Base submission: {base_submission.to_dict()}")
# sys.exit()
# check output message for issues
match result.code:
# code 0: everything is fine.
@@ -309,9 +291,7 @@ class SubmissionFormContainer(QWidget):
# add reagents to submission object
for reagent in base_submission.reagents:
# logger.debug(f"Updating: {reagent} with {reagent.lot}")
# update_last_used(reagent=reagent, kit=base_submission.extraction_kit)
reagent.update_last_used(kit=base_submission.extraction_kit)
# sys.exit()
# logger.debug(f"Here is the final submission: {pformat(base_submission.__dict__)}")
# logger.debug(f"Parsed reagents: {pformat(base_submission.reagents)}")
# logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
@@ -324,24 +304,15 @@ class SubmissionFormContainer(QWidget):
# reset form
self.form.setParent(None)
# logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
# wkb = self.pyd.autofill_excel()
# if wkb != None:
# fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx")
# try:
# wkb.save(filename=fname.__str__())
# except PermissionError:
# logger.error("Hit a permission error when saving workbook. Cancelled?")
# if hasattr(self.pyd, 'csv'):
# dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
# if dlg.exec():
# fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv")
# try:
# self.pyd.csv.to_csv(fname.__str__(), index=False)
# except PermissionError:
# logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
self.report.add_result(report)
def export_csv_function(self, fname:Path|None=None):
"""
Save the submission's csv file.
Args:
fname (Path | None, optional): Input filename. Defaults to None.
"""
if isinstance(fname, bool) or fname == None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
try:
@@ -351,6 +322,9 @@ class SubmissionFormContainer(QWidget):
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
def import_pcr_results(self):
"""
Pull QuantStudio results into db
"""
self.import_pcr_results_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -370,7 +344,6 @@ class SubmissionFormContainer(QWidget):
fname = select_open_file(self, file_extension="xlsx")
parser = PCRParser(filepath=fname)
logger.debug(f"Attempting lookup for {parser.plate_num}")
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
sub = BasicSubmission.query(rsl_number=parser.plate_num)
try:
logger.debug(f"Found submission: {sub.rsl_plate_num}")
@@ -378,14 +351,11 @@ class SubmissionFormContainer(QWidget):
# If no plate is found, may be because this is a repeat. Lop off the '-1' or '-2' and repeat
logger.error(f"Submission of number {parser.plate_num} not found. Attempting rescue of plate repeat.")
parser.plate_num = "-".join(parser.plate_num.split("-")[:-1])
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=parser.plate_num)
sub = BasicSubmission.query(rsl_number=parser.plate_num)
try:
logger.debug(f"Found submission: {sub.rsl_plate_num}")
except AttributeError:
logger.error(f"Rescue of {parser.plate_num} failed.")
# return obj, dict(message="Couldn't find a submission with that RSL number.", status="warning")
self.report.add_result(Result(msg="Couldn't find a submission with that RSL number.", status="Warning"))
return
# Check if PCR info already exists
@@ -407,7 +377,6 @@ class SubmissionFormContainer(QWidget):
logger.debug(f"Final pcr info for {sub.rsl_plate_num}: {sub.pcr_info}")
else:
sub.pcr_info = json.dumps([parser.pcr])
# obj.ctx.database_session.add(sub)
logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}")
logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}")
sub.save(original=False)
@@ -419,18 +388,13 @@ class SubmissionFormContainer(QWidget):
sample_dict = [item for item in parser.samples if item['sample']==sample.rsl_number][0]
except IndexError:
continue
# update_subsampassoc_with_pcr(submission=sub, sample=sample, input_dict=sample_dict)
sub.update_subsampassoc(sample=sample, input_dict=sample_dict)
self.report.add_result(Result(msg=f"We added PCR info to {sub.rsl_plate_num}.", status='Information'))
# return obj, result
class SubmissionFormWidget(QWidget):
def __init__(self, parent: QWidget, **kwargs) -> None:
super().__init__(parent)
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn"
# ]
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment', 'equipment']
self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
layout = QVBoxLayout()
@@ -441,25 +405,53 @@ class SubmissionFormWidget(QWidget):
layout.addWidget(add_widget)
else:
setattr(self, k, v)
self.setLayout(layout)
def create_widget(self, key:str, value:dict, submission_type:str|None=None):
def create_widget(self, key:str, value:dict, submission_type:str|None=None) -> "self.InfoItem":
"""
Make an InfoItem widget to hold a field
Args:
key (str): Name of the field
value (dict): Value of field
submission_type (str | None, optional): Submissiontype as str. Defaults to None.
Returns:
self.InfoItem: Form widget to hold name:value
"""
if key not in self.ignore:
return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
return None
def clear_form(self):
"""
Removes all form widgets
"""
for item in self.findChildren(QWidget):
item.setParent(None)
def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
"""
Gets all widgets filtered by object name
Args:
object_name (str | None, optional): name to filter by. Defaults to None.
Returns:
List[QWidget]: Widgets matching filter
"""
query = self.findChildren(QWidget)
if object_name != None:
query = [widget for widget in query if widget.objectName()==object_name]
return query
def parse_form(self) -> PydSubmission:
"""
Transforms form info into PydSubmission
Returns:
PydSubmission: Pydantic submission object
"""
logger.debug(f"Hello from form parser!")
info = {}
reagents = []
@@ -483,8 +475,6 @@ class SubmissionFormWidget(QWidget):
value = getattr(self, item)
logger.debug(f"Setting {item}")
info[item] = value
# app = self.parent().parent().parent().parent().parent().parent().parent().parent
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
submission = PydSubmission(reagents=reagents, **info)
return submission
@@ -513,7 +503,13 @@ class SubmissionFormWidget(QWidget):
case QLineEdit():
self.input.textChanged.connect(self.update_missing)
def parse_form(self):
def parse_form(self) -> Tuple[str, dict]:
"""
Pulls info from widget into dict
Returns:
Tuple[str, dict]: name of field, {value, missing}
"""
match self.input:
case QLineEdit():
value = self.input.text()
@@ -526,6 +522,18 @@ class SubmissionFormWidget(QWidget):
return self.input.objectName(), dict(value=value, missing=self.missing)
def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
"""
Creates form widget
Args:
parent (QWidget): parent widget
key (str): name of field
value (dict): value, and is it missing from scrape
submission_type (str | None, optional): SubmissionType as str. Defaults to None.
Returns:
QWidget: Form object
"""
try:
value = value['value']
except (TypeError, KeyError):
@@ -565,7 +573,6 @@ class SubmissionFormWidget(QWidget):
obj.ext_kit = uses[0]
add_widget.addItems(uses)
# Run reagent scraper whenever extraction kit is changed.
# add_widget.currentTextChanged.connect(obj.scrape_reagents)
case 'submitted_date':
# uses base calendar
add_widget = QDateEdit(calendarPopup=True)
@@ -578,7 +585,6 @@ class SubmissionFormWidget(QWidget):
case 'submission_category':
add_widget = QComboBox()
cats = ['Diagnostic', "Surveillance", "Research"]
# cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
cats += [item.name for item in SubmissionType.query()]
try:
cats.insert(0, cats.pop(cats.index(value)))
@@ -593,10 +599,12 @@ class SubmissionFormWidget(QWidget):
if add_widget != None:
add_widget.setObjectName(key)
add_widget.setParent(parent)
return add_widget
def update_missing(self):
"""
Set widget status to updated
"""
self.missing = True
self.label.updated(self.objectName())
@@ -622,6 +630,13 @@ class SubmissionFormWidget(QWidget):
self.setText(f"MISSING {output}")
def updated(self, key:str, title:bool=True):
"""
Mark widget as updated
Args:
key (str): Name of the field
title (bool, optional): Use title case. Defaults to True.
"""
if title:
output = key.replace('_', ' ').title()
else:
@@ -632,12 +647,9 @@ class ReagentFormWidget(QWidget):
def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
super().__init__(parent)
# self.setParent(parent)
self.app = self.parent().parent().parent().parent().parent().parent().parent().parent()
self.reagent = reagent
self.extraction_kit = extraction_kit
# self.ctx = reagent.ctx
layout = QVBoxLayout()
self.label = self.ReagentParsedLabel(reagent=reagent)
layout.addWidget(self.label)
@@ -652,14 +664,18 @@ class ReagentFormWidget(QWidget):
self.lot.currentTextChanged.connect(self.updated)
def parse_form(self) -> Tuple[PydReagent, dict]:
"""
Pulls form info into PydReagent
Returns:
Tuple[PydReagent, dict]: PydReagent and Report(?)
"""
lot = self.lot.currentText()
# wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
# if reagent doesn't exist in database, off to add it (uses App.add_reagent)
if wanted_reagent == None:
dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
if dlg.exec():
print(self.app)
wanted_reagent = self.app.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
return wanted_reagent, None
else:
@@ -669,15 +685,15 @@ class ReagentFormWidget(QWidget):
else:
# Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
# rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
rt = ReagentType.query(name=self.reagent.type)
if rt == None:
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
def updated(self):
"""
Set widget status to updated
"""
self.missing = True
self.label.updated(self.reagent.type)
@@ -696,19 +712,21 @@ class ReagentFormWidget(QWidget):
self.setText(f"MISSING {reagent.type}")
def updated(self, reagent_type:str):
"""
Marks widget as updated
Args:
reagent_type (str): _description_
"""
self.setText(f"UPDATED {reagent_type}")
class ReagentLot(QComboBox):
def __init__(self, reagent, extraction_kit:str) -> None:
super().__init__()
# self.ctx = reagent.ctx
self.setEditable(True)
# if reagent.parsed:
# pass
logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type)
lookup = Reagent.query(reagent_type=reagent.type)
relevant_reagents = [str(item.lot) for item in lookup]
output_reg = []
@@ -726,11 +744,8 @@ class ReagentFormWidget(QWidget):
if check_not_nan(reagent.lot):
relevant_reagents.insert(0, str(reagent.lot))
else:
# TODO: look up the last used reagent of this type in the database
# looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
try:
# looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
except AttributeError:
looked_up_reg = None
@@ -752,4 +767,3 @@ class ReagentFormWidget(QWidget):
logger.debug(f"New relevant reagents: {relevant_reagents}")
self.setObjectName(f"lot_{reagent.type}")
self.addItems(relevant_reagents)

View File

@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
{% block head %}
<style>
/* Tooltip container */
.tooltip {
@@ -34,11 +35,13 @@
}
</style>
<title>Submission Details for {{ sub['Plate Number'] }}</title>
{% endblock %}
</head>
{% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %}
<body>
{% block body %}
<!-- {% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %} -->
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>&nbsp;&nbsp;&nbsp;{% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
<p>{% for key, value in sub.items() if key not in excluded %}
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key }}: </b>{% if key=='Cost' %}{% if sub['Cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
{% endfor %}</p>
<h3><u>Reagents:</u></h3>
@@ -111,5 +114,6 @@
<h3><u>Plate map:</u></h3>
<img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}">
{% endif %}
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,38 @@
{% extends "basicsubmission_details.html" %}
<head>
{% block head %}
{{ super() }}
{% endblock %}
</head>
<body>
{% block body %}
{{ super() }}
{% if sub['gel_info'] %}
<br/>
<h3><u>Gel Box:</u></h3>
{% if sub['gel_image'] %}
<br/>
<img align='left' height="400px" width="600px" src="data:image/jpeg;base64,{{ sub['gel_image'] | safe }}">
{% endif %}
<br/>
<table style="width:100%; border: 1px solid black; border-collapse: collapse;">
<tr>
{% for header in sub['headers'] %}
<th>{{ header }}</th>
{% endfor %}
</tr>
{% for field in sub['gel_info'] %}
<tr>
<td>{{ field['name'] }}</td>
{% for item in field['values'] %}
<td>{{ item['value'] }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<br/>
{% endif %}
{% endblock %}
</body>

View File

@@ -95,6 +95,16 @@ def convert_nans_to_nones(input_str) -> str|None:
return None
def check_regex_match(pattern:str, check:str) -> bool:
"""
Determines if a pattern matches a str
Args:
pattern (str): regex pattern string
check (str): string to be checked
Returns:
bool: match found?
"""
try:
return bool(re.match(fr"{pattern}", check))
except TypeError:
@@ -375,37 +385,6 @@ def jinja_template_loading() -> Environment:
env.globals['STATIC_PREFIX'] = loader_path.joinpath("static", "css")
return env
def check_authorization(func):
"""
Decorator to check if user is authorized to access function
Args:
func (_type_): Function to be used.
"""
def wrapper(*args, **kwargs):
logger.debug(f"Checking authorization")
if getpass.getuser() in kwargs['ctx'].power_users:
return func(*args, **kwargs)
else:
logger.error(f"User {getpass.getuser()} is not authorized for this function.")
return dict(code=1, message="This user does not have permission for this function.", status="warning")
return wrapper
# def check_authorization(user:str):
# def decorator(function):
# def wrapper(*args, **kwargs):
# # funny_stuff()
# # print(argument)
# power_users =
# if user in ctx.power_users:
# result = function(*args, **kwargs)
# else:
# logger.error(f"User {getpass.getuser()} is not authorized for this function.")
# result = dict(code=1, message="This user does not have permission for this function.", status="warning")
# return result
# return wrapper
# return decorator
def check_if_app() -> bool:
"""
Checks if the program is running from pyinstaller compiled
@@ -431,7 +410,7 @@ def convert_well_to_row_column(input_str:str) -> Tuple[int, int]:
Returns:
Tuple[int, int]: row, column
"""
row_keys = dict(A=1, B=2, C=3, D=4, E=5, F=6, G=7, H=8)
row_keys = {v:k for k,v in row_map.items()}
try:
row = int(row_keys[input_str[0].upper()])
column = int(input_str[1:])
@@ -439,27 +418,13 @@ def convert_well_to_row_column(input_str:str) -> Tuple[int, int]:
return None, None
return row, column
def query_return(query:Query, limit:int=0):
def setup_lookup(func):
"""
Execute sqlalchemy query.
Checks to make sure all args are allowed
Args:
query (Query): Query object
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
_type_: Query result.
func (_type_): _description_
"""
with query.session.no_autoflush:
match limit:
case 0:
return query.all()
case 1:
return query.first()
case _:
return query.limit(limit).all()
def setup_lookup(func):
def wrapper(*args, **kwargs):
for k, v in locals().items():
if k == "kwargs":
@@ -509,32 +474,30 @@ class Report(BaseModel):
except AttributeError:
logger.error(f"Problem adding result.")
case Report():
# logger.debug(f"Adding all results in report to new report")
for res in result.results:
logger.debug(f"Adding {res} from to results.")
self.results.append(res)
case _:
pass
def readInChunks(fileObj, chunkSize=2048):
"""
Lazy function to read a file piece by piece.
Default chunk size: 2kB.
"""
while True:
data = fileObj.readlines(chunkSize)
if not data:
break
yield data
def get_first_blank_df_row(df:pd.DataFrame) -> int:
return len(df) + 1
def is_missing(value:Any) -> Tuple[Any, bool]:
if check_not_nan(value):
return value, False
else:
return convert_nans_to_nones(value), True
def rreplace(s, old, new):
return (s[::-1].replace(old[::-1],new[::-1], 1))[::-1]
ctx = get_config(None)
def check_authorization(func):
"""
Decorator to check if user is authorized to access function
Args:
func (_type_): Function to be used.
"""
def wrapper(*args, **kwargs):
logger.debug(f"Checking authorization")
if getpass.getuser() in ctx.power_users:
return func(*args, **kwargs)
else:
logger.error(f"User {getpass.getuser()} is not authorized for this function.")
return dict(code=1, message="This user does not have permission for this function.", status="warning")
return wrapper