Improved previous sub finding.
This commit is contained in:
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
# Version of the realpython-reader package
|
||||
__project__ = "submissions"
|
||||
__version__ = "202402.4b"
|
||||
__version__ = "202403.1b"
|
||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||
__copyright__ = "2022-2024, Government of Canada"
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ class BasicSubmission(BaseClass):
|
||||
sample_list = self.hitpick_plate()
|
||||
# logger.debug("Setting background colours")
|
||||
for sample in sample_list:
|
||||
if sample['positive']:
|
||||
if sample['Positive']:
|
||||
sample['background_color'] = "#f10f07"
|
||||
else:
|
||||
if "colour" in sample.keys():
|
||||
@@ -288,7 +288,7 @@ class BasicSubmission(BaseClass):
|
||||
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]
|
||||
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)
|
||||
@@ -429,7 +429,8 @@ class BasicSubmission(BaseClass):
|
||||
case "reagents":
|
||||
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
||||
case "samples":
|
||||
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
|
||||
# samples = {k.lower().replace(" ", "_"):v for k,v in dicto['samples'].items()}
|
||||
new_dict[key] = [PydSample(**{k.lower().replace(" ", "_"):v for k,v in sample.items()}) for sample in dicto['samples']]
|
||||
case "equipment":
|
||||
try:
|
||||
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||
@@ -1293,6 +1294,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic)
|
||||
gel_image = Column(String(64)) #: file name of gel image in zip file
|
||||
gel_info = Column(JSON) #: unstructured data from gel.
|
||||
source_plates = Column(JSON) #: wastewater plates that samples come from
|
||||
|
||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
||||
polymorphic_load="inline",
|
||||
@@ -1328,12 +1330,33 @@ class WastewaterArtic(BasicSubmission):
|
||||
output['gel_info'] = self.gel_info
|
||||
output['gel_image'] = self.gel_image
|
||||
output['dna_core_submission_number'] = self.dna_core_submission_number
|
||||
output['source_plates'] = self.source_plates
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_abbreviation(cls) -> str:
|
||||
return "AR"
|
||||
|
||||
@classmethod
|
||||
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||
"""
|
||||
Update submission dictionary with type specific information
|
||||
|
||||
Args:
|
||||
input_dict (dict): Input sample dictionary
|
||||
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
|
||||
|
||||
Returns:
|
||||
dict: Updated sample dictionary
|
||||
"""
|
||||
input_dict = super().parse_info(input_dict)
|
||||
df = xl.parse("First Strand List", header=None)
|
||||
plates = []
|
||||
for row in [8,9,10]:
|
||||
plates.append(dict(plate=df.iat[row-1, 2], start_sample=df.iat[row-1, 3]))
|
||||
input_dict['source_plates'] = plates
|
||||
return input_dict
|
||||
|
||||
@classmethod
|
||||
def parse_samples(cls, input_dict: dict) -> dict:
|
||||
"""
|
||||
@@ -1364,8 +1387,11 @@ class WastewaterArtic(BasicSubmission):
|
||||
|
||||
Returns:
|
||||
str: output name
|
||||
"""
|
||||
"""
|
||||
# Remove letters.
|
||||
processed = re.sub(r"[A-Z]", "", input_str)
|
||||
# Remove trailing '-' if any
|
||||
processed = processed.strip("-")
|
||||
try:
|
||||
en_num = re.search(r"\-\d{1}$", processed).group()
|
||||
processed = rreplace(processed, en_num, "")
|
||||
@@ -1507,18 +1533,22 @@ class WastewaterArtic(BasicSubmission):
|
||||
worksheet = input_excel["First Strand List"]
|
||||
samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations
|
||||
samples = sorted(samples, key=attrgetter('column', 'row'))
|
||||
source_plates = []
|
||||
first_samples = []
|
||||
for sample in samples:
|
||||
sample = sample.sample
|
||||
try:
|
||||
assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
||||
except IndexError:
|
||||
logger.error(f"Association not found for {sample}")
|
||||
continue
|
||||
if assoc not in source_plates:
|
||||
source_plates.append(assoc)
|
||||
first_samples.append(sample.ww_processing_num)
|
||||
try:
|
||||
source_plates = [item['plate'] for item in info['source_plates']]
|
||||
first_samples = [item['start_sample'] for item in info['source_plates']]
|
||||
except:
|
||||
source_plates = []
|
||||
first_samples = []
|
||||
for sample in samples:
|
||||
sample = sample.sample
|
||||
try:
|
||||
assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
||||
except IndexError:
|
||||
logger.error(f"Association not found for {sample}")
|
||||
continue
|
||||
if assoc not in source_plates:
|
||||
source_plates.append(assoc)
|
||||
first_samples.append(sample.ww_processing_num)
|
||||
# Pad list to length of 3
|
||||
source_plates += ['None'] * (3 - len(source_plates))
|
||||
first_samples += [''] * (3 - len(first_samples))
|
||||
@@ -1573,7 +1603,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
Tuple[dict, Template]: (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
base_dict, template = super().get_details_template(base_dict=base_dict)
|
||||
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number"]
|
||||
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number", "source_plates"]
|
||||
base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
|
||||
check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] != None
|
||||
if check:
|
||||
@@ -1598,20 +1628,20 @@ class WastewaterArtic(BasicSubmission):
|
||||
List[dict]: Updated dictionaries
|
||||
"""
|
||||
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
|
||||
if backup:
|
||||
output = []
|
||||
for assoc in self.submission_sample_associations:
|
||||
dicto = assoc.to_sub_dict()
|
||||
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
|
||||
try:
|
||||
dicto['plate_name'] = old_sub.rsl_plate_num
|
||||
except AttributeError:
|
||||
dicto['plate_name'] = ""
|
||||
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
|
||||
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
|
||||
output.append(dicto)
|
||||
else:
|
||||
output = super().adjust_to_dict_samples(backup=False)
|
||||
# if backup:
|
||||
output = []
|
||||
for assoc in self.submission_sample_associations:
|
||||
dicto = assoc.to_sub_dict()
|
||||
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
|
||||
try:
|
||||
dicto['plate_name'] = old_sub.rsl_plate_num
|
||||
except AttributeError:
|
||||
dicto['plate_name'] = ""
|
||||
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
|
||||
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
|
||||
output.append(dicto)
|
||||
# else:
|
||||
# output = super().adjust_to_dict_samples(backup=False)
|
||||
return output
|
||||
|
||||
def custom_context_events(self) -> dict:
|
||||
@@ -1637,9 +1667,15 @@ class WastewaterArtic(BasicSubmission):
|
||||
fname = select_open_file(obj=obj, file_extension="jpg")
|
||||
dlg = GelBox(parent=obj, img_path=fname)
|
||||
if dlg.exec():
|
||||
self.dna_core_submission_number, img_path, output = dlg.parse_form()
|
||||
self.dna_core_submission_number, img_path, output, comment = dlg.parse_form()
|
||||
self.gel_image = img_path.name
|
||||
self.gel_info = output
|
||||
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
|
||||
com = dict(text=comment, name=getuser(), time=dt)
|
||||
if self.comment is not None:
|
||||
self.comment.append(com)
|
||||
else:
|
||||
self.comment = [com]
|
||||
logger.debug(pformat(self.gel_info))
|
||||
with ZipFile(self.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf:
|
||||
# Add a file located at the source_path to the destination within the zip
|
||||
@@ -1703,7 +1739,7 @@ class BasicSample(BaseClass):
|
||||
except AttributeError:
|
||||
return f"<Sample({self.submitter_id})"
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
def to_sub_dict(self, full_data:bool=False) -> dict:
|
||||
"""
|
||||
gui friendly dictionary, extends parent method.
|
||||
|
||||
@@ -1712,8 +1748,10 @@ class BasicSample(BaseClass):
|
||||
"""
|
||||
# logger.debug(f"Converting {self} to dict.")
|
||||
sample = {}
|
||||
sample['submitter_id'] = self.submitter_id
|
||||
sample['sample_type'] = self.sample_type
|
||||
sample['Submitter ID'] = self.submitter_id
|
||||
sample['Sample Type'] = self.sample_type
|
||||
if full_data:
|
||||
sample['submissions'] = [item.to_sub_dict() for item in self.sample_submission_associations]
|
||||
return sample
|
||||
|
||||
def set_attribute(self, name:str, value):
|
||||
@@ -1797,6 +1835,41 @@ class BasicSample(BaseClass):
|
||||
"""
|
||||
return input_dict
|
||||
|
||||
@classmethod
|
||||
def get_details_template(cls, base_dict:dict) -> Tuple[dict, Template]:
|
||||
"""
|
||||
Get the details jinja template for the correct class
|
||||
|
||||
Args:
|
||||
base_dict (dict): incoming dictionary of Submission fields
|
||||
|
||||
Returns:
|
||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
base_dict['excluded'] = ['submissions', 'excluded']
|
||||
env = jinja_template_loading()
|
||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||
logger.debug(f"Returning template: {temp_name}")
|
||||
try:
|
||||
template = env.get_template(temp_name)
|
||||
except TemplateNotFound as e:
|
||||
logger.error(f"Couldn't find template {e}")
|
||||
template = env.get_template("basicsample_details.html")
|
||||
return base_dict, template
|
||||
|
||||
def show_details(self, obj):
|
||||
"""
|
||||
Creates Widget for showing sample details.
|
||||
|
||||
Args:
|
||||
obj (_type_): parent widget
|
||||
"""
|
||||
logger.debug("Hello from details")
|
||||
from frontend.widgets.sample_details import SampleDetails
|
||||
dlg = SampleDetails(parent=obj, samp=self)
|
||||
if dlg.exec():
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
@@ -1896,18 +1969,18 @@ class WastewaterSample(BasicSample):
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSample.id))
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
def to_sub_dict(self, full_data:bool=False) -> dict:
|
||||
"""
|
||||
gui friendly dictionary, extends parent method.
|
||||
|
||||
Returns:
|
||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||
"""
|
||||
sample = super().to_sub_dict()
|
||||
sample['ww_processing_num'] = self.ww_processing_num
|
||||
sample['sample_location'] = self.sample_location
|
||||
sample['received_date'] = self.received_date
|
||||
sample['collection_date'] = self.collection_date
|
||||
sample = super().to_sub_dict(full_data=full_data)
|
||||
sample['WW Processing Number'] = self.ww_processing_num
|
||||
sample['Sample Location'] = self.sample_location
|
||||
sample['Received Date'] = self.received_date
|
||||
sample['Collection Date'] = self.collection_date
|
||||
return sample
|
||||
|
||||
@classmethod
|
||||
@@ -1944,9 +2017,14 @@ class WastewaterSample(BasicSample):
|
||||
|
||||
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
|
||||
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
|
||||
subs = self.submissions[:self.submissions.index(current_artic_submission)]
|
||||
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
|
||||
logger.debug(f"Submissions up to current artic submission: {subs}")
|
||||
# subs = self.submissions[:self.submissions.index(current_artic_submission)]
|
||||
try:
|
||||
plates = [item['plate'] for item in current_artic_submission.source_plates]
|
||||
except TypeError as e:
|
||||
logger.error(f"source_plates must not be present")
|
||||
plates = [item.rsl_plate_num for item in self.submissions[:self.submissions.index(current_artic_submission)]]
|
||||
subs = [sub for sub in self.submissions if sub.rsl_plate_num in plates]
|
||||
logger.debug(f"Submissions: {subs}")
|
||||
try:
|
||||
return subs[-1]
|
||||
except IndexError:
|
||||
@@ -1964,17 +2042,17 @@ class BacterialCultureSample(BasicSample):
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSample.id))
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
def to_sub_dict(self, full_data:bool=False) -> dict:
|
||||
"""
|
||||
gui friendly dictionary, extends parent method.
|
||||
|
||||
Returns:
|
||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||
"""
|
||||
sample = super().to_sub_dict()
|
||||
sample['name'] = self.submitter_id
|
||||
sample['organism'] = self.organism
|
||||
sample['concentration'] = self.concentration
|
||||
sample = super().to_sub_dict(full_data=full_data)
|
||||
sample['Name'] = self.submitter_id
|
||||
sample['Organism'] = self.organism
|
||||
sample['Concentration'] = self.concentration
|
||||
if self.control != None:
|
||||
sample['colour'] = [0,128,0]
|
||||
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
||||
@@ -2038,16 +2116,16 @@ class SubmissionSampleAssociation(BaseClass):
|
||||
# Get sample info
|
||||
# logger.debug(f"Running {self.__repr__()}")
|
||||
sample = self.sample.to_sub_dict()
|
||||
sample['name'] = self.sample.submitter_id
|
||||
sample['row'] = self.row
|
||||
sample['column'] = self.column
|
||||
sample['Name'] = self.sample.submitter_id
|
||||
sample['Row'] = self.row
|
||||
sample['Column'] = self.column
|
||||
try:
|
||||
sample['well'] = f"{row_map[self.row]}{self.column}"
|
||||
sample['Well'] = f"{row_map[self.row]}{self.column}"
|
||||
except KeyError as e:
|
||||
logger.error(f"Unable to find row {self.row} in row_map.")
|
||||
sample['well'] = None
|
||||
sample['plate_name'] = self.submission.rsl_plate_num
|
||||
sample['positive'] = False
|
||||
sample['Well'] = None
|
||||
sample['Plate Name'] = self.submission.rsl_plate_num
|
||||
sample['Positive'] = False
|
||||
return sample
|
||||
|
||||
def to_hitpick(self) -> dict|None:
|
||||
|
||||
@@ -225,6 +225,7 @@ class PydEquipment(BaseModel, extra='ignore'):
|
||||
@classmethod
|
||||
def make_empty_list(cls, value):
|
||||
# logger.debug(f"Pydantic value: {value}")
|
||||
value = convert_nans_to_nones(value)
|
||||
if value == None:
|
||||
value = ['']
|
||||
if len(value)==0:
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
Gel box for artic quality control
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
|
||||
QLabel, QLineEdit, QDialogButtonBox
|
||||
QLabel, QLineEdit, QDialogButtonBox,
|
||||
QTextEdit
|
||||
)
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
@@ -44,7 +45,8 @@ class GelBox(QDialog):
|
||||
# creating image view object
|
||||
self.imv = pg.ImageView()
|
||||
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, scale=None)#, xvals=np.linspace(1., 3., data.shape[0]))
|
||||
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
|
||||
self.core_number = QLineEdit()
|
||||
@@ -59,7 +61,7 @@ class GelBox(QDialog):
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
|
||||
layout.addWidget(self.buttonBox, 23, 1, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
def parse_form(self) -> Tuple[str, str|Path, list]:
|
||||
@@ -70,14 +72,14 @@ class GelBox(QDialog):
|
||||
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()
|
||||
values, comment = self.form.parse_form()
|
||||
return dna_core_submission_number, self.img_path, values, comment
|
||||
|
||||
class ControlsForm(QWidget):
|
||||
|
||||
def __init__(self, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.layout = QGridLayout()
|
||||
|
||||
columns = []
|
||||
rows = []
|
||||
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
|
||||
@@ -98,6 +100,11 @@ class ControlsForm(QWidget):
|
||||
widge.setText("Neg")
|
||||
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
|
||||
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
|
||||
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
|
||||
self.comment_field = QTextEdit(self)
|
||||
self.comment_field.setFixedHeight(50)
|
||||
self.layout.addWidget(self.comment_field, 1,5,4,1)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self) -> List[dict]:
|
||||
@@ -118,4 +125,4 @@ class ControlsForm(QWidget):
|
||||
if label[0] not in [item['name'] for item in output]:
|
||||
output.append(dicto)
|
||||
logger.debug(pformat(output))
|
||||
return output
|
||||
return output, self.comment_field.toPlainText()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
|
||||
QDialogButtonBox, QTextEdit)
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtCore import Qt
|
||||
from backend.db.models import BasicSubmission
|
||||
from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtCore import Qt, pyqtSlot
|
||||
|
||||
from backend.db.models import BasicSubmission, BasicSample
|
||||
from tools import check_if_app
|
||||
from .functions import select_save_file
|
||||
from io import BytesIO
|
||||
@@ -31,7 +33,7 @@ class SubmissionDetails(QDialog):
|
||||
self.app = parent.parent().parent().parent().parent().parent().parent()
|
||||
except AttributeError:
|
||||
self.app = None
|
||||
self.setWindowTitle("Submission Details")
|
||||
self.setWindowTitle(f"Submission Details - {sub.rsl_plate_num}")
|
||||
# create scrollable interior
|
||||
interior = QScrollArea()
|
||||
interior.setParent(self)
|
||||
@@ -46,19 +48,34 @@ class SubmissionDetails(QDialog):
|
||||
self.base_dict['platemap'] = sub.make_plate_map()
|
||||
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
|
||||
self.html = self.template.render(sub=self.base_dict)
|
||||
webview = QWebEngineView()
|
||||
webview.setMinimumSize(900, 500)
|
||||
webview.setMaximumSize(900, 500)
|
||||
webview.setHtml(self.html)
|
||||
self.webview = QWebEngineView(parent=self)
|
||||
self.webview.setMinimumSize(900, 500)
|
||||
self.webview.setMaximumSize(900, 500)
|
||||
self.webview.setHtml(self.html)
|
||||
self.layout = QVBoxLayout()
|
||||
interior.resize(900, 500)
|
||||
interior.setWidget(webview)
|
||||
interior.setWidget(self.webview)
|
||||
self.setFixedSize(900, 500)
|
||||
# button to export a pdf version
|
||||
btn = QPushButton("Export PDF")
|
||||
btn.setParent(self)
|
||||
btn.setFixedWidth(900)
|
||||
btn.clicked.connect(self.export)
|
||||
# setup channel
|
||||
self.channel = QWebChannel()
|
||||
self.channel.registerObject('backend', self)
|
||||
self.webview.page().setWebChannel(self.channel)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sample_details(self, sample):
|
||||
# print(f"{string} is in row {row}, column {column}")
|
||||
# self.webview.setHtml(f"<html><body><br><br>{sample}</body></html>")
|
||||
sample = BasicSample.query(submitter_id=sample)
|
||||
base_dict = sample.to_sub_dict(full_data=True)
|
||||
base_dict, template = sample.get_details_template(base_dict=base_dict)
|
||||
html = template.render(sample=base_dict)
|
||||
self.webview.setHtml(html)
|
||||
# sample.show_details(obj=self)
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
@@ -130,4 +147,4 @@ class SubmissionComment(QDialog):
|
||||
full_comment = [{"name":commenter, "time": dt, "text": comment}]
|
||||
logger.debug(f"Full comment: {full_comment}")
|
||||
return full_comment
|
||||
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ class SubmissionsSheet(QTableView):
|
||||
Args:
|
||||
event (_type_): the item of interest
|
||||
"""
|
||||
# logger.debug(event().__dict__)
|
||||
id = self.selectionModel().currentIndex()
|
||||
id = id.sibling(id.row(),0).data()
|
||||
submission = BasicSubmission.query(id=id)
|
||||
|
||||
54
src/submissions/templates/basicsample_details.html
Normal file
54
src/submissions/templates/basicsample_details.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
<style>
|
||||
/* Tooltip container */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
||||
}
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: 120px;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
|
||||
/* Position the tooltip text - see examples below! */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
margin-left: -60px;
|
||||
}
|
||||
|
||||
/* Show the tooltip text when you mouse over the tooltip container */
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
font-size: large;
|
||||
}
|
||||
</style>
|
||||
<title>Sample Details for {{ sample['Submitter ID'] }}</title>
|
||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<h2><u>Sample Details for {{ sample['Submitter ID'] }}</u></h2>
|
||||
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
|
||||
<b>{{ key }}: </b>{{ value }}<br>
|
||||
{% endfor %}</p>
|
||||
{% if sample['submissions'] %}<h2>Submissions:</h2>
|
||||
{% for submission in sample['submissions'] %}
|
||||
<p>{{ submission['Plate Name'] }}: {{ submission['Well'] }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,6 +35,7 @@
|
||||
}
|
||||
</style>
|
||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
@@ -57,7 +58,7 @@
|
||||
{% if sub['samples'] %}
|
||||
<h3><u>Samples:</u></h3>
|
||||
<p>{% for item in sub['samples'] %}
|
||||
<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
||||
<b>{{ item['Well'] }}:</b> {% if item['Organism'] %} {{ item['Name'] }} - ({{ item['Organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['Name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['controls'] %}
|
||||
@@ -116,4 +117,15 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
var backend;
|
||||
new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
backend = channel.objects.backend;
|
||||
});
|
||||
{% for sample in sub['samples'] %}
|
||||
document.getElementById("{{sample['Submitter ID']}}").addEventListener("dblclick", function(){
|
||||
backend.sample_details("{{ sample['Submitter ID'] }}");
|
||||
});
|
||||
{% endfor %}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<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" style="background-color: {{sample['background_color']}};
|
||||
<div class="well" id="{{sample['Submitter ID']}}" style="background-color: {{sample['background_color']}};
|
||||
border: 1px solid #000;
|
||||
padding: 20px;
|
||||
grid-column-start: {{sample['column']}};
|
||||
grid-column-end: {{sample['column']}};
|
||||
grid-row-start: {{sample['row']}};
|
||||
grid-row-end: {{sample['row']}};
|
||||
grid-column-start: {{sample['Column']}};
|
||||
grid-column-end: {{sample['Column']}};
|
||||
grid-row-start: {{sample['Row']}};
|
||||
grid-row-end: {{sample['Row']}};
|
||||
display: flex;
|
||||
">
|
||||
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['name'] }}
|
||||
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['Name'] }}
|
||||
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Sample name: {{ fields['submitter_id'] }}<br>
|
||||
{% if fields['organism'] %}Organism: {{ fields['organism'] }}<br>{% endif %}
|
||||
{% if fields['concentration'] %}Concentration: {{ fields['concentration'] }}<br>{% endif %}
|
||||
Well: {{ fields['well'] }}<!--{{ fields['column'] }}-->
|
||||
Sample name: {{ fields['Submitter ID'] }}<br>
|
||||
{% if fields['Organism'] %}Organism: {{ fields['Organism'] }}<br>{% endif %}
|
||||
{% if fields['Concentration'] %}Concentration: {{ fields['Concentration'] }}<br>{% endif %}
|
||||
Well: {{ fields['Well'] }}<!--{{ fields['column'] }}-->
|
||||
@@ -116,37 +116,7 @@ def check_regex_match(pattern:str, check:str) -> bool:
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
||||
|
||||
def doRollover(self):
|
||||
"""
|
||||
Override base class method to make the new log file group writable.
|
||||
"""
|
||||
# Rotate the file first.
|
||||
handlers.RotatingFileHandler.doRollover(self)
|
||||
# Add group write to the current permissions.
|
||||
currMode = os.stat(self.baseFilename).st_mode
|
||||
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
|
||||
|
||||
def _open(self):
|
||||
prevumask=os.umask(0o002)
|
||||
rtv=handlers.RotatingFileHandler._open(self)
|
||||
os.umask(prevumask)
|
||||
return rtv
|
||||
|
||||
class StreamToLogger(object):
|
||||
"""
|
||||
Fake file-like stream object that redirects writes to a logger instance.
|
||||
"""
|
||||
|
||||
def __init__(self, logger, log_level=logging.INFO):
|
||||
self.logger = logger
|
||||
self.log_level = log_level
|
||||
self.linebuf = ''
|
||||
|
||||
def write(self, buf):
|
||||
for line in buf.rstrip().splitlines():
|
||||
self.logger.log(self.log_level, line.rstrip())
|
||||
# Settings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
@@ -303,6 +273,26 @@ def get_config(settings_path: Path|str|None=None) -> Settings:
|
||||
settings = yaml.load(stream, Loader=yaml.Loader)
|
||||
return Settings(**settings)
|
||||
|
||||
# Logging formatters
|
||||
|
||||
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
||||
|
||||
def doRollover(self):
|
||||
"""
|
||||
Override base class method to make the new log file group writable.
|
||||
"""
|
||||
# Rotate the file first.
|
||||
handlers.RotatingFileHandler.doRollover(self)
|
||||
# Add group write to the current permissions.
|
||||
currMode = os.stat(self.baseFilename).st_mode
|
||||
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
|
||||
|
||||
def _open(self):
|
||||
prevumask=os.umask(0o002)
|
||||
rtv=handlers.RotatingFileHandler._open(self)
|
||||
os.umask(prevumask)
|
||||
return rtv
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
|
||||
grey = "\x1b[38;20m"
|
||||
@@ -326,6 +316,20 @@ class CustomFormatter(logging.Formatter):
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
class StreamToLogger(object):
|
||||
"""
|
||||
Fake file-like stream object that redirects writes to a logger instance.
|
||||
"""
|
||||
|
||||
def __init__(self, logger, log_level=logging.INFO):
|
||||
self.logger = logger
|
||||
self.log_level = log_level
|
||||
self.linebuf = ''
|
||||
|
||||
def write(self, buf):
|
||||
for line in buf.rstrip().splitlines():
|
||||
self.logger.log(self.log_level, line.rstrip())
|
||||
|
||||
def setup_logger(verbosity:int=3):
|
||||
"""
|
||||
Set logger levels using settings.
|
||||
|
||||
Reference in New Issue
Block a user