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

@@ -135,6 +135,7 @@ class App(QMainWindow):
logger.debug(f"Attempting to open {url}")
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):
if result != None:
msg = AlertPop(message=result['message'], status=result['status'])
@@ -147,128 +148,6 @@ class App(QMainWindow):
self, result = import_submission_function(self)
logger.debug(f"Import result: {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):
@@ -277,17 +156,7 @@ class App(QMainWindow):
"""
self, result = kit_reload_function(self)
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):
"""
@@ -297,26 +166,6 @@ class App(QMainWindow):
"""
self, result = kit_integrity_completion_function(self)
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):

View File

@@ -3,6 +3,8 @@ Contains widgets specific to the submission summary and submission details.
'''
import base64
from datetime import datetime
from io import BytesIO
import math
from PyQt6 import QtPrintSupport
from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView,
@@ -10,16 +12,18 @@ from PyQt6.QtWidgets import (
QMessageBox, QFileDialog, QMenu, QLabel,
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 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 xhtml2pdf import pisa
import sys
from pathlib import Path
import logging
from .pop_ups import QuestionAsker
from ..visualizations import make_plate_barcode
from .pop_ups import QuestionAsker, AlertPop
from ..visualizations import make_plate_barcode, make_plate_map
from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}")
@@ -92,6 +96,7 @@ class SubmissionsSheet(QTableView):
self.resizeColumnsToContents()
self.resizeRowsToContents()
self.setSortingEnabled(True)
self.doubleClicked.connect(self.show_details)
def setData(self) -> None:
@@ -111,10 +116,8 @@ class SubmissionsSheet(QTableView):
pass
proxyModel = QSortFilterProxyModel()
proxyModel.setSourceModel(pandasModel(self.data))
# self.model = pandasModel(self.data)
# self.setModel(self.model)
self.setModel(proxyModel)
# self.resize(800,600)
def show_details(self) -> None:
"""
@@ -124,7 +127,7 @@ class SubmissionsSheet(QTableView):
value = index.sibling(index.row(),0).data()
dlg = SubmissionDetails(ctx=self.ctx, id=value)
if dlg.exec():
pass
pass
def create_barcode(self) -> None:
index = (self.selectionModel().currentIndex())
@@ -155,14 +158,17 @@ class SubmissionsSheet(QTableView):
detailsAction = QAction('Details', self)
barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", self)
hitpickAction = QAction("Hitpicks", self)
renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details())
barcodeAction.triggered.connect(lambda: self.create_barcode())
commentAction.triggered.connect(lambda: self.add_comment())
hitpickAction.triggered.connect(lambda: self.hit_pick())
self.menu.addAction(detailsAction)
self.menu.addAction(renameAction)
self.menu.addAction(barcodeAction)
self.menu.addAction(commentAction)
self.menu.addAction(hitpickAction)
# add other required actions
self.menu.popup(QCursor.pos())
@@ -185,7 +191,63 @@ class SubmissionsSheet(QTableView):
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):
"""
@@ -239,7 +301,17 @@ class SubmissionDetails(QDialog):
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
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')
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)
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])
@@ -269,18 +341,10 @@ class BarcodeWindow(QDialog):
# creating label
self.label = QLabel()
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.loadFromData(self.img.asString("bmp"))
self.pixmap.loadFromData(self.img)
# adding image to label
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]
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
@@ -300,8 +364,6 @@ class BarcodeWindow(QDialog):
adds items to menu bar
"""
toolbar = QToolBar("My main toolbar")
# self.addToolBar(toolbar)
# self.layout.setToolBar(toolbar)
toolbar.addAction(self.printAction)
@@ -321,7 +383,6 @@ class BarcodeWindow(QDialog):
def print_barcode(self):
printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec():
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]:
result = None
# from .custom_widgets.misc import ImportReagent
# from .custom_widgets.pop_ups import AlertPop
logger.debug(obj.ctx)
# initialize samples
obj.samples = []
obj.reagents = {}
# 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")
logger.debug(f"Attempting to parse file: {fname}")
if not fname.exists():
@@ -69,7 +65,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
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$ |
@@ -161,13 +156,11 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
if hasattr(obj, 'ext_kit'):
obj.kit_integrity_completion()
logger.debug(f"Imported reagents: {obj.reagents}")
return obj, result
def kit_reload_function(obj:QMainWindow) -> QMainWindow:
result = None
for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
# item.setParent(None)
if isinstance(item, QLabel):
if item.text().startswith("Lot"):
item.setParent(None)
@@ -180,20 +173,22 @@ def kit_reload_function(obj:QMainWindow) -> QMainWindow:
def kit_integrity_completion_function(obj:QMainWindow) -> QMainWindow:
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)
# find the widget that contains lit info
kit_widget = obj.table_widget.formlayout.parentWidget().findChild(QComboBox, 'extraction_kit')
logger.debug(f"Kit selector: {kit_widget}")
# get current kit info
obj.ext_kit = kit_widget.currentText()
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)
# get all reagents stored in the QWindow object
reagents_to_lookup = [item.replace("lot_", "") for item in obj.reagents]
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)
# if kit integrity comes back with an error, make widgets with missing reagents using default info
if kit_integrity != None:
# msg = AlertPop(message=kit_integrity['message'], status="critical")
# msg.exec()
result = dict(message=kit_integrity['message'], status="Warning")
for item in kit_integrity['missing']:
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:
result = None
# from .custom_widgets.misc import ImportReagent
# from .custom_widgets.pop_ups import AlertPop, QuestionAsker
# extract info from the form widgets
info = extract_form_info(obj.table_widget.tab1)
# seperate out reagents
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_")}
logger.debug(f"Info: {info}")
@@ -268,12 +263,9 @@ def submit_new_sample_function(obj:QMainWindow) -> QMainWindow:
# reset form
for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
item.setParent(None)
# print(dir(obj))
if hasattr(obj, 'csv'):
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
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")
try:
obj.csv.to_csv(fname.__str__(), index=False)
@@ -282,8 +274,8 @@ def submit_new_sample_function(obj:QMainWindow) -> QMainWindow:
return obj, result
def generate_report_function(obj:QMainWindow) -> QMainWindow:
# from .custom_widgets import ReportDatePicker
result = None
# ask for date ranges
dlg = ReportDatePicker()
if dlg.exec():
info = extract_form_info(dlg)
@@ -324,8 +316,6 @@ def generate_report_function(obj:QMainWindow) -> QMainWindow:
def add_kit_function(obj:QMainWindow) -> QMainWindow:
result = None
# 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")
assert fname.exists()
# read yaml file
@@ -340,19 +330,11 @@ def add_kit_function(obj:QMainWindow) -> QMainWindow:
return
# send to kit creator function
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
def add_org_function(obj:QMainWindow) -> QMainWindow:
result = None
# 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")
assert fname.exists()
# read yaml file
@@ -367,12 +349,6 @@ def add_org_function(obj:QMainWindow) -> QMainWindow:
return obj, result
# send to kit creator function
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
def controls_getter_function(obj:QMainWindow) -> QMainWindow:
@@ -391,7 +367,7 @@ def controls_getter_function(obj:QMainWindow) -> QMainWindow:
obj.table_widget.datepicker.start_date.setDate(threemonthsago)
obj._controls_getter()
return obj, result
# convert to python useable date object
# convert to python useable date object
obj.start_date = obj.table_widget.datepicker.start_date.date().toPyDate()
obj.end_date = obj.table_widget.datepicker.end_date.date().toPyDate()
obj.con_type = obj.table_widget.control_typer.currentText()
@@ -412,8 +388,18 @@ def controls_getter_function(obj:QMainWindow) -> QMainWindow:
return obj, result
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
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() == "":
obj.subtype = None
else:
@@ -425,7 +411,7 @@ def chart_maker_function(obj:QMainWindow) -> QMainWindow:
if controls == None:
fig = None
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]
# flatten data to one dimensional list
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"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}")
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:
logger.debug(f"Running update on: {sample['sample']}")
sample['plate_rsl'] = sub.rsl_plate_num
@@ -655,11 +642,3 @@ def import_pcr_results_function(obj:QMainWindow) -> QMainWindow:
return obj, result
# dlg.exec()

View File

@@ -2,4 +2,5 @@
Contains all operations for creating charts, graphs and visual effects.
'''
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