Replaced some list comprehension with generators, javascript in templates to create click event.

This commit is contained in:
lwark
2024-09-27 14:26:12 -05:00
parent c0f78390b5
commit 1e9923cc56
6 changed files with 178 additions and 116 deletions

View File

@@ -283,6 +283,17 @@ class BasicSubmission(BaseClass):
del input_dict['id']
return input_dict
def generate_associations(self, name:str, extra:str|None=None):
try:
field = self.__getattribute__(name)
except AttributeError:
return None
for item in field:
if extra:
yield item.to_sub_dict(extra)
else:
yield item.to_sub_dict()
def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict:
"""
Constructs dictionary used in submissions summary
@@ -332,6 +343,10 @@ class BasicSubmission(BaseClass):
try:
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in
self.submission_reagent_associations]
except Exception as e:
logger.error(f"We got an error retrieving reagents: {e}")
reagents = []
finally:
for k, v in self.extraction_kit.construct_xl_map_for_use(self.submission_type):
if k == 'info':
continue
@@ -341,26 +356,26 @@ class BasicSubmission(BaseClass):
reagents.append(
dict(role=k, name="Not Applicable", lot="NA", expiry=expiry,
missing=True))
except Exception as e:
logger.error(f"We got an error retrieving reagents: {e}")
reagents = None
# logger.debug(f"Running samples.")
samples = self.adjust_to_dict_samples(backup=backup)
# samples = self.adjust_to_dict_samples(backup=backup)
samples = self.generate_associations(name="submission_sample_associations")
# logger.debug("Running equipment")
try:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if not equipment:
equipment = None
except Exception as e:
logger.error(f"Error setting equipment: {e}")
equipment = None
try:
tips = [item.to_sub_dict() for item in self.submission_tips_associations]
if not tips:
tips = None
except Exception as e:
logger.error(f"Error setting tips: {e}")
tips = None
equipment = self.generate_associations(name="submission_equipment_associations")
# try:
# equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
# if not equipment:
# equipment = None
# except Exception as e:
# logger.error(f"Error setting equipment: {e}")
# equipment = None
tips = self.generate_associations(name="submission_tips_associations")
# try:
# tips = [item.to_sub_dict() for item in self.submission_tips_associations]
# if not tips:
# tips = None
# except Exception as e:
# logger.error(f"Error setting tips: {e}")
# tips = None
cost_centre = self.cost_centre
custom = self.custom
else:
@@ -1013,18 +1028,18 @@ class BasicSubmission(BaseClass):
logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler")
return samples
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.")
return [item.to_sub_dict() for item in self.submission_sample_associations]
# 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.")
# return [item.to_sub_dict() for item in self.submission_sample_associations]
@classmethod
def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]:

View File

@@ -53,7 +53,6 @@ class SheetParser(object):
self.parse_samples()
self.parse_equipment()
self.parse_tips()
# self.finalize_parse()
# logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}")
def parse_info(self):
@@ -63,6 +62,8 @@ class SheetParser(object):
parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object)
info = parser.parse_info()
self.info_map = parser.map
# NOTE: in order to accommodate generic submission types we have to check for the type in the excel sheet and
# rerun accordingly
try:
check = info['submission_type']['value'] not in [None, "None", "", " "]
except KeyError:
@@ -78,13 +79,15 @@ class SheetParser(object):
else:
self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath)
self.parse_info()
for k, v in info.items():
match k:
# NOTE: exclude samples.
case "sample":
continue
case _:
self.sub[k] = v
[self.sub.__setitem__(k, v) for k, v in info.items()]
# for k, v in info.items():
# match k:
# # NOTE: exclude samples.
# case "sample":
# logger.debug(f"Sample found: {k}: {v}")
# continue
# case _:
# self.sub[k] = v
def parse_reagents(self, extraction_kit: str | None = None):
"""
@@ -105,7 +108,7 @@ class SheetParser(object):
Calls sample parser to pull info from the excel sheet
"""
parser = SampleParser(xl=self.xl, submission_type=self.submission_type)
self.sub['samples'] = parser.reconcile_samples()
self.sub['samples'] = parser.parse_samples()
def parse_equipment(self):
"""
@@ -145,29 +148,29 @@ class SheetParser(object):
PydSubmission: output pydantic model
"""
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
pyd_dict = copy(self.sub)
pyd_dict['samples'] = [PydSample(**sample) for sample in self.sub['samples']]
# pyd_dict = copy(self.sub)
# self.sub['samples'] = [PydSample(**sample) for sample in self.sub['samples']]
# logger.debug(f"Reagents: {pformat(self.sub['reagents'])}")
pyd_dict['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']]
# self.sub['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']]
# logger.debug(f"Equipment: {self.sub['equipment']}")
try:
check = bool(self.sub['equipment'])
except TypeError:
check = False
if check:
pyd_dict['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']]
else:
pyd_dict['equipment'] = None
try:
check = bool(self.sub['tips'])
except TypeError:
check = False
if check:
pyd_dict['tips'] = [PydTips(**tips) for tips in self.sub['tips']]
else:
pyd_dict['tips'] = None
psm = PydSubmission(filepath=self.filepath, run_custom=True, **pyd_dict)
return psm
# try:
# check = bool(self.sub['equipment'])
# except TypeError:
# check = False
# if check:
# self.sub['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']]
# else:
# self.sub['equipment'] = None
# try:
# check = bool(self.sub['tips'])
# except TypeError:
# check = False
# if check:
# self.sub['tips'] = [PydTips(**tips) for tips in self.sub['tips']]
# else:
# self.sub['tips'] = None
return PydSubmission(filepath=self.filepath, run_custom=True, **self.sub)
# return psm
class InfoParser(object):
@@ -287,7 +290,7 @@ class ReagentParser(object):
extraction_kit (str): Extraction kit used.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
"""
# logger.debug("\n\nHello from ReagentParser!\n\n")
logger.debug("\n\nHello from ReagentParser!\n\n")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
self.submission_type_obj = submission_type
@@ -382,7 +385,7 @@ class SampleParser(object):
sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
"""
# logger.debug("\n\nHello from SampleParser!\n\n")
logger.debug("\n\nHello from SampleParser!\n\n")
self.samples = []
self.xl = xl
if isinstance(submission_type, str):
@@ -477,49 +480,50 @@ class SampleParser(object):
lookup_samples.append(self.samp_object.parse_sample(row_dict))
return lookup_samples
def parse_samples(self) -> Tuple[Report | None, List[dict] | List[PydSample]]:
"""
Parse merged platemap/lookup info into dicts/samples
# def parse_samples(self) -> Tuple[Report | None, List[dict] | List[PydSample]]:
# """
# Parse merged platemap/lookup info into dicts/samples
#
# Returns:
# List[dict]|List[models.BasicSample]: List of samples
# """
# result = None
# new_samples = []
# # logger.debug(f"Starting samples: {pformat(self.samples)}")
# for sample in self.samples:
# translated_dict = {}
# for k, v in sample.items():
# match v:
# case dict():
# v = None
# case float():
# v = convert_nans_to_nones(v)
# case _:
# v = v
# translated_dict[k] = convert_nans_to_nones(v)
# translated_dict['sample_type'] = f"{self.submission_type} Sample"
# translated_dict = self.sub_object.parse_samples(translated_dict)
# translated_dict = self.samp_object.parse_sample(translated_dict)
# # logger.debug(f"Here is the output of the custom parser:\n{translated_dict}")
# new_samples.append(PydSample(**translated_dict))
# return result, new_samples
Returns:
List[dict]|List[models.BasicSample]: List of samples
"""
result = None
new_samples = []
# logger.debug(f"Starting samples: {pformat(self.samples)}")
for sample in self.samples:
translated_dict = {}
for k, v in sample.items():
match v:
case dict():
v = None
case float():
v = convert_nans_to_nones(v)
case _:
v = v
translated_dict[k] = convert_nans_to_nones(v)
translated_dict['sample_type'] = f"{self.submission_type} Sample"
translated_dict = self.sub_object.parse_samples(translated_dict)
translated_dict = self.samp_object.parse_sample(translated_dict)
# logger.debug(f"Here is the output of the custom parser:\n{translated_dict}")
new_samples.append(PydSample(**translated_dict))
return result, new_samples
def reconcile_samples(self) -> Generator[dict, None, None]:
def parse_samples(self) -> Generator[dict, None, None]:
"""
Merges sample info from lookup table and plate map.
Returns:
List[dict]: Reconciled samples
"""
if self.plate_map_samples is None or self.lookup_samples is None:
if not self.plate_map_samples or not self.lookup_samples:
logger.error(f"No separate samples, returning")
self.samples = self.lookup_samples or self.plate_map_samples
return
merge_on_id = self.sample_info_map['lookup_table']['merge_on_id']
plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id'])
lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
print(pformat(plate_map_samples))
print(pformat(lookup_samples))
# print(pformat(plate_map_samples))
# print(pformat(lookup_samples))
for ii, psample in enumerate(plate_map_samples):
try:
check = psample['id'] == lookup_samples[ii][merge_on_id]
@@ -563,6 +567,7 @@ class EquipmentParser(object):
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
"""
logger.debug("\n\nHello from EquipmentParser!\n\n")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type
@@ -649,6 +654,7 @@ class TipParser(object):
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
"""
logger.debug("\n\nHello from TipParser!\n\n")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type

View File

@@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta
from dateutil.parser import parse
from dateutil.parser import ParserError
from typing import List, Tuple, Literal
from types import GeneratorType
from . import RSLNamer
from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result
@@ -395,16 +396,32 @@ class PydSubmission(BaseModel, extra='allow'):
submission_category: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
comment: dict | None = Field(default=dict(value="", missing=True), validate_default=True)
reagents: List[dict] | List[PydReagent] = []
samples: List[PydSample]
samples: List[PydSample] | Generator
equipment: List[PydEquipment] | None = []
cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
contact: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
tips: List[PydTips] | None =[]
@field_validator("tips", mode="before")
@classmethod
def expand_tips(cls, value):
# print(f"\n{type(value)}\n")
if isinstance(value, dict):
value = value['value']
if isinstance(value, Generator):
logger.debug("We have a generator")
return [PydTips(**tips) for tips in value]
if not value:
return []
return value
@field_validator('equipment', mode='before')
@classmethod
def convert_equipment_dict(cls, value):
# logger.debug(f"Equipment: {value}")
if isinstance(value, Generator):
logger.debug("We have a generator")
return [PydEquipment(**equipment) for equipment in value]
if isinstance(value, dict):
return value['value']
return value
@@ -580,6 +597,24 @@ class PydSubmission(BaseModel, extra='allow'):
value['value'] = values.data['submission_type']['value']
return value
@field_validator("reagents", mode="before")
@classmethod
def expand_reagents(cls, value):
# print(f"\n{type(value)}\n")
if isinstance(value, Generator):
logger.debug("We have a generator")
return [PydReagent(**reagent) for reagent in value]
return value
@field_validator("samples", mode="before")
@classmethod
def expand_samples(cls, value):
# print(f"\n{type(value)}\n")
if isinstance(value, Generator):
logger.debug("We have a generator")
return [PydSample(**sample) for sample in value]
return value
@field_validator("samples")
@classmethod
def assign_ids(cls, value):

View File

@@ -93,15 +93,16 @@ class SubmissionDetails(QDialog):
Args:
sample (str): Submitter Id of the sample.
"""
logger.debug(f"Details: {sample}")
if isinstance(sample, str):
sample = BasicSample.query(submitter_id=sample)
base_dict = sample.to_sub_dict(full_data=True)
exclude = ['submissions', 'excluded', 'colour', 'tooltip']
try:
base_dict['excluded'] += exclude
except KeyError:
base_dict['excluded'] = exclude
template = sample.get_details_template(base_dict=base_dict)
# try:
# base_dict['excluded'] += exclude
# except KeyError:
base_dict['excluded'] = exclude
template = sample.get_details_template()
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
@@ -158,6 +159,8 @@ class SubmissionDetails(QDialog):
# logger.debug(f"Submission_details: {pformat(self.base_dict)}")
# logger.debug(f"User is power user: {is_power_user()}")
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css)
with open("test.html", "w") as f:
f.write(self.html)
self.webview.setHtml(self.html)

View File

@@ -20,7 +20,7 @@
<h3><u>Reagents:</u></h3>
<p>{% for item in sub['reagents'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}</b>: <a class="data-link" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}</b>: <a class="data-link reagent" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
{% endfor %}</p>
{% if sub['equipment'] %}
@@ -38,7 +38,7 @@
{% if sub['samples'] %}
<h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b><a class="data-link" id="{{ item['submitter_id'] }}_alpha">{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}</a><br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b><a class="data-link sample" id="{{ item['submitter_id'] }}">{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}</a><br>
{% endfor %}</p>
{% endif %}
@@ -83,22 +83,25 @@
<script>
{% block script %}
{{ super() }}
{% for sample in sub['samples'] %}
document.getElementById("{{ sample['submitter_id'] }}").addEventListener("click", function(){
backend.sample_details("{{ sample['submitter_id'] }}");
});
document.getElementById("{{ sample['submitter_id'] }}_alpha").addEventListener("click", function(){
backend.sample_details("{{ sample['submitter_id'] }}");
});
{% endfor %}
{% for reagent in sub['reagents'] %}
document.getElementById("{{ reagent['lot'] }}").addEventListener("click", function(){
backend.reagent_details("{{ reagent['lot'] }}", "{{ sub['extraction_kit'] }}");
});
{% endfor %}
document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}");
});
var sampleSelection = document.getElementsByClassName('sample');
for(let i = 0; i < sampleSelection.length; i++) {
sampleSelection[i].addEventListener("click", function() {
backend.sample_details(sampleSelection[i].id);
})
}
var reagentSelection = document.getElementsByClassName('reagent');
for(let i = 0; i < reagentSelection.length; i++) {
reagentSelection[i].addEventListener("click", function() {
backend.reagent_details(reagentSelection[i].id, "{{ sub['extraction_kit'] }}");
})
}
{% endblock %}
</script>

View File

@@ -1,6 +1,6 @@
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
{% for sample in samples %}
<div class="well data-link" id="{{sample['submitter_id']}}" style="background-color: {{sample['background_color']}};
<div class="well data-link sample" id="{{sample['submitter_id']}}" style="background-color: {{sample['background_color']}};
border: 1px solid #000;
padding: 20px;
grid-column-start: {{sample['column']}};