hitpicking complete, pre-addition of WW-Arctic parsers and models.

This commit is contained in:
Landon Wark
2023-05-31 09:44:20 -05:00
parent 01d95e80f5
commit 1d6823705c
17 changed files with 334 additions and 247 deletions

View File

@@ -1,3 +1,13 @@
## 202305.05
- Hitpicking now creates source plate map image.
- Hitpick plate map is now included in exported plate results.
## 202305.04
- Added in hitpicking for plates with PCR results
- Fixed error when expiry date stored as int in excel sheet.
## 202305.03 ## 202305.03
- Added a detailed tab to the cost report. - Added a detailed tab to the cost report.

View File

@@ -62,3 +62,8 @@ This is meant to import .xslx files created from the Design & Analysis Software
## Linking PCR Logs: ## Linking PCR Logs:
1. Click "Monthly" -> "Link PCR Logs". 1. Click "Monthly" -> "Link PCR Logs".
2. Chose the .csv file taken from the PCR table runlogs folder. 2. Chose the .csv file taken from the PCR table runlogs folder.
## Hitpicking:
1. Select all submissions you wish to hitpick using "Ctrl + click". All must have PCR results.
2. Right click on the last sample and select "Hitpick" from the contex menu.
3. Select location to save csv file.

View File

@@ -1,3 +1,4 @@
- [ ] Create a method for creation of hitpicking .csvs because of reasons that may or may not exist.
- [x] Create a method for commenting submissions. - [x] Create a method for commenting submissions.
- [x] Create barcode generator, because of reasons that may or may not exist. - [x] Create barcode generator, because of reasons that may or may not exist.
- [x] Move bulk of functions from frontend.__init__ to frontend.functions as __init__ is getting bloated. - [x] Move bulk of functions from frontend.__init__ to frontend.functions as __init__ is getting bloated.

View File

@@ -0,0 +1,31 @@
"""added elution well to ww_sample
Revision ID: 64fec6271a50
Revises: a31943b2284c
Create Date: 2023-05-24 14:43:25.477637
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = '64fec6271a50'
down_revision = 'a31943b2284c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_ww_samples', schema=None) as batch_op:
batch_op.add_column(sa.Column('elution_well', sa.String(length=8), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_ww_samples', schema=None) as batch_op:
batch_op.drop_column('elution_well')
# ### 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__ = "202305.3b" __version__ = "202305.4b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2023, Government of Canada" __copyright__ = "2022-2023, Government of Canada"
@@ -24,7 +24,7 @@ class bcolors:
# Hello Landon, this is your past self here. I'm trying not to screw you over like I usually do, so I will # Hello Landon, this is your past self here. I'm trying not to screw you over like I usually do, so I will
# set out the workflow I've imagined for creating new submission types. # set out the workflow I've imagined for creating new submission types.
# First of all, you will need to write new parsing methods in backend.excel.parser to pull information out of the submission form # First of all, you will need to write new parsing methods in backend.excel.parser to pull information out of the submission form
# for the submission itself as well as for any samples you can pull out of that same sheet. # for the submission itself as well as for any samples you can pull out of that same workbook.
# Second, you will have to update the model in backend.db.models.submissions and provide a new polymorph to the BasicSubmission object. # Second, you will have to update the model in backend.db.models.submissions and provide a new polymorph to the BasicSubmission object.
# The BSO should hold the majority of the general info. # The BSO should hold the majority of the general info.
# You can also update any of the parsers to pull out any custom info you need, like enforcing RSL plate numbers, scraping PCR results, etc. # You can also update any of the parsers to pull out any custom info you need, like enforcing RSL plate numbers, scraping PCR results, etc.

View File

@@ -20,7 +20,6 @@ from getpass import getuser
import numpy as np import numpy as np
import yaml import yaml
from pathlib import Path from pathlib import Path
from math import ceil
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -136,7 +135,13 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
try: try:
field_value = lookup_kittype_by_name(ctx=ctx, name=q_str) field_value = lookup_kittype_by_name(ctx=ctx, name=q_str)
except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e: except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e:
logger.error(f"Hit an integrity error: {e}") logger.error(f"Hit an integrity error looking up kit type: {e}")
logger.error(f"Details: {e.__dict__}")
if "submitter_plate_num" in e.__dict__['statement']:
msg = "SQL integrity error. Submitter plate id is a duplicate or invalid."
else:
msg = "SQL integrity error of unknown origin."
return instance, dict(code=2, message=msg)
logger.debug(f"Got {field_value} for kit {q_str}") logger.debug(f"Got {field_value} for kit {q_str}")
case "submitting_lab": case "submitting_lab":
q_str = info_dict[item].replace(" ", "_").lower() q_str = info_dict[item].replace(" ", "_").lower()
@@ -179,7 +184,7 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
discounts = sum(discounts) discounts = sum(discounts)
instance.run_cost = instance.run_cost - discounts instance.run_cost = instance.run_cost - discounts
except Exception as e: except Exception as e:
logger.error(f"An unknown exception occurred: {e}") logger.error(f"An unknown exception occurred when calculating discounts: {e}")
# We need to make sure there's a proper rsl plate number # We need to make sure there's a proper rsl plate number
logger.debug(f"We've got a total cost of {instance.run_cost}") logger.debug(f"We've got a total cost of {instance.run_cost}")
try: try:
@@ -748,9 +753,15 @@ def update_ww_sample(ctx:dict, sample_obj:dict):
ww_samp = lookup_ww_sample_by_sub_sample_rsl(ctx=ctx, sample_rsl=sample_obj['sample'], plate_rsl=sample_obj['plate_rsl']) ww_samp = lookup_ww_sample_by_sub_sample_rsl(ctx=ctx, sample_rsl=sample_obj['sample'], plate_rsl=sample_obj['plate_rsl'])
# ww_samp = lookup_ww_sample_by_sub_sample_well(ctx=ctx, sample_rsl=sample_obj['sample'], well_num=sample_obj['well_num'], plate_rsl=sample_obj['plate_rsl']) # ww_samp = lookup_ww_sample_by_sub_sample_well(ctx=ctx, sample_rsl=sample_obj['sample'], well_num=sample_obj['well_num'], plate_rsl=sample_obj['plate_rsl'])
if ww_samp != None: if ww_samp != None:
# del sample_obj['well_number']
for key, value in sample_obj.items(): for key, value in sample_obj.items():
logger.debug(f"Setting {key} to {value}")
# set attribute 'key' to 'value' # set attribute 'key' to 'value'
try:
check = getattr(ww_samp, key)
except AttributeError:
continue
if check == None:
logger.debug(f"Setting {key} to {value}")
setattr(ww_samp, key, value) setattr(ww_samp, key, value)
else: else:
logger.error(f"Unable to find sample {sample_obj['sample']}") logger.error(f"Unable to find sample {sample_obj['sample']}")
@@ -763,3 +774,28 @@ def lookup_discounts_by_org_and_kit(ctx:dict, kit_id:int, lab_id:int):
models.KitType.id==kit_id, models.KitType.id==kit_id,
models.Organization.id==lab_id models.Organization.id==lab_id
)).all() )).all()
def hitpick_plate(submission:models.BasicSubmission, plate_number:int=0) -> list:
plate_dicto = []
for sample in submission.samples:
# have sample report back its info if it's positive, otherwise, None
samp = sample.to_hitpick()
if samp == None:
continue
else:
logger.debug(f"Item name: {samp['name']}")
# plate can handle 88 samples to leave column for controls
# if len(dicto) < 88:
this_sample = dict(
plate_number = plate_number,
sample_name = samp['name'],
column = samp['col'],
row = samp['row'],
plate_name = submission.rsl_plate_num
)
# append to plate samples
plate_dicto.append(this_sample)
# append to all samples
# image = make_plate_map(plate_dicto)
return plate_dicto

View File

@@ -4,6 +4,9 @@ All models for individual samples.
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN, JSON from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN, JSON
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
import logging
logger = logging.getLogger(f"submissions.{__name__}")
class WWSample(Base): class WWSample(Base):
@@ -19,7 +22,7 @@ class WWSample(Base):
rsl_plate = relationship("Wastewater", back_populates="samples") #: relationship to parent plate rsl_plate = relationship("Wastewater", back_populates="samples") #: relationship to parent plate
rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_submission_id")) rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_submission_id"))
collection_date = Column(TIMESTAMP) #: Date submission received collection_date = Column(TIMESTAMP) #: Date submission received
well_number = Column(String(8)) #: location on plate well_number = Column(String(8)) #: location on 24 well plate
# The following are fields from the sample tracking excel sheet Ruth put together. # The following are fields from the sample tracking excel sheet Ruth put together.
# I have no idea when they will be implemented or how. # I have no idea when they will be implemented or how.
testing_type = Column(String(64)) testing_type = Column(String(64))
@@ -33,6 +36,7 @@ class WWSample(Base):
ww_seq_run_id = Column(String(64)) ww_seq_run_id = Column(String(64))
sample_type = Column(String(8)) sample_type = Column(String(8))
pcr_results = Column(JSON) pcr_results = Column(JSON)
elution_well = Column(String(8)) #: location on 96 well plate
def to_string(self) -> str: def to_string(self) -> str:
@@ -51,6 +55,10 @@ class WWSample(Base):
Returns: Returns:
dict: well location and id NOTE: keys must sync with BCSample to_sub_dict below dict: well location and id NOTE: keys must sync with BCSample to_sub_dict below
""" """
# well_col = self.well_number[1:]
# well_row = self.well_number[0]
# if well_col > 4:
# well
if self.ct_n1 != None and self.ct_n2 != None: if self.ct_n1 != None and self.ct_n2 != None:
name = f"{self.ww_sample_full_id}\n\t- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})\n\t- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})" name = f"{self.ww_sample_full_id}\n\t- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})\n\t- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})"
else: else:
@@ -60,6 +68,34 @@ class WWSample(Base):
"name": name, "name": name,
} }
def to_hitpick(self) -> dict|None:
"""
Outputs a dictionary of locations if sample is positive
Returns:
dict: dictionary of sample id, row and column in elution plate
"""
# dictionary to translate row letters into numbers
row_dict = dict(A=1, B=2, C=3, D=4, E=5, F=6, G=7, H=8)
# if either n1 or n2 is positive, include this sample
try:
positive = any(["positive" in item for item in [self.n1_status, self.n2_status]])
except TypeError as e:
logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
return None
if positive:
try:
# The first character of the elution well is the row
well_row = row_dict[self.elution_well[0]]
# The remaining charagers are the columns
well_col = self.elution_well[1:]
except TypeError as e:
logger.error(f"This sample doesn't have elution plate info.")
return None
return dict(name=self.ww_sample_full_id, row=well_row, col=well_col)
else:
return None
class BCSample(Base): class BCSample(Base):
""" """

View File

@@ -132,7 +132,10 @@ class SheetParser(object):
try: try:
expiry = row[3].date() expiry = row[3].date()
except AttributeError as e: except AttributeError as e:
try:
expiry = datetime.strptime(row[3], "%Y-%m-%d") expiry = datetime.strptime(row[3], "%Y-%m-%d")
except TypeError as e:
expiry = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + row[3] - 2)
else: else:
logger.debug(f"Date: {row[3]}") logger.debug(f"Date: {row[3]}")
expiry = date.today() expiry = date.today()
@@ -378,7 +381,7 @@ class PCRParser(object):
self.samples_df['Assessment'] = well_call_df.values self.samples_df['Assessment'] = well_call_df.values
except ValueError: except ValueError:
logger.error("Well call number doesn't match sample number") logger.error("Well call number doesn't match sample number")
logger.debug(f"Well call dr: {well_call_df}") logger.debug(f"Well call df: {well_call_df}")
# iloc is [row][column] # iloc is [row][column]
for ii, row in self.samples_df.iterrows(): for ii, row in self.samples_df.iterrows():
try: try:
@@ -387,7 +390,7 @@ class PCRParser(object):
sample_obj = dict( sample_obj = dict(
sample = row['Sample'], sample = row['Sample'],
plate_rsl = self.plate_num, plate_rsl = self.plate_num,
well_num = row['Well Position'] elution_well = row['Well Position']
) )
logger.debug(f"Got sample obj: {sample_obj}") logger.debug(f"Got sample obj: {sample_obj}")
# logger.debug(f"row: {row}") # logger.debug(f"row: {row}")

View File

@@ -213,3 +213,7 @@ def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
return df return df
# else: # else:
# return df # return df
def make_hitpicks(input:list) -> DataFrame:
return DataFrame.from_records(input)

View File

@@ -135,6 +135,7 @@ class App(QMainWindow):
logger.debug(f"Attempting to open {url}") logger.debug(f"Attempting to open {url}")
webbrowser.get('windows-default').open(f"file://{url.__str__()}") webbrowser.get('windows-default').open(f"file://{url.__str__()}")
# All main window functions return a result which is reported here, unless it is None
def result_reporter(self, result:dict|None=None): def result_reporter(self, result:dict|None=None):
if result != None: if result != None:
msg = AlertPop(message=result['message'], status=result['status']) msg = AlertPop(message=result['message'], status=result['status'])
@@ -147,128 +148,6 @@ class App(QMainWindow):
self, result = import_submission_function(self) self, result = import_submission_function(self)
logger.debug(f"Import result: {result}") logger.debug(f"Import result: {result}")
self.result_reporter(result) self.result_reporter(result)
# logger.debug(self.ctx)
# # initialize samples
# self.samples = []
# self.reagents = {}
# # set file dialog
# home_dir = str(Path(self.ctx["directory_path"]))
# fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0])
# logger.debug(f"Attempting to parse file: {fname}")
# assert fname.exists()
# # create sheetparser using excel sheet and context from gui
# try:
# prsr = SheetParser(fname, **self.ctx)
# except PermissionError:
# logger.error(f"Couldn't get permission to access file: {fname}")
# return
# if prsr.sub['rsl_plate_num'] == None:
# prsr.sub['rsl_plate_num'] = RSLNamer(fname.__str__()).parsed_name
# logger.debug(f"prsr.sub = {prsr.sub}")
# # destroy any widgets from previous imports
# for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
# item.setParent(None)
# # regex to parser out different variable types for decision making
# variable_parser = re.compile(r"""
# # (?x)
# (?P<extraction_kit>^extraction_kit$) |
# (?P<submitted_date>^submitted_date$) |
# (?P<submitting_lab>)^submitting_lab$ |
# (?P<samples>)^samples$ |
# (?P<reagent>^lot_.*$) |
# (?P<csv>^csv$)
# """, re.VERBOSE)
# for item in prsr.sub:
# logger.debug(f"Item: {item}")
# # attempt to match variable name to regex group
# try:
# mo = variable_parser.fullmatch(item).lastgroup
# except AttributeError:
# mo = "other"
# logger.debug(f"Mo: {mo}")
# match mo:
# case 'submitting_lab':
# # create label
# self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# logger.debug(f"{item}: {prsr.sub[item]}")
# # create combobox to hold looked up submitting labs
# add_widget = QComboBox()
# labs = [item.__str__() for item in lookup_all_orgs(ctx=self.ctx)]
# # try to set closest match to top of list
# try:
# labs = difflib.get_close_matches(prsr.sub[item], labs, len(labs), 0)
# except (TypeError, ValueError):
# pass
# # set combobox values to lookedup values
# add_widget.addItems(labs)
# case 'extraction_kit':
# # create label
# self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# # if extraction kit not available, all other values fail
# if not check_not_nan(prsr.sub[item]):
# msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
# msg.exec()
# # create combobox to hold looked up kits
# # add_widget = KitSelector(ctx=self.ctx, submission_type=prsr.sub['submission_type'], parent=self)
# add_widget = QComboBox()
# # add_widget.currentTextChanged.connect(self.kit_reload)
# # lookup existing kits by 'submission_type' decided on by sheetparser
# uses = [item.__str__() for item in lookup_kittype_by_use(ctx=self.ctx, used_by=prsr.sub['submission_type'])]
# # if len(uses) > 0:
# add_widget.addItems(uses)
# # else:
# # add_widget.addItems(['bacterial_culture'])
# if check_not_nan(prsr.sub[item]):
# self.ext_kit = prsr.sub[item]
# else:
# self.ext_kit = add_widget.currentText()
# case 'submitted_date':
# # create label
# self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# # uses base calendar
# add_widget = QDateEdit(calendarPopup=True)
# # sets submitted date based on date found in excel sheet
# try:
# add_widget.setDate(prsr.sub[item])
# # if not found, use today
# except:
# add_widget.setDate(date.today())
# case 'reagent':
# # create label
# reg_label = QLabel(item.replace("_", " ").title())
# reg_label.setObjectName(f"lot_{item}_label")
# self.table_widget.formlayout.addWidget(reg_label)
# # create reagent choice widget
# add_widget = ImportReagent(ctx=self.ctx, item=item, prsr=prsr)
# self.reagents[item] = prsr.sub[item]
# case 'samples':
# # hold samples in 'self' until form submitted
# logger.debug(f"{item}: {prsr.sub[item]}")
# self.samples = prsr.sub[item]
# add_widget = None
# case 'csv':
# self.csv = prsr.sub[item]
# case _:
# # anything else gets added in as a line edit
# self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# add_widget = QLineEdit()
# logger.debug(f"Setting widget text to {str(prsr.sub[item]).replace('_', ' ')}")
# add_widget.setText(str(prsr.sub[item]).replace("_", " "))
# try:
# add_widget.setObjectName(item)
# logger.debug(f"Widget name set to: {add_widget.objectName()}")
# self.table_widget.formlayout.addWidget(add_widget)
# except AttributeError as e:
# logger.error(e)
# # compare self.reagents with expected reagents in kit
# if hasattr(self, 'ext_kit'):
# self.kit_integrity_completion()
# # create submission button
# # submit_btn = QPushButton("Submit")
# # submit_btn.setObjectName("submit_btn")
# # self.table_widget.formlayout.addWidget(submit_btn)
# # submit_btn.clicked.connect(self.submit_new_sample)
# logger.debug(f"Imported reagents: {self.reagents}")
def kit_reload(self): def kit_reload(self):
@@ -277,16 +156,6 @@ class App(QMainWindow):
""" """
self, result = kit_reload_function(self) self, result = kit_reload_function(self)
self.result_reporter(result) self.result_reporter(result)
# for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
# # item.setParent(None)
# if isinstance(item, QLabel):
# if item.text().startswith("Lot"):
# item.setParent(None)
# else:
# logger.debug(f"Type of {item.objectName()} is {type(item)}")
# if item.objectName().startswith("lot_"):
# item.setParent(None)
# self.kit_integrity_completion()
def kit_integrity_completion(self): def kit_integrity_completion(self):
@@ -297,26 +166,6 @@ class App(QMainWindow):
""" """
self, result = kit_integrity_completion_function(self) self, result = kit_integrity_completion_function(self)
self.result_reporter(result) self.result_reporter(result)
# logger.debug(inspect.currentframe().f_back.f_code.co_name)
# kit_widget = self.table_widget.formlayout.parentWidget().findChild(QComboBox, 'extraction_kit')
# logger.debug(f"Kit selector: {kit_widget}")
# self.ext_kit = kit_widget.currentText()
# logger.debug(f"Checking integrity of {self.ext_kit}")
# kit = lookup_kittype_by_name(ctx=self.ctx, name=self.ext_kit)
# reagents_to_lookup = [item.replace("lot_", "") for item in self.reagents]
# logger.debug(f"Reagents for lookup for {kit.name}: {reagents_to_lookup}")
# kit_integrity = check_kit_integrity(kit, reagents_to_lookup)
# if kit_integrity != None:
# msg = AlertPop(message=kit_integrity['message'], status="critical")
# msg.exec()
# for item in kit_integrity['missing']:
# self.table_widget.formlayout.addWidget(QLabel(f"Lot {item.replace('_', ' ').title()}"))
# add_widget = ImportReagent(ctx=self.ctx, item=item)
# self.table_widget.formlayout.addWidget(add_widget)
# submit_btn = QPushButton("Submit")
# submit_btn.setObjectName("lot_submit_btn")
# self.table_widget.formlayout.addWidget(submit_btn)
# submit_btn.clicked.connect(self.submit_new_sample)
def submit_new_sample(self): def submit_new_sample(self):

View File

@@ -3,6 +3,8 @@ Contains widgets specific to the submission summary and submission details.
''' '''
import base64 import base64
from datetime import datetime from datetime import datetime
from io import BytesIO
import math
from PyQt6 import QtPrintSupport from PyQt6 import QtPrintSupport
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView, QVBoxLayout, QDialog, QTableView,
@@ -10,16 +12,18 @@ from PyQt6.QtWidgets import (
QMessageBox, QFileDialog, QMenu, QLabel, QMessageBox, QFileDialog, QMenu, QLabel,
QDialogButtonBox, QToolBar, QMainWindow QDialogButtonBox, QToolBar, QMainWindow
) )
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, QItemSelectionModel
from PyQt6.QtGui import QFontMetrics, QAction, QCursor, QPixmap, QPainter from PyQt6.QtGui import QFontMetrics, QAction, QCursor, QPixmap, QPainter
from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id, lookup_submission_by_rsl_num from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id, lookup_submission_by_rsl_num, hitpick_plate
# from backend.misc import hitpick_plate
from backend.excel import make_hitpicks
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa from xhtml2pdf import pisa
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
from .pop_ups import QuestionAsker from .pop_ups import QuestionAsker, AlertPop
from ..visualizations import make_plate_barcode from ..visualizations import make_plate_barcode, make_plate_map
from getpass import getuser from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -92,6 +96,7 @@ class SubmissionsSheet(QTableView):
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
def setData(self) -> None: def setData(self) -> None:
@@ -111,10 +116,8 @@ class SubmissionsSheet(QTableView):
pass pass
proxyModel = QSortFilterProxyModel() proxyModel = QSortFilterProxyModel()
proxyModel.setSourceModel(pandasModel(self.data)) proxyModel.setSourceModel(pandasModel(self.data))
# self.model = pandasModel(self.data)
# self.setModel(self.model)
self.setModel(proxyModel) self.setModel(proxyModel)
# self.resize(800,600)
def show_details(self) -> None: def show_details(self) -> None:
""" """
@@ -155,14 +158,17 @@ class SubmissionsSheet(QTableView):
detailsAction = QAction('Details', self) detailsAction = QAction('Details', self)
barcodeAction = QAction("Print Barcode", self) barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", self) commentAction = QAction("Add Comment", self)
hitpickAction = QAction("Hitpicks", self)
renameAction.triggered.connect(lambda: self.delete_item(event)) renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details()) detailsAction.triggered.connect(lambda: self.show_details())
barcodeAction.triggered.connect(lambda: self.create_barcode()) barcodeAction.triggered.connect(lambda: self.create_barcode())
commentAction.triggered.connect(lambda: self.add_comment()) commentAction.triggered.connect(lambda: self.add_comment())
hitpickAction.triggered.connect(lambda: self.hit_pick())
self.menu.addAction(detailsAction) self.menu.addAction(detailsAction)
self.menu.addAction(renameAction) self.menu.addAction(renameAction)
self.menu.addAction(barcodeAction) self.menu.addAction(barcodeAction)
self.menu.addAction(commentAction) self.menu.addAction(commentAction)
self.menu.addAction(hitpickAction)
# add other required actions # add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
@@ -185,6 +191,62 @@ class SubmissionsSheet(QTableView):
self.setData() self.setData()
def hit_pick(self):
"""
Extract positive samples from submissions with PCR results and export to csv.
NOTE: For this to work for arbitrary samples, positive samples must have 'positive' in their name
"""
# Get all selected rows
indices = self.selectionModel().selectedIndexes()
# convert to id numbers
indices = [index.sibling(index.row(), 0).data() for index in indices]
# biomek can handle 4 plates maximum
if len(indices) > 4:
logger.error(f"Error: Had to truncate number of plates to 4.")
indices = indices[:4]
# lookup ids in the database
subs = [lookup_submission_by_id(self.ctx, id) for id in indices]
# full list of samples
dicto = []
# list to contain plate images
images = []
for iii, sub in enumerate(subs):
# second check to make sure there aren't too many plates
if iii > 3:
logger.error(f"Error: Had to truncate number of plates to 4.")
continue
plate_dicto = hitpick_plate(submission=sub, plate_number=iii+1)
if plate_dicto == None:
continue
image = make_plate_map(plate_dicto)
images.append(image)
for item in plate_dicto:
if len(dicto) < 94:
dicto.append(item)
else:
logger.error(f"We had to truncate the number of samples to 94.")
logger.debug(f"We found {len(dicto)} to hitpick")
msg = AlertPop(message=f"We found {len(dicto)} samples to hitpick", status="INFORMATION")
msg.exec()
# convert all samples to dataframe
df = make_hitpicks(dicto)
logger.debug(f"Size of the dataframe: {df.size}")
if df.size == 0:
return
date = datetime.strftime(datetime.today(), "%Y-%m-%d")
# ask for filename and save as csv.
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Hitpicks_{date}.csv").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".csv")[0])
if fname.__str__() == ".":
logger.debug("Saving csv was cancelled.")
return
df.to_csv(fname.__str__(), index=False)
# show plate maps
for image in images:
try:
image.show()
except Exception as e:
logger.error(f"Could not show image: {e}.")
class SubmissionDetails(QDialog): class SubmissionDetails(QDialog):
@@ -239,7 +301,17 @@ class SubmissionDetails(QDialog):
Renders submission to html, then creates and saves .pdf file to user selected file. Renders submission to html, then creates and saves .pdf file to user selected file.
""" """
template = env.get_template("submission_details.html") template = env.get_template("submission_details.html")
# make barcode because, reasons
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8') self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
sub = lookup_submission_by_rsl_num(ctx=self.ctx, rsl_num=self.base_dict['Plate Number'])
plate_dicto = hitpick_plate(sub)
platemap = make_plate_map(plate_dicto)
logger.debug(f"platemap: {platemap}")
image_io = BytesIO()
platemap.save(image_io, 'JPEG')
platemap.save("test.jpg", 'JPEG')
self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
logger.debug(self.base_dict)
html = template.render(sub=self.base_dict) html = template.render(sub=self.base_dict)
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__() home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0]) fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
@@ -269,18 +341,10 @@ class BarcodeWindow(QDialog):
# creating label # creating label
self.label = QLabel() self.label = QLabel()
self.img = make_plate_barcode(rsl_num) self.img = make_plate_barcode(rsl_num)
# logger.debug(dir(img), img.contents[0])
# fp = BytesIO().read()
# img.save(formats=['png'], fnRoot=fp)
# pixmap = QPixmap("C:\\Users\\lwark\\Documents\\python\\submissions\\src\\Drawing000.png")
self.pixmap = QPixmap() self.pixmap = QPixmap()
# self.pixmap.loadFromData(self.img.asString("bmp"))
self.pixmap.loadFromData(self.img) self.pixmap.loadFromData(self.img)
# adding image to label # adding image to label
self.label.setPixmap(self.pixmap) self.label.setPixmap(self.pixmap)
# Optional, resize label to image size
# self.label.resize(self.pixmap.width(), self.pixmap.height())
# self.label.resize(200, 200)
# show all the widgets] # show all the widgets]
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
@@ -300,8 +364,6 @@ class BarcodeWindow(QDialog):
adds items to menu bar adds items to menu bar
""" """
toolbar = QToolBar("My main toolbar") toolbar = QToolBar("My main toolbar")
# self.addToolBar(toolbar)
# self.layout.setToolBar(toolbar)
toolbar.addAction(self.printAction) toolbar.addAction(self.printAction)
@@ -321,7 +383,6 @@ class BarcodeWindow(QDialog):
def print_barcode(self): def print_barcode(self):
printer = QtPrintSupport.QPrinter() printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer) dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec(): if dialog.exec():
self.handle_paint_request(printer, self.pixmap.toImage()) self.handle_paint_request(printer, self.pixmap.toImage())

View File

@@ -41,15 +41,11 @@ logger = logging.getLogger(f"submissions.{__name__}")
def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]: def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]:
result = None result = None
# from .custom_widgets.misc import ImportReagent
# from .custom_widgets.pop_ups import AlertPop
logger.debug(obj.ctx) logger.debug(obj.ctx)
# initialize samples # initialize samples
obj.samples = [] obj.samples = []
obj.reagents = {} obj.reagents = {}
# set file dialog # set file dialog
# home_dir = str(Path(obj.ctx["directory_path"]))
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir)[0])
fname = select_open_file(obj, extension="xlsx") fname = select_open_file(obj, extension="xlsx")
logger.debug(f"Attempting to parse file: {fname}") logger.debug(f"Attempting to parse file: {fname}")
if not fname.exists(): if not fname.exists():
@@ -69,7 +65,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
item.setParent(None) item.setParent(None)
# regex to parser out different variable types for decision making # regex to parser out different variable types for decision making
variable_parser = re.compile(r""" variable_parser = re.compile(r"""
# (?x)
(?P<extraction_kit>^extraction_kit$) | (?P<extraction_kit>^extraction_kit$) |
(?P<submitted_date>^submitted_date$) | (?P<submitted_date>^submitted_date$) |
(?P<submitting_lab>)^submitting_lab$ | (?P<submitting_lab>)^submitting_lab$ |
@@ -161,13 +156,11 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
if hasattr(obj, 'ext_kit'): if hasattr(obj, 'ext_kit'):
obj.kit_integrity_completion() obj.kit_integrity_completion()
logger.debug(f"Imported reagents: {obj.reagents}") logger.debug(f"Imported reagents: {obj.reagents}")
return obj, result return obj, result
def kit_reload_function(obj:QMainWindow) -> QMainWindow: def kit_reload_function(obj:QMainWindow) -> QMainWindow:
result = None result = None
for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget): for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
# item.setParent(None)
if isinstance(item, QLabel): if isinstance(item, QLabel):
if item.text().startswith("Lot"): if item.text().startswith("Lot"):
item.setParent(None) item.setParent(None)
@@ -180,20 +173,22 @@ def kit_reload_function(obj:QMainWindow) -> QMainWindow:
def kit_integrity_completion_function(obj:QMainWindow) -> QMainWindow: def kit_integrity_completion_function(obj:QMainWindow) -> QMainWindow:
result = None result = None
# from .custom_widgets.misc import ImportReagent
# from .custom_widgets.pop_ups import AlertPop
logger.debug(inspect.currentframe().f_back.f_code.co_name) logger.debug(inspect.currentframe().f_back.f_code.co_name)
# find the widget that contains lit info
kit_widget = obj.table_widget.formlayout.parentWidget().findChild(QComboBox, 'extraction_kit') kit_widget = obj.table_widget.formlayout.parentWidget().findChild(QComboBox, 'extraction_kit')
logger.debug(f"Kit selector: {kit_widget}") logger.debug(f"Kit selector: {kit_widget}")
# get current kit info
obj.ext_kit = kit_widget.currentText() obj.ext_kit = kit_widget.currentText()
logger.debug(f"Checking integrity of {obj.ext_kit}") logger.debug(f"Checking integrity of {obj.ext_kit}")
# get the kit from database using current kit info
kit = lookup_kittype_by_name(ctx=obj.ctx, name=obj.ext_kit) kit = lookup_kittype_by_name(ctx=obj.ctx, name=obj.ext_kit)
# get all reagents stored in the QWindow object
reagents_to_lookup = [item.replace("lot_", "") for item in obj.reagents] reagents_to_lookup = [item.replace("lot_", "") for item in obj.reagents]
logger.debug(f"Reagents for lookup for {kit.name}: {reagents_to_lookup}") logger.debug(f"Reagents for lookup for {kit.name}: {reagents_to_lookup}")
# make sure kit contains all necessary info
kit_integrity = check_kit_integrity(kit, reagents_to_lookup) kit_integrity = check_kit_integrity(kit, reagents_to_lookup)
# if kit integrity comes back with an error, make widgets with missing reagents using default info
if kit_integrity != None: if kit_integrity != None:
# msg = AlertPop(message=kit_integrity['message'], status="critical")
# msg.exec()
result = dict(message=kit_integrity['message'], status="Warning") result = dict(message=kit_integrity['message'], status="Warning")
for item in kit_integrity['missing']: for item in kit_integrity['missing']:
obj.table_widget.formlayout.addWidget(QLabel(f"Lot {item.replace('_', ' ').title()}")) obj.table_widget.formlayout.addWidget(QLabel(f"Lot {item.replace('_', ' ').title()}"))
@@ -207,9 +202,9 @@ def kit_integrity_completion_function(obj:QMainWindow) -> QMainWindow:
def submit_new_sample_function(obj:QMainWindow) -> QMainWindow: def submit_new_sample_function(obj:QMainWindow) -> QMainWindow:
result = None result = None
# from .custom_widgets.misc import ImportReagent # extract info from the form widgets
# from .custom_widgets.pop_ups import AlertPop, QuestionAsker
info = extract_form_info(obj.table_widget.tab1) info = extract_form_info(obj.table_widget.tab1)
# seperate out reagents
reagents = {k:v for k,v in info.items() if k.startswith("lot_")} reagents = {k:v for k,v in info.items() if k.startswith("lot_")}
info = {k:v for k,v in info.items() if not k.startswith("lot_")} info = {k:v for k,v in info.items() if not k.startswith("lot_")}
logger.debug(f"Info: {info}") logger.debug(f"Info: {info}")
@@ -268,12 +263,9 @@ def submit_new_sample_function(obj:QMainWindow) -> QMainWindow:
# reset form # reset form
for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget): for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
item.setParent(None) item.setParent(None)
# print(dir(obj))
if hasattr(obj, 'csv'): if hasattr(obj, 'csv'):
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?") dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
if dlg.exec(): if dlg.exec():
# home_dir = Path(obj.ctx["directory_path"]).joinpath(f"{base_submission.rsl_plate_num}.csv").resolve().__str__()
# fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter=".csv")[0])
fname = select_save_file(obj, f"{base_submission.rsl_plate_num}.csv", extension="csv") fname = select_save_file(obj, f"{base_submission.rsl_plate_num}.csv", extension="csv")
try: try:
obj.csv.to_csv(fname.__str__(), index=False) obj.csv.to_csv(fname.__str__(), index=False)
@@ -282,8 +274,8 @@ def submit_new_sample_function(obj:QMainWindow) -> QMainWindow:
return obj, result return obj, result
def generate_report_function(obj:QMainWindow) -> QMainWindow: def generate_report_function(obj:QMainWindow) -> QMainWindow:
# from .custom_widgets import ReportDatePicker
result = None result = None
# ask for date ranges
dlg = ReportDatePicker() dlg = ReportDatePicker()
if dlg.exec(): if dlg.exec():
info = extract_form_info(dlg) info = extract_form_info(dlg)
@@ -324,8 +316,6 @@ def generate_report_function(obj:QMainWindow) -> QMainWindow:
def add_kit_function(obj:QMainWindow) -> QMainWindow: def add_kit_function(obj:QMainWindow) -> QMainWindow:
result = None result = None
# setup file dialog to find yaml flie # setup file dialog to find yaml flie
# home_dir = str(Path(obj.ctx["directory_path"]))
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = "yml(*.yml)")[0])
fname = select_open_file(obj, extension="yml") fname = select_open_file(obj, extension="yml")
assert fname.exists() assert fname.exists()
# read yaml file # read yaml file
@@ -340,19 +330,11 @@ def add_kit_function(obj:QMainWindow) -> QMainWindow:
return return
# send to kit creator function # send to kit creator function
result = create_kit_from_yaml(ctx=obj.ctx, exp=exp) result = create_kit_from_yaml(ctx=obj.ctx, exp=exp)
# match result['code']:
# case 0:
# msg = AlertPop(message=result['message'], status='info')
# case 1:
# msg = AlertPop(message=result['message'], status='critical')
# msg.exec()
return obj, result return obj, result
def add_org_function(obj:QMainWindow) -> QMainWindow: def add_org_function(obj:QMainWindow) -> QMainWindow:
result = None result = None
# setup file dialog to find yaml flie # setup file dialog to find yaml flie
# home_dir = str(Path(obj.ctx["directory_path"]))
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = "yml(*.yml)")[0])
fname = select_open_file(obj, extension="yml") fname = select_open_file(obj, extension="yml")
assert fname.exists() assert fname.exists()
# read yaml file # read yaml file
@@ -367,12 +349,6 @@ def add_org_function(obj:QMainWindow) -> QMainWindow:
return obj, result return obj, result
# send to kit creator function # send to kit creator function
result = create_org_from_yaml(ctx=obj.ctx, org=org) result = create_org_from_yaml(ctx=obj.ctx, org=org)
# match result['code']:
# case 0:
# msg = AlertPop(message=result['message'], status='information')
# case 1:
# msg = AlertPop(message=result['message'], status='critical')
# msg.exec()
return obj, result return obj, result
def controls_getter_function(obj:QMainWindow) -> QMainWindow: def controls_getter_function(obj:QMainWindow) -> QMainWindow:
@@ -412,8 +388,18 @@ def controls_getter_function(obj:QMainWindow) -> QMainWindow:
return obj, result return obj, result
def chart_maker_function(obj:QMainWindow) -> QMainWindow: def chart_maker_function(obj:QMainWindow) -> QMainWindow:
"""
create html chart for controls reporting
Args:
obj (QMainWindow): original MainWindow
Returns:
QMainWindow: MainWindow with control display updates
"""
result = None result = None
logger.debug(f"Control getter context: \n\tControl type: {obj.con_type}\n\tMode: {obj.mode}\n\tStart Date: {obj.start_date}\n\tEnd Date: {obj.end_date}") logger.debug(f"Control getter context: \n\tControl type: {obj.con_type}\n\tMode: {obj.mode}\n\tStart Date: {obj.start_date}\n\tEnd Date: {obj.end_date}")
# set the subtype for kraken
if obj.table_widget.sub_typer.currentText() == "": if obj.table_widget.sub_typer.currentText() == "":
obj.subtype = None obj.subtype = None
else: else:
@@ -425,7 +411,7 @@ def chart_maker_function(obj:QMainWindow) -> QMainWindow:
if controls == None: if controls == None:
fig = None fig = None
else: else:
# change each control to list of dicts # change each control to list of dictionaries
data = [control.convert_by_mode(mode=obj.mode) for control in controls] data = [control.convert_by_mode(mode=obj.mode) for control in controls]
# flatten data to one dimensional list # flatten data to one dimensional list
data = [item for sublist in data for item in sublist] data = [item for sublist in data for item in sublist]
@@ -646,7 +632,8 @@ def import_pcr_results_function(obj:QMainWindow) -> QMainWindow:
logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}") logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}")
logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}") logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}")
obj.ctx["database_session"].commit() obj.ctx["database_session"].commit()
logger.debug(f"Got {len(parser.samples)} to update!") logger.debug(f"Got {len(parser.samples)} samples to update!")
logger.debug(f"Parser samples: {parser.samples}")
for sample in parser.samples: for sample in parser.samples:
logger.debug(f"Running update on: {sample['sample']}") logger.debug(f"Running update on: {sample['sample']}")
sample['plate_rsl'] = sub.rsl_plate_num sample['plate_rsl'] = sub.rsl_plate_num
@@ -655,11 +642,3 @@ def import_pcr_results_function(obj:QMainWindow) -> QMainWindow:
return obj, result return obj, result
# dlg.exec() # dlg.exec()

View File

@@ -3,3 +3,4 @@ Contains all operations for creating charts, graphs and visual effects.
''' '''
from .control_charts import * from .control_charts import *
from .barcode import * from .barcode import *
from .plate_map import *

View File

@@ -0,0 +1,80 @@
from pathlib import Path
import sys
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from tools import check_if_app
import logging
logger = logging.getLogger(f"submissions.{__name__}")
def make_plate_map(sample_list:list) -> Image:
"""
Makes a pillow image of a plate from hitpicks
Args:
sample_list (list): list of positive sample dictionaries from the hitpicks
Returns:
Image: Image of the 96 well plate with positive samples in red.
"""
# If we can't get a plate number, do nothing
try:
plate_num = sample_list[0]['plate_name']
except IndexError as e:
logger.error(f"Couldn't get a plate number. Will not make plate.")
return None
except TypeError as e:
logger.error(f"No samples for this plate. Nothing to do.")
return None
# Make a 8 row, 12 column, 3 color ints array, filled with white by default
grid = np.full((8,12,3),255, dtype=np.uint8)
# Go through samples and change its row/column to red
for sample in sample_list:
grid[int(sample['row'])-1][int(sample['column'])-1] = [255,0,0]
# Create image from the grid
img = Image.fromarray(grid).resize((1200, 800), resample=Image.NEAREST)
# create a drawer over the image
draw = ImageDraw.Draw(img)
# draw grid over the image
y_start = 0
y_end = img.height
step_size = int(img.width / 12)
for x in range(0, img.width, step_size):
line = ((x, y_start), (x, y_end))
draw.line(line, fill=128)
x_start = 0
x_end = img.width
step_size = int(img.height / 8)
for y in range(0, img.height, step_size):
line = ((x_start, y), (x_end, y))
draw.line(line, fill=128)
del draw
old_size = img.size
new_size = (1300, 900)
# create a new, larger white image to hold the annotations
new_img = Image.new("RGB", new_size, "White")
box = tuple((n - o) // 2 for n, o in zip(new_size, old_size))
# paste plate map into the new image
new_img.paste(img, box)
# create drawer over the new image
draw = ImageDraw.Draw(new_img)
# font = ImageFont.truetype("sans-serif.ttf", 16)
if check_if_app():
font_path = Path(sys._MEIPASS).joinpath("files", "resources")
else:
font_path = Path(__file__).parents[2].joinpath('resources').absolute()
logger.debug(f"Font path: {font_path}")
font = ImageFont.truetype(font_path.joinpath('arial.ttf').__str__(), 32)
row_dict = ["A", "B", "C", "D", "E", "F", "G", "H"]
# write the plate number on the image
draw.text((100, 850),plate_num,(0,0,0),font=font)
# write column numbers
for num in range(1,13):
x = (num * 100) - 10
draw.text((x, 0), str(num), (0,0,0),font=font)
# write row letters
for num in range(1,9):
letter = row_dict[num-1]
y = (num * 100) - 10
draw.text((10, y), letter, (0,0,0),font=font)
return new_img

Binary file not shown.

View File

@@ -3,7 +3,7 @@
<head> <head>
<title>Submission Details for {{ sub['Plate Number'] }}</title> <title>Submission Details for {{ sub['Plate Number'] }}</title>
</head> </head>
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments', 'barcode'] %} {% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments', 'barcode', 'platemap'] %}
<body> <body>
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>&nbsp;&nbsp;&nbsp;<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}"> <h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>&nbsp;&nbsp;&nbsp;<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">
<p>{% for key, value in sub.items() if key not in excluded %} <p>{% for key, value in sub.items() if key not in excluded %}
@@ -93,5 +93,9 @@
{% endif %} {% endif %}
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}
{% if sub['platemap'] %}
<h3><u>>Plate map:</u></h3>
<img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['platemap'] | safe }}">
{% endif %}
</body> </body>
</html> </html>

View File

@@ -92,14 +92,12 @@ def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None
check = set(ext_kit_rtypes) == set(reagenttypes) check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}") logger.debug(f"Checking if reagents match kit contents: {check}")
# what reagent types are in both lists? # what reagent types are in both lists?
# common = list(set(ext_kit_rtypes).intersection(reagenttypes))
missing = list(set(ext_kit_rtypes).difference(reagenttypes)) missing = list(set(ext_kit_rtypes).difference(reagenttypes))
logger.debug(f"Missing reagents types: {missing}") logger.debug(f"Missing reagents types: {missing}")
# if lists are equal return no problem # if lists are equal return no problem
if len(missing)==0: if len(missing)==0:
result = None result = None
else: else:
# missing = [x for x in ext_kit_rtypes if x not in common]
result = {'message' : f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", 'missing': missing} result = {'message' : f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", 'missing': missing}
return result return result
@@ -168,7 +166,6 @@ class RSLNamer(object):
Object that will enforce proper formatting on RSL plate names. Object that will enforce proper formatting on RSL plate names.
""" """
def __init__(self, instr:str): def __init__(self, instr:str):
# self.parsed_name, self.submission_type = self.retrieve_rsl_number(instr)
self.retrieve_rsl_number(in_str=instr) self.retrieve_rsl_number(in_str=instr)
if self.submission_type != None: if self.submission_type != None:
parser = getattr(self, f"enforce_{self.submission_type}") parser = getattr(self, f"enforce_{self.submission_type}")
@@ -195,12 +192,13 @@ class RSLNamer(object):
return return
logger.debug(f"Attempting match of {in_str}") logger.debug(f"Attempting match of {in_str}")
regex = re.compile(r""" regex = re.compile(r"""
(?P<wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(?:_|-)R?\d(?!\d))?)| (?P<wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(?:_|-)\d?(?!\d)R?\d(?!\d))?)|
(?P<bacterial_culture>RSL-?\d{2}-?\d{4}) (?P<bacterial_culture>RSL-?\d{2}-?\d{4})
""", flags = re.IGNORECASE | re.VERBOSE) """, flags = re.IGNORECASE | re.VERBOSE)
m = regex.search(in_str) m = regex.search(in_str)
try: try:
self.parsed_name = m.group().upper() self.parsed_name = m.group().upper()
logger.debug(f"Got parsed submission name: {self.parsed_name}")
self.submission_type = m.lastgroup self.submission_type = m.lastgroup
except AttributeError as e: except AttributeError as e:
logger.critical("No RSL plate number found or submission type found!") logger.critical("No RSL plate number found or submission type found!")
@@ -210,11 +208,8 @@ class RSLNamer(object):
""" """
Uses regex to enforce proper formatting of wastewater samples Uses regex to enforce proper formatting of wastewater samples
""" """
# self.parsed_name = re.sub(r"(\d)-(\d)", "\1\2", self.parsed_name)
# year = str(date.today().year)[:2]
self.parsed_name = re.sub(r"PCR(-|_)", "", self.parsed_name) self.parsed_name = re.sub(r"PCR(-|_)", "", self.parsed_name)
self.parsed_name = self.parsed_name.replace("RSLWW", "RSL-WW") self.parsed_name = self.parsed_name.replace("RSLWW", "RSL-WW")
# .replace(f"WW{year}", f"WW-{year}")
self.parsed_name = re.sub(r"WW(\d{4})", r"WW-\1", self.parsed_name, flags=re.IGNORECASE) self.parsed_name = re.sub(r"WW(\d{4})", r"WW-\1", self.parsed_name, flags=re.IGNORECASE)
self.parsed_name = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", self.parsed_name) self.parsed_name = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", self.parsed_name)
@@ -222,14 +217,6 @@ class RSLNamer(object):
""" """
Uses regex to enforce proper formatting of bacterial culture samples Uses regex to enforce proper formatting of bacterial culture samples
""" """
# year = str(date.today().year)[2:]
# self.parsed_name = self.parsed_name.replace(f"RSL{year}", f"RSL-{year}")
# reg_year = re.compile(fr"{year}(?P<rsl>\d\d\d\d)")
self.parsed_name = re.sub(r"RSL(\d{2})", r"RSL-\1", self.parsed_name, flags=re.IGNORECASE) self.parsed_name = re.sub(r"RSL(\d{2})", r"RSL-\1", self.parsed_name, flags=re.IGNORECASE)
self.parsed_name = re.sub(r"RSL-(\d{2})(\d{4})", r"RSL-\1-\2", self.parsed_name, flags=re.IGNORECASE) self.parsed_name = re.sub(r"RSL-(\d{2})(\d{4})", r"RSL-\1-\2", self.parsed_name, flags=re.IGNORECASE)
# year = regex.group('year')
# rsl = regex.group('rsl')
# self.parsed_name = re.sub(fr"{year}(\d\d\d\d)", fr"{year}-\1", self.parsed_name)
# plate_search = reg_year.search(self.parsed_name)
# if plate_search != None:
# self.parsed_name = re.sub(reg_year, f"{year}-{plate_search.group('rsl')}", self.parsed_name)