Code cleanup and documentation
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
2
TODO.md
2
TODO.md
@@ -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*.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 *
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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 *
|
|
||||||
@@ -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")
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()}")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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> {% 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> {% 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'] %}
|
||||||
<b>{{ key }}: </b>{% if key=='Cost' %}{% if sub['Cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
<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>
|
||||||
38
src/submissions/templates/wastewaterartic_details.html
Normal file
38
src/submissions/templates/wastewaterartic_details.html
Normal 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>
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user