Improved previous sub finding.

This commit is contained in:
Landon Wark
2024-03-06 15:04:43 -06:00
parent 8b4b39f33e
commit b466cb61d2
14 changed files with 343 additions and 117 deletions

View File

@@ -1,3 +1,7 @@
## 202402.04
- Addition of comments to gel box.
## 202402.01 ## 202402.01
- Addition of gel box for Artic quality control. - Addition of gel box for Artic quality control.

View File

@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db ; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db ; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-demo.db
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
[post_write_hooks] [post_write_hooks]

View File

@@ -0,0 +1,48 @@
"""adding source plates to Artic submission
Revision ID: fabf697c721d
Revises: 70426df72f80
Create Date: 2024-03-06 11:01:34.794411
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fabf697c721d'
down_revision = '70426df72f80'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint("ssa_unique", ['id'])
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=True)
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.drop_column('source_plates')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint("ssa_unique", type_='unique')
# ### end Alembic commands ###

View File

@@ -4,7 +4,7 @@ from pathlib import Path
# Version of the realpython-reader package # Version of the realpython-reader package
__project__ = "submissions" __project__ = "submissions"
__version__ = "202402.4b" __version__ = "202403.1b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2024, Government of Canada" __copyright__ = "2022-2024, Government of Canada"

View File

@@ -276,7 +276,7 @@ class BasicSubmission(BaseClass):
sample_list = self.hitpick_plate() sample_list = self.hitpick_plate()
# logger.debug("Setting background colours") # logger.debug("Setting background colours")
for sample in sample_list: for sample in sample_list:
if sample['positive']: if sample['Positive']:
sample['background_color'] = "#f10f07" sample['background_color'] = "#f10f07"
else: else:
if "colour" in sample.keys(): if "colour" in sample.keys():
@@ -288,7 +288,7 @@ class BasicSubmission(BaseClass):
for column in range(1, plate_columns+1): for column in range(1, plate_columns+1):
for row in range(1, plate_rows+1): for row in range(1, plate_rows+1):
try: 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: except IndexError:
well = dict(name="", row=row, column=column, background_color="#ffffff") well = dict(name="", row=row, column=column, background_color="#ffffff")
output_samples.append(well) output_samples.append(well)
@@ -429,7 +429,8 @@ class BasicSubmission(BaseClass):
case "reagents": case "reagents":
new_dict[key] = [PydReagent(**reagent) for reagent in value] new_dict[key] = [PydReagent(**reagent) for reagent in value]
case "samples": 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": case "equipment":
try: try:
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']] 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) 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_image = Column(String(64)) #: file name of gel image in zip file
gel_info = Column(JSON) #: unstructured data from gel. 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", __mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
polymorphic_load="inline", polymorphic_load="inline",
@@ -1328,12 +1330,33 @@ class WastewaterArtic(BasicSubmission):
output['gel_info'] = self.gel_info output['gel_info'] = self.gel_info
output['gel_image'] = self.gel_image output['gel_image'] = self.gel_image
output['dna_core_submission_number'] = self.dna_core_submission_number output['dna_core_submission_number'] = self.dna_core_submission_number
output['source_plates'] = self.source_plates
return output return output
@classmethod @classmethod
def get_abbreviation(cls) -> str: def get_abbreviation(cls) -> str:
return "AR" 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 @classmethod
def parse_samples(cls, input_dict: dict) -> dict: def parse_samples(cls, input_dict: dict) -> dict:
""" """
@@ -1364,8 +1387,11 @@ class WastewaterArtic(BasicSubmission):
Returns: Returns:
str: output name str: output name
""" """
# Remove letters.
processed = re.sub(r"[A-Z]", "", input_str) processed = re.sub(r"[A-Z]", "", input_str)
# Remove trailing '-' if any
processed = processed.strip("-")
try: try:
en_num = re.search(r"\-\d{1}$", processed).group() en_num = re.search(r"\-\d{1}$", processed).group()
processed = rreplace(processed, en_num, "") processed = rreplace(processed, en_num, "")
@@ -1507,18 +1533,22 @@ class WastewaterArtic(BasicSubmission):
worksheet = input_excel["First Strand List"] worksheet = input_excel["First Strand List"]
samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations
samples = sorted(samples, key=attrgetter('column', 'row')) samples = sorted(samples, key=attrgetter('column', 'row'))
source_plates = [] try:
first_samples = [] source_plates = [item['plate'] for item in info['source_plates']]
for sample in samples: first_samples = [item['start_sample'] for item in info['source_plates']]
sample = sample.sample except:
try: source_plates = []
assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1] first_samples = []
except IndexError: for sample in samples:
logger.error(f"Association not found for {sample}") sample = sample.sample
continue try:
if assoc not in source_plates: assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
source_plates.append(assoc) except IndexError:
first_samples.append(sample.ww_processing_num) 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 # Pad list to length of 3
source_plates += ['None'] * (3 - len(source_plates)) source_plates += ['None'] * (3 - len(source_plates))
first_samples += [''] * (3 - len(first_samples)) first_samples += [''] * (3 - len(first_samples))
@@ -1573,7 +1603,7 @@ class WastewaterArtic(BasicSubmission):
Tuple[dict, Template]: (Updated dictionary, Template to be rendered) Tuple[dict, Template]: (Updated dictionary, Template to be rendered)
""" """
base_dict, template = super().get_details_template(base_dict=base_dict) 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'] base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] != None check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] != None
if check: if check:
@@ -1598,20 +1628,20 @@ class WastewaterArtic(BasicSubmission):
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.")
if backup: # if backup:
output = [] output = []
for assoc in self.submission_sample_associations: for assoc in self.submission_sample_associations:
dicto = assoc.to_sub_dict() dicto = assoc.to_sub_dict()
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self) old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
try: try:
dicto['plate_name'] = old_sub.rsl_plate_num dicto['plate_name'] = old_sub.rsl_plate_num
except AttributeError: except AttributeError:
dicto['plate_name'] = "" dicto['plate_name'] = ""
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1) old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}" dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
output.append(dicto) output.append(dicto)
else: # else:
output = super().adjust_to_dict_samples(backup=False) # output = super().adjust_to_dict_samples(backup=False)
return output return output
def custom_context_events(self) -> dict: def custom_context_events(self) -> dict:
@@ -1637,9 +1667,15 @@ class WastewaterArtic(BasicSubmission):
fname = select_open_file(obj=obj, file_extension="jpg") fname = select_open_file(obj=obj, file_extension="jpg")
dlg = GelBox(parent=obj, img_path=fname) dlg = GelBox(parent=obj, img_path=fname)
if dlg.exec(): 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_image = img_path.name
self.gel_info = output 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)) logger.debug(pformat(self.gel_info))
with ZipFile(self.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf: 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 # Add a file located at the source_path to the destination within the zip
@@ -1703,7 +1739,7 @@ class BasicSample(BaseClass):
except AttributeError: except AttributeError:
return f"<Sample({self.submitter_id})" 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. gui friendly dictionary, extends parent method.
@@ -1712,8 +1748,10 @@ class BasicSample(BaseClass):
""" """
# logger.debug(f"Converting {self} to dict.") # logger.debug(f"Converting {self} to dict.")
sample = {} sample = {}
sample['submitter_id'] = self.submitter_id sample['Submitter ID'] = self.submitter_id
sample['sample_type'] = self.sample_type sample['Sample Type'] = self.sample_type
if full_data:
sample['submissions'] = [item.to_sub_dict() for item in self.sample_submission_associations]
return sample return sample
def set_attribute(self, name:str, value): def set_attribute(self, name:str, value):
@@ -1797,6 +1835,41 @@ class BasicSample(BaseClass):
""" """
return input_dict 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 @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
@@ -1896,18 +1969,18 @@ class WastewaterSample(BasicSample):
polymorphic_load="inline", polymorphic_load="inline",
inherit_condition=(id == BasicSample.id)) 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. gui friendly dictionary, extends parent method.
Returns: Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
""" """
sample = super().to_sub_dict() sample = super().to_sub_dict(full_data=full_data)
sample['ww_processing_num'] = self.ww_processing_num sample['WW Processing Number'] = self.ww_processing_num
sample['sample_location'] = self.sample_location sample['Sample Location'] = self.sample_location
sample['received_date'] = self.received_date sample['Received Date'] = self.received_date
sample['collection_date'] = self.collection_date sample['Collection Date'] = self.collection_date
return sample return sample
@classmethod @classmethod
@@ -1944,9 +2017,14 @@ class WastewaterSample(BasicSample):
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic): 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"] # 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 = self.submissions[:self.submissions.index(current_artic_submission)]
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"] try:
logger.debug(f"Submissions up to current artic submission: {subs}") 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: try:
return subs[-1] return subs[-1]
except IndexError: except IndexError:
@@ -1964,17 +2042,17 @@ class BacterialCultureSample(BasicSample):
polymorphic_load="inline", polymorphic_load="inline",
inherit_condition=(id == BasicSample.id)) 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. gui friendly dictionary, extends parent method.
Returns: Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
""" """
sample = super().to_sub_dict() sample = super().to_sub_dict(full_data=full_data)
sample['name'] = self.submitter_id sample['Name'] = self.submitter_id
sample['organism'] = self.organism sample['Organism'] = self.organism
sample['concentration'] = self.concentration sample['Concentration'] = self.concentration
if self.control != None: if self.control != None:
sample['colour'] = [0,128,0] sample['colour'] = [0,128,0]
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}" sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
@@ -2038,16 +2116,16 @@ class SubmissionSampleAssociation(BaseClass):
# Get sample info # Get sample info
# logger.debug(f"Running {self.__repr__()}") # logger.debug(f"Running {self.__repr__()}")
sample = self.sample.to_sub_dict() sample = self.sample.to_sub_dict()
sample['name'] = self.sample.submitter_id sample['Name'] = self.sample.submitter_id
sample['row'] = self.row sample['Row'] = self.row
sample['column'] = self.column sample['Column'] = self.column
try: try:
sample['well'] = f"{row_map[self.row]}{self.column}" sample['Well'] = f"{row_map[self.row]}{self.column}"
except KeyError as e: except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.") logger.error(f"Unable to find row {self.row} in row_map.")
sample['well'] = None sample['Well'] = None
sample['plate_name'] = self.submission.rsl_plate_num sample['Plate Name'] = self.submission.rsl_plate_num
sample['positive'] = False sample['Positive'] = False
return sample return sample
def to_hitpick(self) -> dict|None: def to_hitpick(self) -> dict|None:

View File

@@ -225,6 +225,7 @@ class PydEquipment(BaseModel, extra='ignore'):
@classmethod @classmethod
def make_empty_list(cls, value): def make_empty_list(cls, value):
# logger.debug(f"Pydantic value: {value}") # logger.debug(f"Pydantic value: {value}")
value = convert_nans_to_nones(value)
if value == None: if value == None:
value = [''] value = ['']
if len(value)==0: if len(value)==0:

View File

@@ -2,7 +2,8 @@
Gel box for artic quality control Gel box for artic quality control
""" """
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout, from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
QLabel, QLineEdit, QDialogButtonBox QLabel, QLineEdit, QDialogButtonBox,
QTextEdit
) )
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
@@ -44,7 +45,8 @@ class GelBox(QDialog):
# creating image view object # creating image view object
self.imv = pg.ImageView() self.imv = pg.ImageView()
img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT)) img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0])) self.imv.setImage(img, scale=None)#, xvals=np.linspace(1., 3., data.shape[0]))
layout = QGridLayout() layout = QGridLayout()
layout.addWidget(QLabel("DNA Core Submission Number"),0,1) layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
self.core_number = QLineEdit() self.core_number = QLineEdit()
@@ -59,7 +61,7 @@ class GelBox(QDialog):
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop) layout.addWidget(self.buttonBox, 23, 1, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
self.setLayout(layout) self.setLayout(layout)
def parse_form(self) -> Tuple[str, str|Path, list]: def parse_form(self) -> Tuple[str, str|Path, list]:
@@ -70,14 +72,14 @@ class GelBox(QDialog):
Tuple[str, str|Path, list]: output values Tuple[str, str|Path, list]: output values
""" """
dna_core_submission_number = self.core_number.text() 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): class ControlsForm(QWidget):
def __init__(self, parent) -> None: def __init__(self, parent) -> None:
super().__init__(parent) super().__init__(parent)
self.layout = QGridLayout() self.layout = QGridLayout()
columns = [] columns = []
rows = [] rows = []
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]): for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
@@ -98,6 +100,11 @@ class ControlsForm(QWidget):
widge.setText("Neg") widge.setText("Neg")
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}") widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1) 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) self.setLayout(self.layout)
def parse_form(self) -> List[dict]: def parse_form(self) -> List[dict]:
@@ -118,4 +125,4 @@ class ControlsForm(QWidget):
if label[0] not in [item['name'] for item in output]: if label[0] not in [item['name'] for item in output]:
output.append(dicto) output.append(dicto)
logger.debug(pformat(output)) logger.debug(pformat(output))
return output return output, self.comment_field.toPlainText()

View File

@@ -1,8 +1,10 @@
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox, from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
QDialogButtonBox, QTextEdit) QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt from PyQt6.QtWebChannel import QWebChannel
from backend.db.models import BasicSubmission from PyQt6.QtCore import Qt, pyqtSlot
from backend.db.models import BasicSubmission, BasicSample
from tools import check_if_app from tools import check_if_app
from .functions import select_save_file from .functions import select_save_file
from io import BytesIO from io import BytesIO
@@ -31,7 +33,7 @@ class SubmissionDetails(QDialog):
self.app = parent.parent().parent().parent().parent().parent().parent() self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError: except AttributeError:
self.app = None self.app = None
self.setWindowTitle("Submission Details") self.setWindowTitle(f"Submission Details - {sub.rsl_plate_num}")
# create scrollable interior # create scrollable interior
interior = QScrollArea() interior = QScrollArea()
interior.setParent(self) interior.setParent(self)
@@ -46,19 +48,34 @@ class SubmissionDetails(QDialog):
self.base_dict['platemap'] = sub.make_plate_map() self.base_dict['platemap'] = sub.make_plate_map()
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict) self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
self.html = self.template.render(sub=self.base_dict) self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView() self.webview = QWebEngineView(parent=self)
webview.setMinimumSize(900, 500) self.webview.setMinimumSize(900, 500)
webview.setMaximumSize(900, 500) self.webview.setMaximumSize(900, 500)
webview.setHtml(self.html) self.webview.setHtml(self.html)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
interior.resize(900, 500) interior.resize(900, 500)
interior.setWidget(webview) interior.setWidget(self.webview)
self.setFixedSize(900, 500) self.setFixedSize(900, 500)
# button to export a pdf version # button to export a pdf version
btn = QPushButton("Export PDF") btn = QPushButton("Export PDF")
btn.setParent(self) btn.setParent(self)
btn.setFixedWidth(900) btn.setFixedWidth(900)
btn.clicked.connect(self.export) 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): def export(self):
""" """
@@ -130,4 +147,4 @@ class SubmissionComment(QDialog):
full_comment = [{"name":commenter, "time": dt, "text": comment}] full_comment = [{"name":commenter, "time": dt, "text": comment}]
logger.debug(f"Full comment: {full_comment}") logger.debug(f"Full comment: {full_comment}")
return full_comment return full_comment

View File

@@ -103,6 +103,7 @@ class SubmissionsSheet(QTableView):
Args: Args:
event (_type_): the item of interest event (_type_): the item of interest
""" """
# logger.debug(event().__dict__)
id = self.selectionModel().currentIndex() id = self.selectionModel().currentIndex()
id = id.sibling(id.row(),0).data() id = id.sibling(id.row(),0).data()
submission = BasicSubmission.query(id=id) submission = BasicSubmission.query(id=id)

View 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'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<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>

View File

@@ -35,6 +35,7 @@
} }
</style> </style>
<title>Submission Details for {{ sub['Plate Number'] }}</title> <title>Submission Details for {{ sub['Plate Number'] }}</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
@@ -57,7 +58,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> {% 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 %}<br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['Well'] }}:</b> {% 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 %}<br>
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}
{% if sub['controls'] %} {% if sub['controls'] %}
@@ -116,4 +117,15 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </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> </html>

View File

@@ -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;"> <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" style="background-color: {{sample['background_color']}}; <div class="well" 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']}};
grid-column-end: {{sample['column']}}; grid-column-end: {{sample['Column']}};
grid-row-start: {{sample['row']}}; grid-row-start: {{sample['Row']}};
grid-row-end: {{sample['row']}}; grid-row-end: {{sample['Row']}};
display: flex; 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> <span class="tooltiptext">{{ sample['tooltip'] }}</span>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
Sample name: {{ fields['submitter_id'] }}<br> Sample name: {{ fields['Submitter ID'] }}<br>
{% if fields['organism'] %}Organism: {{ fields['organism'] }}<br>{% endif %} {% if fields['Organism'] %}Organism: {{ fields['Organism'] }}<br>{% endif %}
{% if fields['concentration'] %}Concentration: {{ fields['concentration'] }}<br>{% endif %} {% if fields['Concentration'] %}Concentration: {{ fields['Concentration'] }}<br>{% endif %}
Well: {{ fields['well'] }}<!--{{ fields['column'] }}--> Well: {{ fields['Well'] }}<!--{{ fields['column'] }}-->

View File

@@ -116,37 +116,7 @@ def check_regex_match(pattern:str, check:str) -> bool:
except TypeError: except TypeError:
return False return False
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler): # Settings
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())
class Settings(BaseSettings): class Settings(BaseSettings):
""" """
@@ -303,6 +273,26 @@ def get_config(settings_path: Path|str|None=None) -> Settings:
settings = yaml.load(stream, Loader=yaml.Loader) settings = yaml.load(stream, Loader=yaml.Loader)
return Settings(**settings) 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): class CustomFormatter(logging.Formatter):
grey = "\x1b[38;20m" grey = "\x1b[38;20m"
@@ -326,6 +316,20 @@ class CustomFormatter(logging.Formatter):
formatter = logging.Formatter(log_fmt) formatter = logging.Formatter(log_fmt)
return formatter.format(record) 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): def setup_logger(verbosity:int=3):
""" """
Set logger levels using settings. Set logger levels using settings.