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

@@ -1,3 +1,7 @@
## 202402.01
- Addition of gel box for Artic quality control.
## 202401.04 ## 202401.04
- Large scale database refactor to increase modularity. - Large scale database refactor to increase modularity.

View File

@@ -1,3 +1,5 @@
- [x] Create platemap image from html for export to pdf.
- [x] Move plate map maker to submission.
- [x] Finish Equipment Parser (add in regex to id asset_number) - [x] Finish Equipment Parser (add in regex to id asset_number)
- [ ] Complete info_map in the SubmissionTypeCreator widget. - [ ] Complete info_map in the SubmissionTypeCreator widget.
- [x] Update Artic and add in equipment listings... *sigh*. - [x] Update Artic and add in equipment listings... *sigh*.

View File

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

View File

@@ -2,7 +2,7 @@
Contains all models for sqlalchemy Contains all models for sqlalchemy
''' '''
import sys import sys
from sqlalchemy.orm import DeclarativeMeta, declarative_base from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
from pathlib import Path from pathlib import Path
@@ -23,10 +23,16 @@ class BaseClass(Base):
@declared_attr @declared_attr
def __tablename__(cls): def __tablename__(cls):
"""
Set tablename to lowercase class name
"""
return f"_{cls.__name__.lower()}" return f"_{cls.__name__.lower()}"
@declared_attr @declared_attr
def __database_session__(cls): def __database_session__(cls):
"""
Pull db session from ctx
"""
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
from tools import ctx from tools import ctx
else: else:
@@ -35,6 +41,9 @@ class BaseClass(Base):
@declared_attr @declared_attr
def __directory_path__(cls): def __directory_path__(cls):
"""
Pull submission directory from ctx
"""
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
from tools import ctx from tools import ctx
else: else:
@@ -43,14 +52,39 @@ class BaseClass(Base):
@declared_attr @declared_attr
def __backup_path__(cls): def __backup_path__(cls):
"""
Pull backup directory from ctx
"""
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
from tools import ctx from tools import ctx
else: else:
from test_settings import ctx from test_settings import ctx
return ctx.backup_path 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): def save(self):
# logger.debug(f"Saving {self}") """
Add the object to the database and commit
"""
try: try:
self.__database_session__.add(self) self.__database_session__.add(self)
self.__database_session__.commit() self.__database_session__.commit()

View File

@@ -7,7 +7,7 @@ from sqlalchemy.orm import relationship, Query
import logging, json import logging, json
from operator import itemgetter from operator import itemgetter
from . import BaseClass from . import BaseClass
from tools import setup_lookup, query_return from tools import setup_lookup
from datetime import date, datetime from datetime import date, datetime
from typing import List from typing import List
from dateutil.parser import parse from dateutil.parser import parse
@@ -18,7 +18,6 @@ class ControlType(BaseClass):
""" """
Base class of a control archetype. Base class of a control archetype.
""" """
# __tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS) name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -48,7 +47,7 @@ class ControlType(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return query_return(query=query, limit=limit) return cls.query_return(query=query, limit=limit)
def get_subtypes(self, mode:str) -> List[str]: def get_subtypes(self, mode:str) -> List[str]:
""" """
@@ -60,10 +59,13 @@ class ControlType(BaseClass):
Returns: Returns:
List[str]: list of subtypes available List[str]: list of subtypes available
""" """
# Get first instance since all should have same subtypes
outs = self.instances[0] outs = self.instances[0]
# Get mode of instance
jsoner = json.loads(getattr(outs, mode)) jsoner = json.loads(getattr(outs, mode))
logger.debug(f"JSON out: {jsoner.keys()}") logger.debug(f"JSON out: {jsoner.keys()}")
try: try:
# Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0] genera = list(jsoner.keys())[0]
except IndexError: except IndexError:
return [] return []
@@ -74,8 +76,6 @@ class Control(BaseClass):
""" """
Base class of a control sample. Base class of a control sample.
""" """
# __tablename__ = '_control_samples'
id = Column(INTEGER, primary_key=True) #: primary key 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 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 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_version = Column(String(16)) #: version of kraken2 used in fastq parsing
kraken2_db_version = Column(String(32)) #: folder name of kraken2 db kraken2_db_version = Column(String(32)) #: folder name of kraken2 db
sample = relationship("BacterialCultureSample", back_populates="control") 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 = Column(INTEGER, ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
def __repr__(self) -> str: def __repr__(self) -> str:
"""
Returns:
str: Representation of self
"""
return f"<Control({self.name})>" return f"<Control({self.name})>"
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
@@ -103,25 +107,25 @@ class Control(BaseClass):
Returns: Returns:
dict: output dictionary containing: Name, Type, Targets, Top Kraken results dict: output dictionary containing: Name, Type, Targets, Top Kraken results
""" """
# load json string into dict # logger.debug("loading json string into dict")
try: try:
kraken = json.loads(self.kraken) kraken = json.loads(self.kraken)
except TypeError: except TypeError:
kraken = {} 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]) kraken_cnt_total = sum([kraken[item]['kraken_count'] for item in kraken])
new_kraken = [] new_kraken = []
for item in 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 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.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) new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
# set targets # logger.debug("setting targets")
if self.controltype.targets == []: if self.controltype.targets == []:
targets = ["None"] targets = ["None"]
else: else:
targets = self.controltype.targets targets = self.controltype.targets
# construct output dictionary # logger.debug("constructing output dictionary")
output = { output = {
"name" : self.name, "name" : self.name,
"type" : self.controltype.name, "type" : self.controltype.name,
@@ -141,49 +145,28 @@ class Control(BaseClass):
list[dict]: list of records list[dict]: list of records
""" """
output = [] output = []
# load json string for mode (i.e. contains, matches, kraken2) # logger.debug("load json string for mode (i.e. contains, matches, kraken2)")
try: try:
data = json.loads(getattr(self, mode)) data = json.loads(getattr(self, mode))
except TypeError: except TypeError:
data = {} data = {}
logger.debug(f"Length of data: {len(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: for genus in data:
_dict = {} _dict = {}
_dict['name'] = self.name _dict['name'] = self.name
_dict['submitted_date'] = self.submitted_date _dict['submitted_date'] = self.submitted_date
_dict['genus'] = genus _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" _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]: for key in data[genus]:
_dict[key] = data[genus][key] _dict[key] = data[genus][key]
output.append(_dict) 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: if "kraken" in mode:
output = sorted(output, key=lambda d: d[f"{mode}_count"], reverse=True)[:49] output = sorted(output, key=lambda d: d[f"{mode}_count"], reverse=True)[:49]
return output 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 @classmethod
def get_modes(cls) -> List[str]: def get_modes(cls) -> List[str]:
@@ -194,6 +177,7 @@ class Control(BaseClass):
List[str]: List of control mode names. List[str]: List of control mode names.
""" """
try: 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)] cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
except AttributeError as e: except AttributeError as e:
logger.error(f"Failed to get available modes from db: {e}") logger.error(f"Failed to get available modes from db: {e}")
@@ -243,25 +227,32 @@ class Control(BaseClass):
if start_date != None: if start_date != None:
match start_date: match start_date:
case date(): case date():
# logger.debug(f"Lookup control by start date({start_date})")
start_date = start_date.strftime("%Y-%m-%d") start_date = start_date.strftime("%Y-%m-%d")
case int(): 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") start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _: case _:
# logger.debug(f"Lookup control with parsed start date {start_date}")
start_date = parse(start_date).strftime("%Y-%m-%d") start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date: match end_date:
case date(): case date():
# logger.debug(f"Lookup control by end date({end_date})")
end_date = end_date.strftime("%Y-%m-%d") end_date = end_date.strftime("%Y-%m-%d")
case int(): 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") end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
case _: case _:
# logger.debug(f"Lookup control with parsed end date {end_date}")
end_date = parse(end_date).strftime("%Y-%m-%d") 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}") # 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)) query = query.filter(cls.submitted_date.between(start_date, end_date))
match control_name: match control_name:
case str(): case str():
# logger.debug(f"Lookup control by name {control_name}")
query = query.filter(cls.name.startswith(control_name)) query = query.filter(cls.name.startswith(control_name))
limit = 1 limit = 1
case _: case _:
pass 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 import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship, Query from sqlalchemy.orm import relationship, Query
from . import Base, BaseClass 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 from typing import List
import logging import logging
@@ -25,8 +25,7 @@ class Organization(BaseClass):
""" """
Base of organization Base of organization
""" """
# __tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name name = Column(String(64)) #: organization name
submissions = relationship("BasicSubmission", back_populates="submitting_lab") #: submissions this organization has submitted 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 contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) #: contacts involved with this org
def __repr__(self) -> str: def __repr__(self) -> str:
"""
Returns:
str: Representation of this Organization
"""
return f"<Organization({self.name})>" return f"<Organization({self.name})>"
def set_attribute(self, name:str, value):
setattr(self, name, value)
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
@@ -63,24 +63,17 @@ class Organization(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return query_return(query=query, limit=limit) return cls.query_return(query=query, limit=limit)
@check_authorization @check_authorization
def save(self, ctx:Settings): def save(self):
"""
Adds this instance to the database and commits
Args:
ctx (Settings): Settings object passed down from GUI. Necessary to check authorization
"""
super().save() super().save()
class Contact(BaseClass): class Contact(BaseClass):
""" """
Base of Contact Base of Contact
""" """
# __tablename__ = "_contacts"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name name = Column(String(64)) #: contact name
email = Column(String(64)) #: contact email 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 organization = relationship("Organization", back_populates="contacts", uselist=True, secondary=orgs_contacts) #: relationship to joined organization
def __repr__(self) -> str: def __repr__(self) -> str:
"""
Returns:
str: Representation of this Contact
"""
return f"<Contact({self.name})>" return f"<Contact({self.name})>"
@classmethod @classmethod
@@ -133,5 +130,5 @@ class Contact(BaseClass):
limit = 1 limit = 1
case _: case _:
pass 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 collections import OrderedDict
from datetime import date from datetime import date
from dateutil.parser import parse, ParserError 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__}") 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): class SheetParser(object):
""" """
object to pull and contain data from excel file 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: 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. filepath (Path | None, optional): file path to excel sheet. Defaults to None.
""" """
self.ctx = ctx
logger.debug(f"\n\nParsing {filepath.__str__()}\n\n") logger.debug(f"\n\nParsing {filepath.__str__()}\n\n")
match filepath: match filepath:
case Path(): case Path():
@@ -46,7 +44,7 @@ class SheetParser(object):
raise FileNotFoundError(f"Couldn't parse file {self.filepath}") raise FileNotFoundError(f"Couldn't parse file {self.filepath}")
self.sub = OrderedDict() self.sub = OrderedDict()
# make decision about type of sample we have # 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 # # grab the info map from the submission type in database
self.parse_info() self.parse_info()
self.import_kit_validation_check() self.import_kit_validation_check()
@@ -144,7 +142,6 @@ class InfoParser(object):
def __init__(self, xl:pd.ExcelFile, submission_type:str): def __init__(self, xl:pd.ExcelFile, submission_type:str):
logger.info(f"\n\Hello from InfoParser!\n\n") logger.info(f"\n\Hello from InfoParser!\n\n")
# self.ctx = ctx
self.map = self.fetch_submission_info_map(submission_type=submission_type) self.map = self.fetch_submission_info_map(submission_type=submission_type)
self.xl = xl self.xl = xl
logger.debug(f"Info map for InfoParser: {pformat(self.map)}") 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): def __init__(self, xl:pd.ExcelFile, submission_type:str, extraction_kit:str):
logger.debug("\n\nHello from ReagentParser!\n\n") 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) self.map = self.fetch_kit_info_map(extraction_kit=extraction_kit, submission_type=submission_type)
logger.debug(f"Reagent Parser map: {self.map}") logger.debug(f"Reagent Parser map: {self.map}")
self.xl = xl self.xl = xl
@@ -227,7 +223,6 @@ class ReagentParser(object):
""" """
if isinstance(extraction_kit, dict): if isinstance(extraction_kit, dict):
extraction_kit = extraction_kit['value'] extraction_kit = extraction_kit['value']
# kit = lookup_kit_types(ctx=self.ctx, name=extraction_kit)
kit = KitType.query(name=extraction_kit) kit = KitType.query(name=extraction_kit)
if isinstance(submission_type, dict): if isinstance(submission_type, dict):
submission_type = submission_type['value'] submission_type = submission_type['value']
@@ -272,7 +267,6 @@ class ReagentParser(object):
lot = str(lot) lot = str(lot)
logger.debug(f"Going into pydantic: name: {name}, lot: {lot}, expiry: {expiry}, type: {item.strip()}, comment: {comment}") 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)) listo.append(PydReagent(type=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, missing=missing))
# logger.debug(f"Returning listo: {listo}")
return listo return listo
class SampleParser(object): class SampleParser(object):
@@ -290,7 +284,6 @@ class SampleParser(object):
""" """
logger.debug("\n\nHello from SampleParser!\n\n") logger.debug("\n\nHello from SampleParser!\n\n")
self.samples = [] self.samples = []
# self.ctx = ctx
self.xl = xl self.xl = xl
self.submission_type = submission_type self.submission_type = submission_type
sample_info_map = self.fetch_sample_info_map(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. dict: Info locations.
""" """
logger.debug(f"Looking up submission type: {submission_type}") 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) submission_type = SubmissionType.query(name=submission_type)
logger.debug(f"info_map: {pformat(submission_type.info_map)}") logger.debug(f"info_map: {pformat(submission_type.info_map)}")
sample_info_map = submission_type.info_map['samples'] 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_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 self.custom_sample_parser = BasicSample.find_polymorphic_subclass(polymorphic_identity=f"{submission_type.name} Sample").parse_sample
return sample_info_map return sample_info_map
@@ -341,7 +332,6 @@ class SampleParser(object):
df = pd.DataFrame(df.values[1:], columns=df.iloc[0]) df = pd.DataFrame(df.values[1:], columns=df.iloc[0])
df = df.set_index(df.columns[0]) df = df.set_index(df.columns[0])
logger.debug(f"Vanilla platemap: {df}") 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) custom_mapper = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
df = custom_mapper.custom_platemap(self.xl, df) df = custom_mapper.custom_platemap(self.xl, df)
logger.debug(f"Custom platemap:\n{df}") logger.debug(f"Custom platemap:\n{df}")
@@ -402,7 +392,6 @@ class SampleParser(object):
else: else:
return input_str return input_str
for sample in self.samples: 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() addition = self.lookup_table[self.lookup_table.isin([sample['submitter_id']]).any(axis=1)].squeeze()
# logger.debug(addition) # logger.debug(addition)
if isinstance(addition, pd.DataFrame) and not addition.empty: 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"Output sample dict: {sample}")
logger.debug(f"Final lookup_table: \n\n {self.lookup_table}") 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 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: Returns:
List[dict]|List[models.BasicSample]: List of samples List[dict]|List[models.BasicSample]: List of samples
""" """
result = None result = None
new_samples = [] new_samples = []
logger.debug(f"Starting samples: {pformat(self.samples)}") logger.debug(f"Starting samples: {pformat(self.samples)}")
for ii, sample in enumerate(self.samples): for sample in 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}")
translated_dict = {} translated_dict = {}
for k, v in sample.items(): for k, v in sample.items():
match v: match v:
@@ -483,7 +464,7 @@ class SampleParser(object):
for plate in self.plates: for plate in self.plates:
df = self.xl.parse(plate['sheet'], header=None) df = self.xl.parse(plate['sheet'], header=None)
if isinstance(df.iat[plate['row']-1, plate['column']-1], str): 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: else:
continue continue
plates.append(output) plates.append(output)
@@ -495,25 +476,43 @@ class EquipmentParser(object):
self.submission_type = submission_type self.submission_type = submission_type
self.xl = xl self.xl = xl
self.map = self.fetch_equipment_map() self.map = self.fetch_equipment_map()
# self.equipment = self.parse_equipment()
def fetch_equipment_map(self) -> List[dict]: 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) submission_type = SubmissionType.query(name=self.submission_type)
return submission_type.construct_equipment_map() return submission_type.construct_equipment_map()
def get_asset_number(self, input:str) -> str: 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() regex = Equipment.get_regex()
logger.debug(f"Using equipment regex: {regex} on {input}") logger.debug(f"Using equipment regex: {regex} on {input}")
try: try:
return regex.search(input).group().strip("-") return regex.search(input).group().strip("-")
except AttributeError: except AttributeError:
return input 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__)}") logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
output = [] output = []
# sheets = list(set([item['sheet'] for item in self.map]))
# logger.debug(f"Sheets: {sheets}") # logger.debug(f"Sheets: {sheets}")
for sheet in self.xl.sheet_names: for sheet in self.xl.sheet_names:
df = self.xl.parse(sheet, header=None, dtype=object) df = self.xl.parse(sheet, header=None, dtype=object)
@@ -550,7 +549,6 @@ class PCRParser(object):
Args: Args:
filepath (Path | None, optional): file to parse. Defaults to None. filepath (Path | None, optional): file to parse. Defaults to None.
""" """
# self.ctx = ctx
logger.debug(f"Parsing {filepath.__str__()}") logger.debug(f"Parsing {filepath.__str__()}")
if filepath == None: if filepath == None:
logger.error(f"No filepath given.") logger.error(f"No filepath given.")
@@ -564,9 +562,8 @@ class PCRParser(object):
except PermissionError: except PermissionError:
logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.") logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.")
return return
# self.pcr = OrderedDict()
self.parse_general(sheet_name="Results") self.parse_general(sheet_name="Results")
namer = RSLNamer(instr=filepath.__str__()) namer = RSLNamer(filename=filepath.__str__())
self.plate_num = namer.parsed_name self.plate_num = namer.parsed_name
self.submission_type = namer.submission_type self.submission_type = namer.submission_type
logger.debug(f"Set plate number to {self.plate_num} and type to {self.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: 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: Args:
input (List[dict]): list of hitpicked dictionaries input (List[dict]): list of hitpicked dictionaries

View File

@@ -2,8 +2,8 @@ import logging, re
from pathlib import Path from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
from backend.db.models import BasicSubmission, SubmissionType from backend.db.models import BasicSubmission, SubmissionType
from datetime import date
from tools import jinja_template_loading from tools import jinja_template_loading
from jinja2 import Template
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -11,14 +11,16 @@ class RSLNamer(object):
""" """
Object that will enforce proper formatting on RSL plate names. 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 self.submission_type = sub_type
if self.submission_type == None: 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}") logger.debug(f"got submission type: {self.submission_type}")
if self.submission_type != None: if self.submission_type != None:
# logger.debug("Retrieving BasicSubmission subclass")
enforcer = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) 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: if data == None:
data = dict(submission_type=self.submission_type) data = dict(submission_type=self.submission_type)
if "submission_type" not in data.keys(): 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) self.parsed_name = enforcer.enforce_name(instr=self.parsed_name, data=data)
@classmethod @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 Gets submission type from excel file properties or sheet names or regex pattern match or user input
Args: Args:
instr (str | Path): filename filename (str | Path): filename
Returns: Returns:
str: parsed submission type str: parsed submission type
""" """
match instr: match filename:
case Path(): case Path():
logger.debug(f"Using path method for {instr}.") logger.debug(f"Using path method for {filename}.")
if instr.exists(): if filename.exists():
wb = load_workbook(instr) wb = load_workbook(filename)
try: try:
submission_type = [item.strip().title() for item in wb.properties.category.split(";")][0] submission_type = [item.strip().title() for item in wb.properties.category.split(";")][0]
except AttributeError: except AttributeError:
try: 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()} sts = {item.name:item.get_template_file_sheets() for item in SubmissionType.query()}
for k,v in sts.items(): for k,v in sts.items():
# This gets the *first* submission type that matches the sheet names in the workbook # This gets the *first* submission type that matches the sheet names in the workbook
@@ -54,13 +55,13 @@ class RSLNamer(object):
break break
except: except:
# On failure recurse using filename as string for string method # 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: else:
submission_type = cls.retrieve_submission_type(instr=instr.stem.__str__()) submission_type = cls.retrieve_submission_type(filename=filename.stem.__str__())
case str(): case str():
regex = BasicSubmission.construct_regex() regex = BasicSubmission.construct_regex()
logger.debug(f"Using string method for {instr}.") logger.debug(f"Using string method for {filename}.")
m = regex.search(instr) m = regex.search(filename)
try: try:
submission_type = m.lastgroup submission_type = m.lastgroup
except AttributeError as e: except AttributeError as e:
@@ -72,6 +73,7 @@ class RSLNamer(object):
except UnboundLocalError: except UnboundLocalError:
check = True check = True
if check: if check:
# logger.debug("Final option, ask the user for submission type")
from frontend.widgets import SubmissionTypeSelector from frontend.widgets import SubmissionTypeSelector
dlg = SubmissionTypeSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.") dlg = SubmissionTypeSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.")
if dlg.exec(): if dlg.exec():
@@ -80,25 +82,25 @@ class RSLNamer(object):
return submission_type return submission_type
@classmethod @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 Uses regex to retrieve the plate number and submission type from an input string
Args: Args:
in_str (str): string to be parsed 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: if regex == None:
regex = BasicSubmission.construct_regex() regex = BasicSubmission.construct_regex()
else: else:
regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE) regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE)
logger.debug(f"Using regex: {regex}") logger.debug(f"Using regex: {regex}")
match instr: match filename:
case Path(): case Path():
m = regex.search(instr.stem) m = regex.search(filename.stem)
case str(): case str():
logger.debug(f"Using string method.") logger.debug(f"Using string method.")
m = regex.search(instr) m = regex.search(filename)
case _: case _:
pass pass
if m != None: if m != None:
@@ -113,6 +115,15 @@ class RSLNamer(object):
@classmethod @classmethod
def construct_new_plate_name(cls, data:dict) -> str: 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 "submitted_date" in data.keys():
if isinstance(data['submitted_date'], dict): if isinstance(data['submitted_date'], dict):
if data['submitted_date']['value'] != None: 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}" return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
@classmethod @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"Kwargs: {kwargs}")
logger.debug(f"Template: {template}") logger.debug(f"Template: {template}")
environment = jinja_template_loading() environment = jinja_template_loading()
template = environment.from_string(template) template = environment.from_string(template)
return template.render(**kwargs) 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 typing import List, Tuple
from . import RSLNamer from . import RSLNamer
from pathlib import Path 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 backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError from sqlalchemy.exc import StatementError, IntegrityError
from PyQt6.QtWidgets import QComboBox, QWidget from PyQt6.QtWidgets import QComboBox, QWidget
# from pprint import pformat
from openpyxl import load_workbook, Workbook from openpyxl import load_workbook, Workbook
from io import BytesIO from io import BytesIO
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class PydReagent(BaseModel): class PydReagent(BaseModel):
lot: str|None lot: str|None
type: str|None type: str|None
expiry: date|None expiry: date|None
@@ -103,6 +103,7 @@ class PydReagent(BaseModel):
Tuple[Reagent, Report]: Reagent instance and result of function Tuple[Reagent, Report]: Reagent instance and result of function
""" """
report = Report() report = Report()
# logger.debug("Adding extra fields.")
if self.model_extra != None: if self.model_extra != None:
self.__dict__.update(self.model_extra) self.__dict__.update(self.model_extra)
logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}") logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
@@ -118,16 +119,17 @@ class PydReagent(BaseModel):
match key: match key:
case "lot": case "lot":
reagent.lot = value.upper() reagent.lot = value.upper()
case "expiry":
reagent.expiry = value
case "type": case "type":
reagent_type = ReagentType.query(name=value) reagent_type = ReagentType.query(name=value)
if reagent_type != None: if reagent_type != None:
reagent.type.append(reagent_type) reagent.type.append(reagent_type)
case "name":
reagent.name = value
case "comment": case "comment":
continue continue
case _:
try:
reagent.__setattr__(key, value)
except AttributeError:
logger.error(f"Couldn't set {key} to {value}")
if submission != None: if submission != None:
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission) assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
assoc.comments = self.comment assoc.comments = self.comment
@@ -190,7 +192,8 @@ class PydSample(BaseModel, extra='allow'):
case "row" | "column": case "row" | "column":
continue continue
case _: case _:
instance.set_attribute(name=key, value=value) # instance.set_attribute(name=key, value=value)
instance.__setattr__(key, value)
out_associations = [] out_associations = []
if submission != None: if submission != None:
assoc_type = self.sample_type.replace("Sample", "").strip() assoc_type = self.sample_type.replace("Sample", "").strip()
@@ -228,11 +231,16 @@ class PydEquipment(BaseModel, extra='ignore'):
value=[''] value=['']
return value return value
# def toForm(self, parent): def toSQL(self, submission:BasicSubmission|str=None) -> Tuple[Equipment, SubmissionEquipmentAssociation]:
# from frontend.widgets.equipment_usage import EquipmentCheckBox """
# return EquipmentCheckBox(parent=parent, equipment=self) Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment
def toSQL(self, submission:BasicSubmission|str=None): Args:
submission ( BasicSubmission | str ): BasicSubmission of interest
Returns:
Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects
"""
if isinstance(submission, str): if isinstance(submission, str):
submission = BasicSubmission.query(rsl_number=submission) submission = BasicSubmission.query(rsl_number=submission)
equipment = Equipment.query(asset_number=self.asset_number) equipment = Equipment.query(asset_number=self.asset_number)
@@ -242,6 +250,7 @@ class PydEquipment(BaseModel, extra='ignore'):
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment) assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
process = Process.query(name=self.processes[0]) process = Process.query(name=self.processes[0])
if process == None: if process == None:
# logger.debug("Adding in unknown process.")
from frontend.widgets.pop_ups import QuestionAsker 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?") 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(): if dlg.exec():
@@ -254,8 +263,6 @@ class PydEquipment(BaseModel, extra='ignore'):
process.save() process.save()
assoc.process = process assoc.process = process
assoc.role = self.role assoc.role = self.role
# equipment.equipment_submission_associations.append(assoc)
# equipment.equipment_submission_associations.append(assoc)
else: else:
assoc = None assoc = None
return equipment, assoc return equipment, assoc
@@ -357,7 +364,7 @@ class PydSubmission(BaseModel, extra='allow'):
if check_not_nan(value['value']): if check_not_nan(value['value']):
return value return value
else: 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) return dict(value=output, missing=True)
@field_validator("technician", mode="before") @field_validator("technician", mode="before")
@@ -407,9 +414,10 @@ class PydSubmission(BaseModel, extra='allow'):
return dict(value=value, missing=False) return dict(value=value, missing=False)
else: else:
# return dict(value=RSLNamer(instr=values.data['filepath'].__str__()).submission_type.title(), missing=True) # 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") @field_validator("submission_category", mode="before")
@classmethod
def create_category(cls, value): def create_category(cls, value):
if not isinstance(value, dict): if not isinstance(value, dict):
return dict(value=value, missing=True) return dict(value=value, missing=True)
@@ -423,6 +431,7 @@ class PydSubmission(BaseModel, extra='allow'):
return value return value
@field_validator("samples") @field_validator("samples")
@classmethod
def assign_ids(cls, value, values): def assign_ids(cls, value, values):
starting_id = SubmissionSampleAssociation.autoincrement_id() starting_id = SubmissionSampleAssociation.autoincrement_id()
output = [] output = []
@@ -431,7 +440,6 @@ class PydSubmission(BaseModel, extra='allow'):
output.append(sample) output.append(sample)
return output return output
def handle_duplicate_samples(self): def handle_duplicate_samples(self):
""" """
Collapses multiple samples with same submitter id into one with lists for rows, columns. 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])) submitter_ids = list(set([sample.submitter_id for sample in self.samples]))
output = [] 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] relevants = [item for item in self.samples if item.submitter_id==id]
if len(relevants) <= 1: if len(relevants) <= 1:
output += relevants output += relevants
@@ -447,9 +455,6 @@ class PydSubmission(BaseModel, extra='allow'):
rows = [item.row[0] for item in relevants] rows = [item.row[0] for item in relevants]
columns = [item.column[0] for item in relevants] columns = [item.column[0] for item in relevants]
ids = [item.assoc_id[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 = relevants[0]
dummy.assoc_id = ids dummy.assoc_id = ids
dummy.row = rows dummy.row = rows
@@ -471,6 +476,7 @@ class PydSubmission(BaseModel, extra='allow'):
if dictionaries: if dictionaries:
output = {k:getattr(self, k) for k in fields} output = {k:getattr(self, k) for k in fields}
else: 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} output = {k:(getattr(self, k) if not isinstance(getattr(self, k), dict) else getattr(self, k)['value']) for k in fields}
return output return output
@@ -493,12 +499,14 @@ class PydSubmission(BaseModel, extra='allow'):
Returns: Returns:
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object 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']) 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) result = Result(msg=msg, code=code)
self.handle_duplicate_samples() self.handle_duplicate_samples()
logger.debug(f"Here's our list of duplicate removed samples: {self.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): if isinstance(value, dict):
value = value['value'] value = value['value']
logger.debug(f"Setting {key} to {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)} info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
reagents = self.reagents reagents = self.reagents
if len(reagents + list(info.keys())) == 0: if len(reagents + list(info.keys())) == 0:
# logger.warning("No info to fill in, returning")
return None return None
logger.debug(f"We have blank info and/or reagents in the excel sheet.\n\tLet's try to fill them in.") 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']) # 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 reagents going into autofile: {pformat(reagents)}")
# logger.debug(f"Missing info going into autofile: {pformat(info)}") # logger.debug(f"Missing info going into autofile: {pformat(info)}")
new_reagents = [] new_reagents = []
# logger.debug("Constructing reagent map and values")
for reagent in reagents: for reagent in reagents:
new_reagent = {} new_reagent = {}
new_reagent['type'] = reagent.type new_reagent['type'] = reagent.type
@@ -626,6 +636,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.error(f"Couldn't get name due to {e}") logger.error(f"Couldn't get name due to {e}")
new_reagents.append(new_reagent) new_reagents.append(new_reagent)
new_info = [] new_info = []
# logger.debug("Constructing info map and values")
for k,v in info.items(): for k,v in info.items():
try: try:
new_item = {} new_item = {}
@@ -678,6 +689,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Sample info: {pformat(sample_info)}") logger.debug(f"Sample info: {pformat(sample_info)}")
logger.debug(f"Workbook sheets: {workbook.sheetnames}") logger.debug(f"Workbook sheets: {workbook.sheetnames}")
worksheet = workbook[sample_info["lookup_table"]['sheet']] worksheet = workbook[sample_info["lookup_table"]['sheet']]
# logger.debug("Sorting samples by row/column")
samples = sorted(self.samples, key=attrgetter('column', 'row')) samples = sorted(self.samples, key=attrgetter('column', 'row'))
submission_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) submission_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
samples = submission_obj.adjust_autofill_samples(samples=samples) samples = submission_obj.adjust_autofill_samples(samples=samples)
@@ -704,6 +716,15 @@ class PydSubmission(BaseModel, extra='allow'):
return workbook return workbook
def autofill_equipment(self, workbook:Workbook) -> 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() equipment_map = SubmissionType.query(name=self.submission_type['value']).construct_equipment_map()
logger.debug(f"Equipment map: {equipment_map}") logger.debug(f"Equipment map: {equipment_map}")
# See if all equipment has a location 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") logger.warning("Creating 'Equipment' sheet to hold unmapped equipment")
workbook.create_sheet("Equipment") workbook.create_sheet("Equipment")
equipment = [] equipment = []
# logger.debug("Contructing equipment info map/values")
for ii, equip in enumerate(self.equipment, start=1): for ii, equip in enumerate(self.equipment, start=1):
loc = [item for item in equipment_map if item['role'] == equip.role][0] loc = [item for item in equipment_map if item['role'] == equip.role][0]
try: try:
@@ -746,12 +768,10 @@ class PydSubmission(BaseModel, extra='allow'):
Returns: Returns:
str: Output filename str: Output filename
""" """
env = jinja_template_loading()
template = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type).filename_template() template = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type).filename_template()
logger.debug(f"Using template string: {template}") # logger.debug(f"Using template string: {template}")
template = env.from_string(template) render = RSLNamer.construct_export_name(template=template, **self.improved_dict(dictionaries=False)).replace("/", "")
render = template.render(**self.improved_dict(dictionaries=False)).replace("/", "") # logger.debug(f"Template rendered as: {render}")
logger.debug(f"Template rendered as: {render}")
return render return render
def check_kit_integrity(self, reagenttypes:list=[]) -> Report: def check_kit_integrity(self, reagenttypes:list=[]) -> Report:
@@ -785,6 +805,7 @@ class PydSubmission(BaseModel, extra='allow'):
return report return report
class PydContact(BaseModel): class PydContact(BaseModel):
name: str name: str
phone: str|None phone: str|None
email: str|None email: str|None
@@ -818,7 +839,8 @@ class PydOrganization(BaseModel):
value = [item.toSQL() for item in getattr(self, field)] value = [item.toSQL() for item in getattr(self, field)]
case _: case _:
value = getattr(self, field) 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 return instance
class PydReagentType(BaseModel): class PydReagentType(BaseModel):
@@ -845,19 +867,16 @@ class PydReagentType(BaseModel):
Returns: Returns:
ReagentType: ReagentType instance ReagentType: ReagentType instance
""" """
# instance: ReagentType = lookup_reagent_types(ctx=ctx, name=self.name)
instance: ReagentType = ReagentType.query(name=self.name) instance: ReagentType = ReagentType.query(name=self.name)
if instance == None: if instance == None:
instance = ReagentType(name=self.name, eol_ext=self.eol_ext) instance = ReagentType(name=self.name, eol_ext=self.eol_ext)
logger.debug(f"This is the reagent type instance: {instance.__dict__}") logger.debug(f"This is the reagent type instance: {instance.__dict__}")
try: try:
# assoc = lookup_reagenttype_kittype_association(ctx=ctx, reagent_type=instance, kit_type=kit)
assoc = KitTypeReagentTypeAssociation.query(reagent_type=instance, kit_type=kit) assoc = KitTypeReagentTypeAssociation.query(reagent_type=instance, kit_type=kit)
except StatementError: except StatementError:
assoc = None assoc = None
if assoc == None: if assoc == None:
assoc = KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=instance, uses=self.uses, required=self.required) assoc = KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=instance, uses=self.uses, required=self.required)
# kit.kit_reagenttype_associations.append(assoc)
return instance return instance
class PydKit(BaseModel): class PydKit(BaseModel):
@@ -872,13 +891,10 @@ class PydKit(BaseModel):
Returns: Returns:
Tuple[KitType, Report]: KitType instance and report of results. Tuple[KitType, Report]: KitType instance and report of results.
""" """
# result = dict(message=None, status='Information')
report = Report() report = Report()
# instance = lookup_kit_types(ctx=ctx, name=self.name)
instance = KitType.query(name=self.name) instance = KitType.query(name=self.name)
if instance == None: if instance == None:
instance = KitType(name=self.name) 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] [item.toSQL(instance) for item in self.reagent_types]
return instance, report return instance, report
@@ -888,7 +904,17 @@ class PydEquipmentRole(BaseModel):
equipment: List[PydEquipment] equipment: List[PydEquipment]
processes: List[str]|None 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 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. Contains all operations for creating charts, graphs and visual effects.
''' '''
from .control_charts import * 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__}") logger = logging.getLogger(f"submissions.{__name__}")
def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure: def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
""" """
Constructs figures based on parsed pandas dataframe. 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("") genera.append("")
df['genus'] = df['genus'].replace({'\*':''}, regex=True).replace({"NaN":"Unknown"}) df['genus'] = df['genus'].replace({'\*':''}, regex=True).replace({"NaN":"Unknown"})
df['genera'] = genera df['genera'] = genera
# df = df.dropna()
# remove original runs, using reruns if applicable # remove original runs, using reruns if applicable
df = drop_reruns_from_df(ctx=ctx, df=df) df = drop_reruns_from_df(ctx=ctx, df=df)
# sort by and exclude from # sort by and exclude from
@@ -224,4 +222,4 @@ def construct_html(figure:Figure) -> str:
else: else:
html += "<h1>No data was retrieved for the given parameters.</h1>" html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>' 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._createMenuBar()
self._createToolBar() self._createToolBar()
self._connectActions() self._connectActions()
# self._controls_getter()
self.show() self.show()
self.statusBar().showMessage('Ready', 5000) self.statusBar().showMessage('Ready', 5000)
@@ -114,14 +113,10 @@ class App(QMainWindow):
self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results) self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results)
self.addReagentAction.triggered.connect(self.add_reagent) self.addReagentAction.triggered.connect(self.add_reagent)
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report) 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.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr) self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
self.helpAction.triggered.connect(self.showAbout) self.helpAction.triggered.connect(self.showAbout)
self.docsAction.triggered.connect(self.openDocs) 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) self.searchLog.triggered.connect(self.runSearch)
def showAbout(self): def showAbout(self):

View File

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

View File

@@ -2,9 +2,10 @@ from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QHBoxLayout, QLabel, QWidget, QHBoxLayout,
QVBoxLayout, QDialogButtonBox) QVBoxLayout, QDialogButtonBox)
from backend.db.models import SubmissionType, Equipment, BasicSubmission from backend.db.models import Equipment, BasicSubmission
from backend.validators.pydant import PydEquipment, PydEquipmentRole from backend.validators.pydant import PydEquipment, PydEquipmentRole
import logging import logging
from typing import List
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -24,19 +25,29 @@ class EquipmentUsage(QDialog):
self.populate_form() self.populate_form()
def populate_form(self): def populate_form(self):
"""
Create form widgets
"""
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
label = self.LabelRow(parent=self) label = self.LabelRow(parent=self)
self.layout.addWidget(label) self.layout.addWidget(label)
# logger.debug("Creating widgets for equipment")
for eq in self.opt_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) self.layout.addWidget(widg)
widg.update_processes() widg.update_processes()
self.layout.addWidget(self.buttonBox) 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 = [] output = []
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):
match widget: match widget:
@@ -63,43 +74,18 @@ class EquipmentUsage(QDialog):
self.setLayout(self.layout) self.setLayout(self.layout)
def check_all(self): def check_all(self):
"""
Toggles all checkboxes in the form
"""
for object in self.parent().findChildren(QCheckBox): for object in self.parent().findChildren(QCheckBox):
object.setChecked(self.check.isChecked()) 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): 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) super().__init__(parent)
self.layout = QHBoxLayout() self.layout = QHBoxLayout()
# label = QLabel()
# label.setText(pool.name)
self.role = role self.role = role
self.check = QCheckBox() self.check = QCheckBox()
if role.name in used: if role.name in used:
@@ -111,14 +97,10 @@ class RoleComboBox(QWidget):
self.box.setMinimumWidth(200) self.box.setMinimumWidth(200)
self.box.addItems([item.name for item in role.equipment]) self.box.addItems([item.name for item in role.equipment])
self.box.currentTextChanged.connect(self.update_processes) self.box.currentTextChanged.connect(self.update_processes)
# self.check = QCheckBox()
# self.layout.addWidget(label)
self.process = QComboBox() self.process = QComboBox()
self.process.setMaximumWidth(200) self.process.setMaximumWidth(200)
self.process.setMinimumWidth(200) self.process.setMinimumWidth(200)
self.process.setEditable(True) 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) self.layout.addWidget(self.check)
label = QLabel(f"{role.name}:") label = QLabel(f"{role.name}:")
label.setMinimumWidth(200) label.setMinimumWidth(200)
@@ -127,11 +109,12 @@ class RoleComboBox(QWidget):
self.layout.addWidget(label) self.layout.addWidget(label)
self.layout.addWidget(self.box) self.layout.addWidget(self.box)
self.layout.addWidget(self.process) self.layout.addWidget(self.process)
# self.layout.addWidget(self.check)
self.setLayout(self.layout) self.setLayout(self.layout)
# self.update_processes()
def update_processes(self): def update_processes(self):
"""
Changes processes when equipment is changed
"""
equip = self.box.currentText() equip = self.box.currentText()
logger.debug(f"Updating equipment: {equip}") logger.debug(f"Updating equipment: {equip}")
equip2 = [item for item in self.role.equipment if item.name==equip][0] 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.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.processes]) 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()) eq = Equipment.query(name=self.box.currentText())
# if self.check.isChecked(): try:
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname) return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
# else: except Exception as e:
# return None 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 * from PyQt6.QtWidgets import *
# import sys
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
@@ -9,11 +9,17 @@ from PyQt6.QtGui import *
from PyQt6.QtCore import * from PyQt6.QtCore import *
from PIL import Image from PIL import Image
import numpy as np 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 # Main window class
class GelBox(QDialog): class GelBox(QDialog):
def __init__(self, parent, img_path): def __init__(self, parent, img_path:str|Path):
super().__init__(parent) super().__init__(parent)
# setting title # setting title
self.setWindowTitle("PyQtGraph") self.setWindowTitle("PyQtGraph")
@@ -27,11 +33,12 @@ class GelBox(QDialog):
# calling method # calling method
self.UiComponents() self.UiComponents()
# showing all the widgets # showing all the widgets
# self.show()
# method for components # method for components
def UiComponents(self): def UiComponents(self):
# widget = QWidget() """
Create widgets in ui
"""
# setting configuration options # setting configuration options
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
# creating image view object # 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)) 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])) self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0]))
layout = QGridLayout() 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 # setting this layout to the widget
# widget.setLayout(layout)
# plot window goes on right side, spanning 3 rows # 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 # setting this widget as central widget of the main window
self.form = ControlsForm(parent=self) 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 QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 21, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop) layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
# self.buttonBox.clicked.connect(self.submit)
self.setLayout(layout) self.setLayout(layout)
def parse_form(self): def parse_form(self) -> Tuple[str, str|Path, list]:
return self.img_path, self.form.parse_form() """
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): class ControlsForm(QWidget):
def __init__(self, parent) -> None: def __init__(self, parent) -> None:
super().__init__(parent) super().__init__(parent)
self.layout = QGridLayout() self.layout = QGridLayout()
columns = [] columns = []
rows = [] 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) label = QLabel(item)
self.layout.addWidget(label, 0, iii,1,1) self.layout.addWidget(label, 0, iii,1,1)
if iii > 1: if iii > 1:
@@ -85,11 +100,22 @@ class ControlsForm(QWidget):
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1) self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self): def parse_form(self) -> List[dict]:
dicto = {} """
Pulls the controls statuses from the form.
Returns:
List[dict]: output of values
"""
output = []
for le in self.findChildren(QLineEdit): for le in self.findChildren(QLineEdit):
label = [item.strip() for item in le.objectName().split(" : ")] label = [item.strip() for item in le.objectName().split(" : ")]
if label[0] not in dicto.keys(): try:
dicto[label[0]] = {} dicto = [item for item in output if item['name']==label[0]][0]
dicto[label[0]][label[1]] = le.text() except IndexError:
return dicto 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 insert new reagent type row
""" """
print(self.app)
# get bottommost row # get bottommost row
maxrow = self.grid.rowCount() maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm(parent=self) reg_form = ReagentTypeForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}") 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) self.grid.addWidget(reg_form, maxrow,0,1,4)
def submit(self) -> None: def submit(self) -> None:
@@ -118,6 +116,12 @@ class KitAdder(QWidget):
self.__init__(self.parent()) self.__init__(self.parent())
def parse_form(self) -> Tuple[dict, list]: 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!") logger.debug(f"Hello from {self.__class__} parser!")
info = {} info = {}
reagents = [] reagents = []
@@ -188,10 +192,19 @@ class ReagentTypeForm(QWidget):
] ]
def remove(self): def remove(self):
"""
Destroys this row of reagenttype from the form
"""
self.setParent(None) self.setParent(None)
self.destroy() self.destroy()
def parse_form(self) -> dict: def parse_form(self) -> dict:
"""
Pulls ReagentType info from the form.
Returns:
dict: _description_
"""
logger.debug(f"Hello from {self.__class__} parser!") logger.debug(f"Hello from {self.__class__} parser!")
info = {} info = {}
info['eol'] = self.eol.value() 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: 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__() super().__init__()
# self.ctx = ctx
if reagent_lot == None: if reagent_lot == None:
reagent_lot = reagent_type reagent_lot = reagent_type
@@ -41,7 +40,6 @@ class AddReagentForm(QDialog):
self.name_input.setObjectName("name") self.name_input.setObjectName("name")
self.name_input.setEditable(True) self.name_input.setEditable(True)
self.name_input.setCurrentText(reagent_name) self.name_input.setCurrentText(reagent_name)
# self.name_input.setText(reagent_name)
self.lot_input = QLineEdit() self.lot_input = QLineEdit()
self.lot_input.setObjectName("lot") self.lot_input.setObjectName("lot")
self.lot_input.setText(reagent_lot) self.lot_input.setText(reagent_lot)
@@ -56,7 +54,6 @@ class AddReagentForm(QDialog):
# widget to get reagent type info # widget to get reagent type info
self.type_input = QComboBox() self.type_input = QComboBox()
self.type_input.setObjectName('type') 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()]) self.type_input.addItems([item.name for item in ReagentType.query()])
logger.debug(f"Trying to find index of {reagent_type}") logger.debug(f"Trying to find index of {reagent_type}")
# convert input to user friendly string? # convert input to user friendly string?
@@ -169,7 +166,13 @@ class FirstStrandSalvage(QDialog):
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout) 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()}") 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): class LogParser(QDialog):
@@ -193,9 +196,15 @@ class LogParser(QDialog):
def filelookup(self): def filelookup(self):
"""
Select file to search
"""
self.fname = select_open_file(self, "tabular") self.fname = select_open_file(self, "tabular")
def runsearch(self): def runsearch(self):
"""
Gets total/percent occurences of string in tabular file.
"""
count: int = 0 count: int = 0
total: int = 0 total: int = 0
logger.debug(f"Current search term: {self.phrase_looker.currentText()}") logger.debug(f"Current search term: {self.phrase_looker.currentText()}")

View File

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

View File

@@ -1,25 +1,25 @@
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox, from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
QLabel, QDialogButtonBox, QToolBar, QTextEdit) QDialogButtonBox, QTextEdit)
from PyQt6.QtGui import QAction, QPixmap
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6 import QtPrintSupport
from backend.db.models import BasicSubmission 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
from tools import check_if_app, jinja_template_loading
from .functions import select_save_file from .functions import select_save_file
from io import BytesIO from io import BytesIO
from tempfile import TemporaryFile, TemporaryDirectory
from pathlib import Path
from xhtml2pdf import pisa from xhtml2pdf import pisa
import logging, base64 import logging, base64
from getpass import getuser from getpass import getuser
from datetime import datetime from datetime import datetime
from pprint import pformat from pprint import pformat
from html2image import Html2Image
from PIL import Image
from typing import List
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class SubmissionDetails(QDialog): class SubmissionDetails(QDialog):
""" """
a window showing text details of submission a window showing text details of submission
@@ -27,7 +27,6 @@ class SubmissionDetails(QDialog):
def __init__(self, parent, sub:BasicSubmission) -> None: def __init__(self, parent, sub:BasicSubmission) -> None:
super().__init__(parent) super().__init__(parent)
# self.ctx = ctx
try: try:
self.app = parent.parent().parent().parent().parent().parent().parent() self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError: except AttributeError:
@@ -36,19 +35,16 @@ class SubmissionDetails(QDialog):
# create scrollable interior # create scrollable interior
interior = QScrollArea() interior = QScrollArea()
interior.setParent(self) interior.setParent(self)
# sub = BasicSubmission.query(id=id)
self.base_dict = sub.to_dict(full_data=True) 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'})}") 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 # don't want id
del self.base_dict['id'] del self.base_dict['id']
logger.debug(f"Creating barcode.") logger.debug(f"Creating barcode.")
if not check_if_app(): 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') self.base_dict['barcode'] = base64.b64encode(sub.make_plate_barcode(width=120, height=30)).decode('utf-8')
logger.debug(f"Hitpicking plate...")
self.plate_dicto = sub.hitpick_plate()
logger.debug(f"Making platemap...") logger.debug(f"Making platemap...")
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto) self.base_dict['platemap'] = sub.make_plate_map()
self.template = env.get_template("submission_details.html") self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
self.html = self.template.render(sub=self.base_dict) self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView() webview = QWebEngineView()
webview.setMinimumSize(900, 500) webview.setMinimumSize(900, 500)
@@ -63,21 +59,29 @@ class SubmissionDetails(QDialog):
btn.setParent(self) btn.setParent(self)
btn.setFixedWidth(900) btn.setFixedWidth(900)
btn.clicked.connect(self.export) btn.clicked.connect(self.export)
def export(self): def export(self):
""" """
Renders submission to html, then creates and saves .pdf file to user selected file. 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") 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() 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: try:
export_map.save(image_io, 'JPEG') export_map.save(image_io, 'JPEG')
except AttributeError: except AttributeError:
logger.error(f"No plate map found") logger.error(f"No plate map found")
self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8') 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) self.html2 = self.template.render(sub=self.base_dict)
with open("test.html", "w") as fw:
fw.write(self.html2)
try: try:
with open(fname, "w+b") as f: with open(fname, "w+b") as f:
pisa.CreatePDF(self.html2, dest=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.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error") msg.setWindowTitle("Permission Error")
msg.exec() 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): class SubmissionComment(QDialog):
""" """
@@ -163,7 +100,6 @@ class SubmissionComment(QDialog):
def __init__(self, parent, submission:BasicSubmission) -> None: def __init__(self, parent, submission:BasicSubmission) -> None:
super().__init__(parent) super().__init__(parent)
# self.ctx = ctx
try: try:
self.app = parent.parent().parent().parent().parent().parent().parent self.app = parent.parent().parent().parent().parent().parent().parent
print(f"App: {self.app}") print(f"App: {self.app}")
@@ -185,7 +121,7 @@ class SubmissionComment(QDialog):
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom) self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self): def parse_form(self) -> List[dict]:
""" """
Adds comment to submission object. Adds comment to submission object.
""" """

View File

@@ -1,37 +1,22 @@
''' '''
Contains widgets specific to the submission summary and submission details. Contains widgets specific to the submission summary and submission details.
''' '''
import base64, logging, json import logging, json
from datetime import datetime
from io import BytesIO
from pprint import pformat from pprint import pformat
from PyQt6 import QtPrintSupport from PyQt6.QtWidgets import QTableView, QMenu
from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea,
QMessageBox, QMenu, QLabel,
QDialogButtonBox, QToolBar
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter from PyQt6.QtGui import QAction, QCursor
from backend.db.models import BasicSubmission, Equipment from backend.db.models import BasicSubmission
from backend.excel import make_report_html, make_report_xlsx 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 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 .functions import select_save_file, select_open_file
from .misc import ReportDatePicker from .misc import ReportDatePicker
import pandas as pd import pandas as pd
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class pandasModel(QAbstractTableModel): class pandasModel(QAbstractTableModel):
""" """
pandas model for inserting summary sheet into gui pandas model for inserting summary sheet into gui
@@ -89,20 +74,17 @@ class SubmissionsSheet(QTableView):
""" """
super().__init__(parent) super().__init__(parent)
self.app = self.parent() self.app = self.parent()
# self.ctx = ctx
self.report = Report() self.report = Report()
self.setData() self.setData()
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) 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)) self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
def setData(self) -> None: def setData(self) -> None:
""" """
sets data in model sets data in model
""" """
# self.data = submissions_to_df()
self.data = BasicSubmission.submissions_to_df() self.data = BasicSubmission.submissions_to_df()
try: try:
self.data['id'] = self.data['id'].apply(str) self.data['id'] = self.data['id'].apply(str)
@@ -114,39 +96,6 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data)) proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel) 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): def contextMenuEvent(self, event):
""" """
Creates actions for right click menu events. Creates actions for right click menu events.
@@ -158,21 +107,6 @@ class SubmissionsSheet(QTableView):
id = id.sibling(id.row(),0).data() id = id.sibling(id.row(),0).data()
submission = BasicSubmission.query(id=id) submission = BasicSubmission.query(id=id)
self.menu = QMenu(self) 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() self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys(): for k in self.con_actions.keys():
logger.debug(f"Adding {k}") logger.debug(f"Adding {k}")
@@ -183,57 +117,21 @@ class SubmissionsSheet(QTableView):
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
def triggered_action(self, action_name:str): 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"Action: {action_name}")
logger.debug(f"Responding with {self.con_actions[action_name]}") logger.debug(f"Responding with {self.con_actions[action_name]}")
func = self.con_actions[action_name] func = self.con_actions[action_name]
func(obj=self) 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): def link_extractions(self):
"""
Pull extraction logs into the db
"""
self.link_extractions_function() self.link_extractions_function()
self.app.report.add_result(self.report) self.app.report.add_result(self.report)
self.report = 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')) self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def link_pcr(self): def link_pcr(self):
"""
Pull pcr logs into the db
"""
self.link_pcr_function() self.link_pcr_function()
self.app.report.add_result(self.report) self.app.report.add_result(self.report)
self.report = 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')) self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def generate_report(self): def generate_report(self):
"""
Make a report
"""
self.generate_report_function() self.generate_report_function()
self.app.report.add_result(self.report) self.app.report.add_result(self.report)
self.report = Report() self.report = Report()
@@ -436,12 +340,3 @@ class SubmissionsSheet(QTableView):
cell.style = 'Currency' cell.style = 'Currency'
writer.close() writer.close()
self.report.add_result(report) 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 ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea, QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel, QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox, QLineEdit, QSpinBox
QSpinBox, QDateEdit
) )
from sqlalchemy import FLOAT, INTEGER
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentRoleAssociation, BasicSubmission from backend.db import SubmissionType, BasicSubmission
from backend.validators import PydReagentType, PydKit
import logging import logging
from pprint import pformat
from tools import Report from tools import Report
from typing import Tuple
from .functions import select_open_file from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionTypeAdder(QWidget): class SubmissionTypeAdder(QWidget):
@@ -46,35 +39,21 @@ class SubmissionTypeAdder(QWidget):
self.grid.addWidget(template_selector,3,1) self.grid.addWidget(template_selector,3,1)
self.template_label = QLabel("None") self.template_label = QLabel("None")
self.grid.addWidget(self.template_label,3,2) self.grid.addWidget(self.template_label,3,2)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit # widget to get uses of kit
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost'] 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 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} 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): for iii, key in enumerate(self.columns):
idx = iii + 4 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) self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
scroll.setWidget(scrollContent) scroll.setWidget(scrollContent)
self.submit_btn.clicked.connect(self.submit) self.submit_btn.clicked.connect(self.submit)
template_selector.clicked.connect(self.get_template_path) template_selector.clicked.connect(self.get_template_path)
def submit(self): def submit(self):
"""
Create SubmissionType and send to db
"""
info = self.parse_form() info = self.parse_form()
ST = SubmissionType(name=self.st_name.text(), info_map=info) ST = SubmissionType(name=self.st_name.text(), info_map=info)
try: try:
@@ -84,11 +63,20 @@ class SubmissionTypeAdder(QWidget):
logger.error(f"Could not find template file: {self.template_path}") logger.error(f"Could not find template file: {self.template_path}")
ST.save(ctx=self.app.ctx) 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)] widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
return {widget.objectName():widget.parse_form() for widget in widgets} return {widget.objectName():widget.parse_form() for widget in widgets}
def get_template_path(self): 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_path = select_open_file(obj=self, file_extension="xlsx")
self.template_label.setText(self.template_path.__str__()) self.template_label.setText(self.template_path.__str__())
@@ -113,7 +101,13 @@ class InfoWidget(QWidget):
self.column.setObjectName("column") self.column.setObjectName("column")
grid.addWidget(self.column,2,3) 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( return dict(
sheets = self.sheet.text().split(","), sheets = self.sheet.text().split(","),
row = self.row.value(), row = self.row.value(),

View File

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

View File

@@ -1,6 +1,7 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
{% block head %}
<style> <style>
/* Tooltip container */ /* Tooltip container */
.tooltip { .tooltip {
@@ -34,11 +35,13 @@
} }
</style> </style>
<title>Submission Details for {{ sub['Plate Number'] }}</title> <title>Submission Details for {{ sub['Plate Number'] }}</title>
{% endblock %}
</head> </head>
{% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %}
<body> <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 %} <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> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key }}: </b>{% if key=='Cost' %}{% if sub['Cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
{% endfor %}</p> {% endfor %}</p>
<h3><u>Reagents:</u></h3> <h3><u>Reagents:</u></h3>
@@ -111,5 +114,6 @@
<h3><u>Plate map:</u></h3> <h3><u>Plate map:</u></h3>
<img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}"> <img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}">
{% endif %} {% endif %}
{% endblock %}
</body> </body>
</html> </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 return None
def check_regex_match(pattern:str, check:str) -> bool: 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: try:
return bool(re.match(fr"{pattern}", check)) return bool(re.match(fr"{pattern}", check))
except TypeError: except TypeError:
@@ -375,37 +385,6 @@ def jinja_template_loading() -> Environment:
env.globals['STATIC_PREFIX'] = loader_path.joinpath("static", "css") env.globals['STATIC_PREFIX'] = loader_path.joinpath("static", "css")
return env 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: def check_if_app() -> bool:
""" """
Checks if the program is running from pyinstaller compiled 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: Returns:
Tuple[int, int]: row, column 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: try:
row = int(row_keys[input_str[0].upper()]) row = int(row_keys[input_str[0].upper()])
column = int(input_str[1:]) 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 None, None
return row, column 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: Args:
query (Query): Query object func (_type_): _description_
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 setup_lookup(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
for k, v in locals().items(): for k, v in locals().items():
if k == "kwargs": if k == "kwargs":
@@ -509,32 +474,30 @@ class Report(BaseModel):
except AttributeError: except AttributeError:
logger.error(f"Problem adding result.") logger.error(f"Problem adding result.")
case Report(): case Report():
# logger.debug(f"Adding all results in report to new report")
for res in result.results: for res in result.results:
logger.debug(f"Adding {res} from to results.") logger.debug(f"Adding {res} from to results.")
self.results.append(res) self.results.append(res)
case _: case _:
pass 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]: def rreplace(s, old, new):
if check_not_nan(value): return (s[::-1].replace(old[::-1],new[::-1], 1))[::-1]
return value, False
else:
return convert_nans_to_nones(value), True
ctx = get_config(None) 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