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'] del input_dict['id']
return input_dict 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: def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict:
""" """
Constructs dictionary used in submissions summary Constructs dictionary used in submissions summary
@@ -332,6 +343,10 @@ class BasicSubmission(BaseClass):
try: try:
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in
self.submission_reagent_associations] 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): for k, v in self.extraction_kit.construct_xl_map_for_use(self.submission_type):
if k == 'info': if k == 'info':
continue continue
@@ -341,26 +356,26 @@ class BasicSubmission(BaseClass):
reagents.append( reagents.append(
dict(role=k, name="Not Applicable", lot="NA", expiry=expiry, dict(role=k, name="Not Applicable", lot="NA", expiry=expiry,
missing=True)) missing=True))
except Exception as e:
logger.error(f"We got an error retrieving reagents: {e}")
reagents = None
# logger.debug(f"Running samples.") # 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") # logger.debug("Running equipment")
try: equipment = self.generate_associations(name="submission_equipment_associations")
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations] # try:
if not equipment: # equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
equipment = None # if not equipment:
except Exception as e: # equipment = None
logger.error(f"Error setting equipment: {e}") # except Exception as e:
equipment = None # logger.error(f"Error setting equipment: {e}")
try: # equipment = None
tips = [item.to_sub_dict() for item in self.submission_tips_associations] tips = self.generate_associations(name="submission_tips_associations")
if not tips: # try:
tips = None # tips = [item.to_sub_dict() for item in self.submission_tips_associations]
except Exception as e: # if not tips:
logger.error(f"Error setting tips: {e}") # tips = None
tips = None # except Exception as e:
# logger.error(f"Error setting tips: {e}")
# tips = None
cost_centre = self.cost_centre cost_centre = self.cost_centre
custom = self.custom custom = self.custom
else: else:
@@ -1013,18 +1028,18 @@ class BasicSubmission(BaseClass):
logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler") logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler")
return samples return samples
def adjust_to_dict_samples(self, backup: bool = False) -> List[dict]: # def adjust_to_dict_samples(self, backup: bool = False) -> List[dict]:
""" # """
Updates sample dictionaries with custom values # Updates sample dictionaries with custom values
#
Args: # Args:
backup (bool, optional): Whether to perform backup. Defaults to False. # backup (bool, optional): Whether to perform backup. Defaults to False.
#
Returns: # Returns:
List[dict]: Updated dictionaries # List[dict]: Updated dictionaries
""" # """
# logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.") # # logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
return [item.to_sub_dict() for item in self.submission_sample_associations] # return [item.to_sub_dict() for item in self.submission_sample_associations]
@classmethod @classmethod
def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]: 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_samples()
self.parse_equipment() self.parse_equipment()
self.parse_tips() self.parse_tips()
# self.finalize_parse()
# logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}") # logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}")
def parse_info(self): 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) parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object)
info = parser.parse_info() info = parser.parse_info()
self.info_map = parser.map 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: try:
check = info['submission_type']['value'] not in [None, "None", "", " "] check = info['submission_type']['value'] not in [None, "None", "", " "]
except KeyError: except KeyError:
@@ -78,13 +79,15 @@ class SheetParser(object):
else: else:
self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath) self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath)
self.parse_info() self.parse_info()
for k, v in info.items(): [self.sub.__setitem__(k, v) for k, v in info.items()]
match k: # for k, v in info.items():
# NOTE: exclude samples. # match k:
case "sample": # # NOTE: exclude samples.
continue # case "sample":
case _: # logger.debug(f"Sample found: {k}: {v}")
self.sub[k] = v # continue
# case _:
# self.sub[k] = v
def parse_reagents(self, extraction_kit: str | None = None): 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 Calls sample parser to pull info from the excel sheet
""" """
parser = SampleParser(xl=self.xl, submission_type=self.submission_type) 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): def parse_equipment(self):
""" """
@@ -145,29 +148,29 @@ class SheetParser(object):
PydSubmission: output pydantic model PydSubmission: output pydantic model
""" """
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}") # logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
pyd_dict = copy(self.sub) # pyd_dict = copy(self.sub)
pyd_dict['samples'] = [PydSample(**sample) for sample in self.sub['samples']] # self.sub['samples'] = [PydSample(**sample) for sample in self.sub['samples']]
# logger.debug(f"Reagents: {pformat(self.sub['reagents'])}") # 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']}") # logger.debug(f"Equipment: {self.sub['equipment']}")
try: # try:
check = bool(self.sub['equipment']) # check = bool(self.sub['equipment'])
except TypeError: # except TypeError:
check = False # check = False
if check: # if check:
pyd_dict['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']] # self.sub['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']]
else: # else:
pyd_dict['equipment'] = None # self.sub['equipment'] = None
try: # try:
check = bool(self.sub['tips']) # check = bool(self.sub['tips'])
except TypeError: # except TypeError:
check = False # check = False
if check: # if check:
pyd_dict['tips'] = [PydTips(**tips) for tips in self.sub['tips']] # self.sub['tips'] = [PydTips(**tips) for tips in self.sub['tips']]
else: # else:
pyd_dict['tips'] = None # self.sub['tips'] = None
psm = PydSubmission(filepath=self.filepath, run_custom=True, **pyd_dict) return PydSubmission(filepath=self.filepath, run_custom=True, **self.sub)
return psm # return psm
class InfoParser(object): class InfoParser(object):
@@ -287,7 +290,7 @@ class ReagentParser(object):
extraction_kit (str): Extraction kit used. extraction_kit (str): Extraction kit used.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None. 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): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type_obj = 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. 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. 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.samples = []
self.xl = xl self.xl = xl
if isinstance(submission_type, str): if isinstance(submission_type, str):
@@ -477,49 +480,50 @@ class SampleParser(object):
lookup_samples.append(self.samp_object.parse_sample(row_dict)) lookup_samples.append(self.samp_object.parse_sample(row_dict))
return lookup_samples return lookup_samples
def parse_samples(self) -> Tuple[Report | None, List[dict] | List[PydSample]]: # def parse_samples(self) -> Tuple[Report | None, List[dict] | List[PydSample]]:
""" # """
Parse merged platemap/lookup info into dicts/samples # 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: def parse_samples(self) -> Generator[dict, None, None]:
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]:
""" """
Merges sample info from lookup table and plate map. Merges sample info from lookup table and plate map.
Returns: Returns:
List[dict]: Reconciled samples 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 self.samples = self.lookup_samples or self.plate_map_samples
return return
merge_on_id = self.sample_info_map['lookup_table']['merge_on_id'] 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']) 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]) lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
print(pformat(plate_map_samples)) # print(pformat(plate_map_samples))
print(pformat(lookup_samples)) # print(pformat(lookup_samples))
for ii, psample in enumerate(plate_map_samples): for ii, psample in enumerate(plate_map_samples):
try: try:
check = psample['id'] == lookup_samples[ii][merge_on_id] check = psample['id'] == lookup_samples[ii][merge_on_id]
@@ -563,6 +567,7 @@ class EquipmentParser(object):
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) 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): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -649,6 +654,7 @@ class TipParser(object):
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) 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): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = 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 parse
from dateutil.parser import ParserError from dateutil.parser import ParserError
from typing import List, Tuple, Literal from typing import List, Tuple, Literal
from types import GeneratorType
from . import RSLNamer from . import RSLNamer
from pathlib import Path from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result 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) 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) comment: dict | None = Field(default=dict(value="", missing=True), validate_default=True)
reagents: List[dict] | List[PydReagent] = [] reagents: List[dict] | List[PydReagent] = []
samples: List[PydSample] samples: List[PydSample] | Generator
equipment: List[PydEquipment] | None = [] equipment: List[PydEquipment] | None = []
cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) 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) 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') @field_validator('equipment', mode='before')
@classmethod @classmethod
def convert_equipment_dict(cls, value): def convert_equipment_dict(cls, value):
# logger.debug(f"Equipment: {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): if isinstance(value, dict):
return value['value'] return value['value']
return value return value
@@ -580,6 +597,24 @@ class PydSubmission(BaseModel, extra='allow'):
value['value'] = values.data['submission_type']['value'] value['value'] = values.data['submission_type']['value']
return 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") @field_validator("samples")
@classmethod @classmethod
def assign_ids(cls, value): def assign_ids(cls, value):

View File

@@ -93,15 +93,16 @@ class SubmissionDetails(QDialog):
Args: Args:
sample (str): Submitter Id of the sample. sample (str): Submitter Id of the sample.
""" """
logger.debug(f"Details: {sample}")
if isinstance(sample, str): if isinstance(sample, str):
sample = BasicSample.query(submitter_id=sample) sample = BasicSample.query(submitter_id=sample)
base_dict = sample.to_sub_dict(full_data=True) base_dict = sample.to_sub_dict(full_data=True)
exclude = ['submissions', 'excluded', 'colour', 'tooltip'] exclude = ['submissions', 'excluded', 'colour', 'tooltip']
try: # try:
base_dict['excluded'] += exclude # base_dict['excluded'] += exclude
except KeyError: # except KeyError:
base_dict['excluded'] = exclude base_dict['excluded'] = exclude
template = sample.get_details_template(base_dict=base_dict) template = sample.get_details_template()
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()
@@ -158,6 +159,8 @@ class SubmissionDetails(QDialog):
# logger.debug(f"Submission_details: {pformat(self.base_dict)}") # logger.debug(f"Submission_details: {pformat(self.base_dict)}")
# logger.debug(f"User is power user: {is_power_user()}") # 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) 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) self.webview.setHtml(self.html)

View File

@@ -20,7 +20,7 @@
<h3><u>Reagents:</u></h3> <h3><u>Reagents:</u></h3>
<p>{% for item in sub['reagents'] %} <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> {% endfor %}</p>
{% if sub['equipment'] %} {% if sub['equipment'] %}
@@ -38,7 +38,7 @@
{% if sub['samples'] %} {% if sub['samples'] %}
<h3><u>Samples:</u></h3> <h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %} <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> {% endfor %}</p>
{% endif %} {% endif %}
@@ -83,22 +83,25 @@
<script> <script>
{% block script %} {% block script %}
{{ super() }} {{ 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(){ document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}"); 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 %} {% endblock %}
</script> </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;"> <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 %} {% 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; border: 1px solid #000;
padding: 20px; padding: 20px;
grid-column-start: {{sample['column']}}; grid-column-start: {{sample['column']}};