Mid documentation and code clean-up.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
## 202406.04
|
## 202406.04
|
||||||
|
|
||||||
|
- Exported submission details will now be in docx format.
|
||||||
- Adding in tips to Equipment usage.
|
- Adding in tips to Equipment usage.
|
||||||
- New WastewaterArticAssociation will track previously missed sample info.
|
- New WastewaterArticAssociation will track previously missed sample info.
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class BaseClass(Base):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def execute_query(cls, query: Query = None, model=None, limit: int = 0, **kwargs) -> Any | List[Any]:
|
def execute_query(cls, query: Query = None, model=None, limit: int = 0, **kwargs) -> Any | List[Any]:
|
||||||
"""
|
"""
|
||||||
Execute sqlalchemy query.
|
Execute sqlalchemy query with relevant defaults.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model (Any, optional): model to be queried. Defaults to None
|
model (Any, optional): model to be queried. Defaults to None
|
||||||
@@ -137,6 +137,7 @@ class BaseClass(Base):
|
|||||||
except (ArgumentError, AttributeError) as e:
|
except (ArgumentError, AttributeError) as e:
|
||||||
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
|
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
|
||||||
if k in singles:
|
if k in singles:
|
||||||
|
logger.warning(f"{k} is in singles. Returning only one value.")
|
||||||
limit = 1
|
limit = 1
|
||||||
with query.session.no_autoflush:
|
with query.session.no_autoflush:
|
||||||
match limit:
|
match limit:
|
||||||
@@ -165,8 +166,8 @@ class ConfigItem(BaseClass):
|
|||||||
Key:JSON objects to store config settings in database.
|
Key:JSON objects to store config settings in database.
|
||||||
"""
|
"""
|
||||||
id = Column(INTEGER, primary_key=True)
|
id = Column(INTEGER, primary_key=True)
|
||||||
key = Column(String(32))
|
key = Column(String(32)) #: Name of the configuration item.
|
||||||
value = Column(JSON)
|
value = Column(JSON) #: Value associated with the config item.
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"ConfigItem({self.key} : {self.value})"
|
return f"ConfigItem({self.key} : {self.value})"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class ControlType(BaseClass):
|
|||||||
|
|
||||||
def get_subtypes(self, mode: str) -> List[str]:
|
def get_subtypes(self, mode: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get subtypes associated with this controltype
|
Get subtypes associated with this controltype (currently used only for Kraken)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mode (str): analysis mode name
|
mode (str): analysis mode name
|
||||||
@@ -140,15 +140,11 @@ class Control(BaseClass):
|
|||||||
kraken = {}
|
kraken = {}
|
||||||
# logger.debug("calculating 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 = []
|
# logger.debug("Creating new kraken.")
|
||||||
for item in kraken:
|
new_kraken = [dict(name=item, kraken_count=kraken[item]['kraken_count'], kraken_percent="{0:.0%}".format(kraken[item]['kraken_count'] / kraken_cnt_total)) for item in kraken]
|
||||||
# logger.debug("calculating kraken percent (overwrites what's already been scraped)")
|
|
||||||
kraken_percent = kraken[item]['kraken_count'] / kraken_cnt_total
|
|
||||||
new_kraken.append({'name': item, 'kraken_count': kraken[item]['kraken_count'],
|
|
||||||
'kraken_percent': "{0:.0%}".format(kraken_percent)})
|
|
||||||
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
|
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
|
||||||
# logger.debug("setting targets")
|
# logger.debug("setting targets")
|
||||||
if self.controltype.targets == []:
|
if not self.controltype.targets:
|
||||||
targets = ["None"]
|
targets = ["None"]
|
||||||
else:
|
else:
|
||||||
targets = self.controltype.targets
|
targets = self.controltype.targets
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ kittypes_processes = Table(
|
|||||||
extend_existing=True
|
extend_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# logger.debug("Table for TipRole/Tips relations")
|
||||||
tiproles_tips = Table(
|
tiproles_tips = Table(
|
||||||
"_tiproles_tips",
|
"_tiproles_tips",
|
||||||
Base.metadata,
|
Base.metadata,
|
||||||
@@ -78,6 +79,7 @@ tiproles_tips = Table(
|
|||||||
extend_existing=True
|
extend_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# logger.debug("Table for Process/TipRole relations")
|
||||||
process_tiprole = Table(
|
process_tiprole = Table(
|
||||||
"_process_tiprole",
|
"_process_tiprole",
|
||||||
Base.metadata,
|
Base.metadata,
|
||||||
@@ -86,6 +88,7 @@ process_tiprole = Table(
|
|||||||
extend_existing=True
|
extend_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# logger.debug("Table for Equipment/Tips relations")
|
||||||
equipment_tips = Table(
|
equipment_tips = Table(
|
||||||
"_equipment_tips",
|
"_equipment_tips",
|
||||||
Base.metadata,
|
Base.metadata,
|
||||||
@@ -410,6 +413,7 @@ class Reagent(BaseClass):
|
|||||||
except (TypeError, AttributeError) as e:
|
except (TypeError, AttributeError) as e:
|
||||||
place_holder = date.today()
|
place_holder = date.today()
|
||||||
logger.error(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing")
|
logger.error(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing")
|
||||||
|
# NOTE: The notation for not having an expiry is 1970.1.1
|
||||||
if self.expiry.year == 1970:
|
if self.expiry.year == 1970:
|
||||||
place_holder = "NA"
|
place_holder = "NA"
|
||||||
else:
|
else:
|
||||||
@@ -700,7 +704,13 @@ class SubmissionType(BaseClass):
|
|||||||
output[item.equipment_role.name] = emap
|
output[item.equipment_role.name] = emap
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def construct_tips_map(self):
|
def construct_tips_map(self) -> dict:
|
||||||
|
"""
|
||||||
|
Constructs map of tips to excel cells.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Tip locations in the excel sheet.
|
||||||
|
"""
|
||||||
output = {}
|
output = {}
|
||||||
for item in self.submissiontype_tiprole_associations:
|
for item in self.submissiontype_tiprole_associations:
|
||||||
tmap = item.uses
|
tmap = item.uses
|
||||||
@@ -1142,6 +1152,7 @@ class Equipment(BaseClass):
|
|||||||
processes = [process for process in processes if extraction_kit in process.kit_types]
|
processes = [process for process in processes if extraction_kit in process.kit_types]
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
# NOTE: Convert to strings
|
||||||
processes = [process.name for process in processes]
|
processes = [process.name for process in processes]
|
||||||
assert all([isinstance(process, str) for process in processes])
|
assert all([isinstance(process, str) for process in processes])
|
||||||
if len(processes) == 0:
|
if len(processes) == 0:
|
||||||
@@ -1193,7 +1204,8 @@ class Equipment(BaseClass):
|
|||||||
return cls.execute_query(query=query, limit=limit)
|
return cls.execute_query(query=query, limit=limit)
|
||||||
|
|
||||||
def to_pydantic(self, submission_type: SubmissionType,
|
def to_pydantic(self, submission_type: SubmissionType,
|
||||||
extraction_kit: str | KitType | None = None) -> "PydEquipment":
|
extraction_kit: str | KitType | None = None,
|
||||||
|
role: str = None) -> "PydEquipment":
|
||||||
"""
|
"""
|
||||||
Creates PydEquipment of this Equipment
|
Creates PydEquipment of this Equipment
|
||||||
|
|
||||||
@@ -1206,7 +1218,7 @@ class Equipment(BaseClass):
|
|||||||
"""
|
"""
|
||||||
from backend.validators.pydant import PydEquipment
|
from backend.validators.pydant import PydEquipment
|
||||||
return PydEquipment(
|
return PydEquipment(
|
||||||
processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=None,
|
processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=role,
|
||||||
**self.to_dict(processes=False))
|
**self.to_dict(processes=False))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ orgs_contacts = Table(
|
|||||||
Base.metadata,
|
Base.metadata,
|
||||||
Column("org_id", INTEGER, ForeignKey("_organization.id")),
|
Column("org_id", INTEGER, ForeignKey("_organization.id")),
|
||||||
Column("contact_id", INTEGER, ForeignKey("_contact.id")),
|
Column("contact_id", INTEGER, ForeignKey("_contact.id")),
|
||||||
# __table_args__ = {'extend_existing': True}
|
|
||||||
extend_existing=True
|
extend_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from pathlib import Path
|
|||||||
from jinja2.exceptions import TemplateNotFound
|
from jinja2.exceptions import TemplateNotFound
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from docxtpl import InlineImage
|
from docxtpl import InlineImage
|
||||||
from io import BytesIO
|
from docx.shared import Inches
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -229,6 +229,15 @@ class BasicSubmission(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def finalize_details(cls, input_dict: dict) -> dict:
|
def finalize_details(cls, input_dict: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Make final adjustments to the details dictionary before display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Incoming dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Final details dictionary.
|
||||||
|
"""
|
||||||
del input_dict['id']
|
del input_dict['id']
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
@@ -610,6 +619,12 @@ class BasicSubmission(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_regex(cls) -> str:
|
def get_regex(cls) -> str:
|
||||||
|
"""
|
||||||
|
Dummy for inheritence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Regex for submission type.
|
||||||
|
"""
|
||||||
return cls.construct_regex()
|
return cls.construct_regex()
|
||||||
|
|
||||||
# Polymorphic functions
|
# Polymorphic functions
|
||||||
@@ -733,7 +748,16 @@ class BasicSubmission(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_docx_writer(cls, input_dict:dict, tpl_obj=None):
|
def custom_docx_writer(cls, input_dict:dict, tpl_obj=None):
|
||||||
|
"""
|
||||||
|
Adds custom fields to docx template writer for exported details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Incoming default dictionary.
|
||||||
|
tpl_obj (_type_, optional): Template object. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary with information added.
|
||||||
|
"""
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -785,6 +809,7 @@ class BasicSubmission(BaseClass):
|
|||||||
repeat = ""
|
repeat = ""
|
||||||
outstr = re.sub(r"(-\dR)\d?", rf"\1 {repeat}", outstr).replace(" ", "")
|
outstr = re.sub(r"(-\dR)\d?", rf"\1 {repeat}", outstr).replace(" ", "")
|
||||||
abb = cls.get_default_info('abbreviation')
|
abb = cls.get_default_info('abbreviation')
|
||||||
|
outstr = re.sub(rf"RSL{abb}", rf"RSL-{abb}", outstr)
|
||||||
return re.sub(rf"{abb}(\d)", rf"{abb}-\1", outstr)
|
return re.sub(rf"{abb}(\d)", rf"{abb}-\1", outstr)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -924,7 +949,7 @@ class BasicSubmission(BaseClass):
|
|||||||
if submission_type is not None:
|
if submission_type is not None:
|
||||||
model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type)
|
model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type)
|
||||||
elif len(kwargs) > 0:
|
elif len(kwargs) > 0:
|
||||||
# find the subclass containing the relevant attributes
|
# NOTE: find the subclass containing the relevant attributes
|
||||||
# logger.debug(f"Attributes for search: {kwargs}")
|
# logger.debug(f"Attributes for search: {kwargs}")
|
||||||
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
||||||
else:
|
else:
|
||||||
@@ -1241,7 +1266,7 @@ class BacterialCulture(BasicSubmission):
|
|||||||
plate_map (dict | None, optional): _description_. Defaults to None.
|
plate_map (dict | None, optional): _description_. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: _description_
|
dict: Updated dictionary.
|
||||||
"""
|
"""
|
||||||
from . import ControlType
|
from . import ControlType
|
||||||
input_dict = super().finalize_parse(input_dict, xl, info_map)
|
input_dict = super().finalize_parse(input_dict, xl, info_map)
|
||||||
@@ -1495,7 +1520,17 @@ class Wastewater(BasicSubmission):
|
|||||||
# 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'))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_docx_writer(cls, input_dict:dict, tpl_obj=None):
|
def custom_docx_writer(cls, input_dict:dict, tpl_obj=None) -> dict:
|
||||||
|
"""
|
||||||
|
Adds custom fields to docx template writer for exported details. Extends parent.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Incoming default dictionary.
|
||||||
|
tpl_obj (_type_, optional): Template object. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary with information added.
|
||||||
|
"""
|
||||||
from backend.excel.writer import DocxWriter
|
from backend.excel.writer import DocxWriter
|
||||||
input_dict = super().custom_docx_writer(input_dict)
|
input_dict = super().custom_docx_writer(input_dict)
|
||||||
well_24 = []
|
well_24 = []
|
||||||
@@ -1611,6 +1646,7 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
instr = re.sub(r"^(\d{6})", f"RSL-AR-\\1", instr)
|
instr = re.sub(r"^(\d{6})", f"RSL-AR-\\1", instr)
|
||||||
# logger.debug(f"name coming out of Artic namer: {instr}")
|
# logger.debug(f"name coming out of Artic namer: {instr}")
|
||||||
outstr = super().enforce_name(instr=instr, data=data)
|
outstr = super().enforce_name(instr=instr, data=data)
|
||||||
|
outstr = outstr.replace("RSLAR", "RSL-AR")
|
||||||
return outstr
|
return outstr
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1788,7 +1824,6 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
input_excel = super().custom_info_writer(input_excel, info, backup)
|
input_excel = super().custom_info_writer(input_excel, info, backup)
|
||||||
# logger.debug(f"Info:\n{pformat(info)}")
|
# logger.debug(f"Info:\n{pformat(info)}")
|
||||||
# NOTE: check for source plate information
|
# NOTE: check for source plate information
|
||||||
# check = 'source_plates' in info.keys() and info['source_plates'] is not None
|
|
||||||
if check_key_or_attr(key='source_plates', interest=info, check_none=True):
|
if check_key_or_attr(key='source_plates', interest=info, check_none=True):
|
||||||
worksheet = input_excel['First Strand List']
|
worksheet = input_excel['First Strand List']
|
||||||
start_row = 8
|
start_row = 8
|
||||||
@@ -1805,15 +1840,11 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
# NOTE: check for gel information
|
# NOTE: check for gel information
|
||||||
# check = 'gel_info' in info.keys() and info['gel_info']['value'] is not None
|
|
||||||
if check_key_or_attr(key='gel_info', interest=info, check_none=True):
|
if check_key_or_attr(key='gel_info', interest=info, check_none=True):
|
||||||
# logger.debug(f"Gel info check passed.")
|
# logger.debug(f"Gel info check passed.")
|
||||||
# if info['gel_info'] is not None:
|
|
||||||
# logger.debug(f"Gel info not none.")
|
|
||||||
# NOTE: print json field gel results to Egel results
|
# NOTE: print json field gel results to Egel results
|
||||||
worksheet = input_excel['Egel results']
|
worksheet = input_excel['Egel results']
|
||||||
# TODO: Move all this into a seperate function?
|
# TODO: Move all this into a seperate function?
|
||||||
#
|
|
||||||
start_row = 21
|
start_row = 21
|
||||||
start_column = 15
|
start_column = 15
|
||||||
for row, ki in enumerate(info['gel_info']['value'], start=1):
|
for row, ki in enumerate(info['gel_info']['value'], start=1):
|
||||||
@@ -1831,13 +1862,11 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
worksheet.cell(row=row, column=column, value=kj['value'])
|
worksheet.cell(row=row, column=column, value=kj['value'])
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.error(f"Failed {kj['name']} with value {kj['value']} to row {row}, column {column}")
|
logger.error(f"Failed {kj['name']} with value {kj['value']} to row {row}, column {column}")
|
||||||
# check = 'gel_image' in info.keys() and info['gel_image']['value'] is not None
|
if check_key_or_attr(key='gel_image_path', interest=info, check_none=True):
|
||||||
if check_key_or_attr(key='gel_image', interest=info, check_none=True):
|
|
||||||
# if info['gel_image'] is not None:
|
|
||||||
worksheet = input_excel['Egel results']
|
worksheet = input_excel['Egel results']
|
||||||
# logger.debug(f"We got an image: {info['gel_image']}")
|
# logger.debug(f"We got an image: {info['gel_image']}")
|
||||||
with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped:
|
with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped:
|
||||||
z = zipped.extract(info['gel_image']['value'], Path(TemporaryDirectory().name))
|
z = zipped.extract(info['gel_image_path']['value'], Path(TemporaryDirectory().name))
|
||||||
img = OpenpyxlImage(z)
|
img = OpenpyxlImage(z)
|
||||||
img.height = 400 # insert image height in pixels as float or int (e.g. 305.5)
|
img.height = 400 # insert image height in pixels as float or int (e.g. 305.5)
|
||||||
img.width = 600
|
img.width = 600
|
||||||
@@ -1860,36 +1889,16 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number", "source_plates",
|
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number", "source_plates",
|
||||||
"gel_controls, gel_image_path"]
|
"gel_controls, gel_image_path"]
|
||||||
base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
|
base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
|
||||||
# check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] is not None
|
|
||||||
if check_key_or_attr(key='gel_info', interest=base_dict, check_none=True):
|
if check_key_or_attr(key='gel_info', interest=base_dict, check_none=True):
|
||||||
headers = [item['name'] for item in base_dict['gel_info'][0]['values']]
|
headers = [item['name'] for item in base_dict['gel_info'][0]['values']]
|
||||||
base_dict['headers'] = [''] * (4 - len(headers))
|
base_dict['headers'] = [''] * (4 - len(headers))
|
||||||
base_dict['headers'] += headers
|
base_dict['headers'] += headers
|
||||||
# logger.debug(f"Gel info: {pformat(base_dict['headers'])}")
|
# logger.debug(f"Gel info: {pformat(base_dict['headers'])}")
|
||||||
# check = 'gel_image' in base_dict.keys() and base_dict['gel_image'] is not None
|
|
||||||
if check_key_or_attr(key='gel_image_path', interest=base_dict, check_none=True):
|
if check_key_or_attr(key='gel_image_path', interest=base_dict, check_none=True):
|
||||||
with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped:
|
with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped:
|
||||||
base_dict['gel_image'] = base64.b64encode(zipped.read(base_dict['gel_image_path'])).decode('utf-8')
|
base_dict['gel_image'] = base64.b64encode(zipped.read(base_dict['gel_image_path'])).decode('utf-8')
|
||||||
return base_dict, template
|
return base_dict, template
|
||||||
|
|
||||||
# def adjust_to_dict_samples(self, backup: bool = False) -> List[dict]:
|
|
||||||
# """
|
|
||||||
# Updates sample dictionaries with custom values
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# backup (bool, optional): Whether to perform backup. Defaults to False.
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# List[dict]: Updated dictionaries
|
|
||||||
# """
|
|
||||||
# # logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
|
|
||||||
# output = []
|
|
||||||
#
|
|
||||||
# for assoc in self.submission_sample_associations:
|
|
||||||
# dicto = assoc.to_sub_dict()
|
|
||||||
# output.append(dicto)
|
|
||||||
# return output
|
|
||||||
|
|
||||||
def custom_context_events(self) -> dict:
|
def custom_context_events(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Creates dictionary of str:function to be passed to context menu. Extends parent
|
Creates dictionary of str:function to be passed to context menu. Extends parent
|
||||||
@@ -1902,6 +1911,13 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
return events
|
return events
|
||||||
|
|
||||||
def set_attribute(self, key: str, value):
|
def set_attribute(self, key: str, value):
|
||||||
|
"""
|
||||||
|
Performs custom attribute setting based on values. Extends parent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): name of attribute
|
||||||
|
value (_type_): value of attribute
|
||||||
|
"""
|
||||||
super().set_attribute(key=key, value=value)
|
super().set_attribute(key=key, value=value)
|
||||||
if key == 'gel_info':
|
if key == 'gel_info':
|
||||||
if len(self.gel_info) > 3:
|
if len(self.gel_info) > 3:
|
||||||
@@ -1938,7 +1954,17 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_docx_writer(cls, input_dict:dict, tpl_obj=None):
|
def custom_docx_writer(cls, input_dict:dict, tpl_obj=None) -> dict:
|
||||||
|
"""
|
||||||
|
Adds custom fields to docx template writer for exported details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Incoming default dictionary/
|
||||||
|
tpl_obj (_type_, optional): Template object. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary with information added.
|
||||||
|
"""
|
||||||
input_dict = super().custom_docx_writer(input_dict)
|
input_dict = super().custom_docx_writer(input_dict)
|
||||||
if check_key_or_attr(key='gel_image_path', interest=input_dict, check_none=True):
|
if check_key_or_attr(key='gel_image_path', interest=input_dict, check_none=True):
|
||||||
with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped:
|
with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped:
|
||||||
@@ -1946,12 +1972,11 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
with tempfile.TemporaryFile(mode="wb", suffix=".jpg", delete=False) as tmp:
|
with tempfile.TemporaryFile(mode="wb", suffix=".jpg", delete=False) as tmp:
|
||||||
tmp.write(img)
|
tmp.write(img)
|
||||||
logger.debug(f"Tempfile: {tmp.name}")
|
logger.debug(f"Tempfile: {tmp.name}")
|
||||||
img = InlineImage(tpl_obj, image_descriptor=tmp.name)#, width=5.5)#, height=400)
|
img = InlineImage(tpl_obj, image_descriptor=tmp.name, width=Inches(5.5))#, width=5.5)#, height=400)
|
||||||
input_dict['gel_image'] = img
|
input_dict['gel_image'] = img
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Sample Classes
|
# Sample Classes
|
||||||
|
|
||||||
class BasicSample(BaseClass):
|
class BasicSample(BaseClass):
|
||||||
@@ -2009,6 +2034,12 @@ class BasicSample(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def timestamps(cls) -> List[str]:
|
def timestamps(cls) -> List[str]:
|
||||||
|
"""
|
||||||
|
Constructs a list of all attributes stored as SQL Timestamps
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Attribute list
|
||||||
|
"""
|
||||||
output = [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
|
output = [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
|
||||||
if issubclass(cls, BasicSample) and not cls.__name__ == "BasicSample":
|
if issubclass(cls, BasicSample) and not cls.__name__ == "BasicSample":
|
||||||
output += BasicSample.timestamps()
|
output += BasicSample.timestamps()
|
||||||
@@ -2082,10 +2113,6 @@ class BasicSample(BaseClass):
|
|||||||
logger.info(f"Recruiting model: {model}")
|
logger.info(f"Recruiting model: {model}")
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sql_enforcer(cls, pyd_sample: "PydSample"):
|
|
||||||
return pyd_sample
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_sample(cls, input_dict: dict) -> dict:
|
def parse_sample(cls, input_dict: dict) -> dict:
|
||||||
f"""
|
f"""
|
||||||
@@ -2194,6 +2221,15 @@ class BasicSample(BaseClass):
|
|||||||
# limit: int = 0,
|
# limit: int = 0,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> List[BasicSample]:
|
) -> List[BasicSample]:
|
||||||
|
"""
|
||||||
|
Allows for fuzzy search of samples. (Experimental)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_type (str | BasicSample | None, optional): Type of sample. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[BasicSample]: List of samples that match kwarg search parameters.
|
||||||
|
"""
|
||||||
match sample_type:
|
match sample_type:
|
||||||
case str():
|
case str():
|
||||||
model = cls.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
model = cls.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
||||||
@@ -2220,26 +2256,20 @@ class BasicSample(BaseClass):
|
|||||||
return [dict(label="Submitter ID", field="submitter_id")]
|
return [dict(label="Submitter ID", field="submitter_id")]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def samples_to_df(cls, sample_type: str | None | BasicSample = None, **kwargs):
|
def samples_to_df(cls, sample_list:List[BasicSample], **kwargs) -> pd.DataFrame:
|
||||||
# def samples_to_df(cls, sample_type:str|None|BasicSample=None, searchables:dict={}):
|
"""
|
||||||
logger.debug(f"Checking {sample_type} with type {type(sample_type)}")
|
Runs a fuzzy search and converts into a dataframe.
|
||||||
match sample_type:
|
|
||||||
case str():
|
Args:
|
||||||
model = BasicSample.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
sample_list (List[BasicSample]): List of samples to be parsed. Defaults to None.
|
||||||
case _:
|
|
||||||
try:
|
Returns:
|
||||||
check = issubclass(sample_type, BasicSample)
|
pd.DataFrame: Dataframe all samples
|
||||||
except TypeError:
|
"""
|
||||||
check = False
|
if not isinstance(sample_list, list):
|
||||||
if check:
|
sample_list = [sample_list]
|
||||||
model = sample_type
|
|
||||||
else:
|
|
||||||
model = cls
|
|
||||||
q_out = model.fuzzy_search(sample_type=sample_type, **kwargs)
|
|
||||||
if not isinstance(q_out, list):
|
|
||||||
q_out = [q_out]
|
|
||||||
try:
|
try:
|
||||||
samples = [sample.to_sub_dict() for sample in q_out]
|
samples = [sample.to_sub_dict() for sample in sample_list]
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
logger.error(f"Couldn't find any samples with data: {kwargs}\nDue to {e}")
|
logger.error(f"Couldn't find any samples with data: {kwargs}\nDue to {e}")
|
||||||
return None
|
return None
|
||||||
@@ -2287,6 +2317,12 @@ class WastewaterSample(BasicSample):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_info(cls, *args):
|
def get_default_info(cls, *args):
|
||||||
|
"""
|
||||||
|
Returns default info for a model. Extends BaseClass method.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict | list | str: Output of key:value dict or single (list, str) desired variable
|
||||||
|
"""
|
||||||
dicto = super().get_default_info(*args)
|
dicto = super().get_default_info(*args)
|
||||||
match dicto:
|
match dicto:
|
||||||
case dict():
|
case dict():
|
||||||
|
|||||||
@@ -740,7 +740,10 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
if tips is None:
|
if tips is None:
|
||||||
continue
|
continue
|
||||||
logger.debug(f"Converting tips: {tips} to sql.")
|
logger.debug(f"Converting tips: {tips} to sql.")
|
||||||
association = tips.to_sql(submission=instance)
|
try:
|
||||||
|
association = tips.to_sql(submission=instance)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
if association is not None and association not in instance.submission_tips_associations:
|
if association is not None and association not in instance.submission_tips_associations:
|
||||||
# association.save()
|
# association.save()
|
||||||
instance.submission_tips_associations.append(association)
|
instance.submission_tips_associations.append(association)
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class RoleComboBox(QWidget):
|
|||||||
self.check.setChecked(False)
|
self.check.setChecked(False)
|
||||||
else:
|
else:
|
||||||
self.check.setChecked(True)
|
self.check.setChecked(True)
|
||||||
|
self.check.stateChanged.connect(self.toggle_checked)
|
||||||
self.box = QComboBox()
|
self.box = QComboBox()
|
||||||
self.box.setMaximumWidth(200)
|
self.box.setMaximumWidth(200)
|
||||||
self.box.setMinimumWidth(200)
|
self.box.setMinimumWidth(200)
|
||||||
@@ -118,6 +119,7 @@ class RoleComboBox(QWidget):
|
|||||||
self.layout.addWidget(self.box, 0, 2)
|
self.layout.addWidget(self.box, 0, 2)
|
||||||
self.layout.addWidget(self.process, 0, 3)
|
self.layout.addWidget(self.process, 0, 3)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
self.toggle_checked()
|
||||||
|
|
||||||
def update_processes(self):
|
def update_processes(self):
|
||||||
"""
|
"""
|
||||||
@@ -176,3 +178,14 @@ class RoleComboBox(QWidget):
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Could create PydEquipment due to: {e}")
|
logger.error(f"Could create PydEquipment due to: {e}")
|
||||||
|
|
||||||
|
def toggle_checked(self):
|
||||||
|
for widget in self.findChildren(QWidget):
|
||||||
|
match widget:
|
||||||
|
case QCheckBox():
|
||||||
|
continue
|
||||||
|
case _:
|
||||||
|
if self.check.isChecked():
|
||||||
|
widget.setEnabled(True)
|
||||||
|
else:
|
||||||
|
widget.setEnabled(False)
|
||||||
@@ -51,7 +51,9 @@ class SearchBox(QDialog):
|
|||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
fields = self.parse_form()
|
fields = self.parse_form()
|
||||||
data = self.type.samples_to_df(sample_type=self.type, **fields)
|
# data = self.type.samples_to_df(sample_type=self.type, **fields)
|
||||||
|
data = self.type.fuzzy_search(sample_type=self.type, **fields)
|
||||||
|
data = self.type.samples_to_df(sample_list=data)
|
||||||
# logger.debug(f"Data: {data}")
|
# logger.debug(f"Data: {data}")
|
||||||
self.results.setData(df=data)
|
self.results.setData(df=data)
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ from PIL import Image
|
|||||||
from typing import List
|
from typing import List
|
||||||
from backend.excel.writer import DocxWriter
|
from backend.excel.writer import DocxWriter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
|
|
||||||
@@ -97,7 +95,7 @@ class SubmissionDetails(QDialog):
|
|||||||
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
|
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
|
||||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||||
css = f.read()
|
css = f.read()
|
||||||
logger.debug(f"Submission_details: {pformat(self.base_dict)}")
|
# logger.debug(f"Submission_details: {pformat(self.base_dict)}")
|
||||||
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css)
|
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css)
|
||||||
self.webview.setHtml(self.html)
|
self.webview.setHtml(self.html)
|
||||||
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
|
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
|
||||||
@@ -118,30 +116,10 @@ class SubmissionDetails(QDialog):
|
|||||||
writer = DocxWriter(base_dict=self.base_dict)
|
writer = DocxWriter(base_dict=self.base_dict)
|
||||||
fname = select_save_file(obj=self, default_name=self.base_dict['plate_number'], extension="docx")
|
fname = select_save_file(obj=self, default_name=self.base_dict['plate_number'], extension="docx")
|
||||||
writer.save(fname)
|
writer.save(fname)
|
||||||
# image_io = BytesIO()
|
|
||||||
# temp_dir = Path(TemporaryDirectory().name)
|
|
||||||
# hti = Html2Image(output_path=temp_dir, size=(2400, 1500))
|
|
||||||
# temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name)
|
|
||||||
# screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
|
|
||||||
# export_map = Image.open(screenshot[0])
|
|
||||||
# export_map = export_map.convert('RGB')
|
|
||||||
# try:
|
|
||||||
# export_map.save(image_io, 'JPEG')
|
|
||||||
# except AttributeError:
|
|
||||||
# logger.error(f"No plate map found")
|
|
||||||
# self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
|
||||||
# del self.base_dict['platemap']
|
|
||||||
# self.html2 = self.template.render(sub=self.base_dict)
|
|
||||||
try:
|
try:
|
||||||
html_to_pdf(html=self.html, output_file=fname)
|
html_to_pdf(html=self.html, output_file=fname)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
logger.error(f"Error saving pdf: {e}")
|
logger.error(f"Error saving pdf: {e}")
|
||||||
# msg = QMessageBox()
|
|
||||||
# msg.setText("Permission Error")
|
|
||||||
# msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
|
|
||||||
# msg.setWindowTitle("Permission Error")
|
|
||||||
# msg.exec()
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionComment(QDialog):
|
class SubmissionComment(QDialog):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user