hitpicking complete, pre-addition of WW-Arctic parsers and models.
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -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
|
||||
|
||||
- Added a detailed tab to the cost report.
|
||||
|
||||
@@ -62,3 +62,8 @@ This is meant to import .xslx files created from the Design & Analysis Software
|
||||
## Linking PCR Logs:
|
||||
1. Click "Monthly" -> "Link PCR Logs".
|
||||
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.
|
||||
|
||||
1
TODO.md
1
TODO.md
@@ -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 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.
|
||||
@@ -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 ###
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
# Version of the realpython-reader package
|
||||
__project__ = "submissions"
|
||||
__version__ = "202305.3b"
|
||||
__version__ = "202305.4b"
|
||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||
__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
|
||||
# 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
|
||||
# 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.
|
||||
# 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.
|
||||
@@ -20,7 +20,6 @@ from getpass import getuser
|
||||
import numpy as np
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from math import ceil
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -136,7 +135,13 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
|
||||
try:
|
||||
field_value = lookup_kittype_by_name(ctx=ctx, name=q_str)
|
||||
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}")
|
||||
case "submitting_lab":
|
||||
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)
|
||||
instance.run_cost = instance.run_cost - discounts
|
||||
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
|
||||
logger.debug(f"We've got a total cost of {instance.run_cost}")
|
||||
try:
|
||||
@@ -748,10 +753,16 @@ 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_well(ctx=ctx, sample_rsl=sample_obj['sample'], well_num=sample_obj['well_num'], plate_rsl=sample_obj['plate_rsl'])
|
||||
if ww_samp != None:
|
||||
# del sample_obj['well_number']
|
||||
for key, value in sample_obj.items():
|
||||
logger.debug(f"Setting {key} to {value}")
|
||||
# set attribute 'key' to 'value'
|
||||
setattr(ww_samp, key, 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)
|
||||
else:
|
||||
logger.error(f"Unable to find sample {sample_obj['sample']}")
|
||||
return
|
||||
@@ -762,4 +773,29 @@ def lookup_discounts_by_org_and_kit(ctx:dict, kit_id:int, lab_id:int):
|
||||
return ctx['database_session'].query(models.Discount).join(models.KitType).join(models.Organization).filter(and_(
|
||||
models.KitType.id==kit_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
|
||||
@@ -4,6 +4,9 @@ All models for individual samples.
|
||||
from . import Base
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class WWSample(Base):
|
||||
@@ -19,7 +22,7 @@ class WWSample(Base):
|
||||
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"))
|
||||
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.
|
||||
# I have no idea when they will be implemented or how.
|
||||
testing_type = Column(String(64))
|
||||
@@ -33,6 +36,7 @@ class WWSample(Base):
|
||||
ww_seq_run_id = Column(String(64))
|
||||
sample_type = Column(String(8))
|
||||
pcr_results = Column(JSON)
|
||||
elution_well = Column(String(8)) #: location on 96 well plate
|
||||
|
||||
|
||||
def to_string(self) -> str:
|
||||
@@ -51,6 +55,10 @@ class WWSample(Base):
|
||||
Returns:
|
||||
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:
|
||||
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:
|
||||
@@ -59,6 +67,34 @@ class WWSample(Base):
|
||||
"well": self.well_number,
|
||||
"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):
|
||||
|
||||
@@ -132,7 +132,10 @@ class SheetParser(object):
|
||||
try:
|
||||
expiry = row[3].date()
|
||||
except AttributeError as e:
|
||||
expiry = datetime.strptime(row[3], "%Y-%m-%d")
|
||||
try:
|
||||
expiry = datetime.strptime(row[3], "%Y-%m-%d")
|
||||
except TypeError as e:
|
||||
expiry = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + row[3] - 2)
|
||||
else:
|
||||
logger.debug(f"Date: {row[3]}")
|
||||
expiry = date.today()
|
||||
@@ -378,7 +381,7 @@ class PCRParser(object):
|
||||
self.samples_df['Assessment'] = well_call_df.values
|
||||
except ValueError:
|
||||
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]
|
||||
for ii, row in self.samples_df.iterrows():
|
||||
try:
|
||||
@@ -387,7 +390,7 @@ class PCRParser(object):
|
||||
sample_obj = dict(
|
||||
sample = row['Sample'],
|
||||
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"row: {row}")
|
||||
|
||||
@@ -213,3 +213,7 @@ def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
|
||||
return df
|
||||
# else:
|
||||
# return df
|
||||
|
||||
|
||||
def make_hitpicks(input:list) -> DataFrame:
|
||||
return DataFrame.from_records(input)
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 *
|
||||
80
src/submissions/frontend/visualizations/plate_map.py
Normal file
80
src/submissions/frontend/visualizations/plate_map.py
Normal 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
|
||||
BIN
src/submissions/resources/arial.ttf
Normal file
BIN
src/submissions/resources/arial.ttf
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||
</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>
|
||||
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2> <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 %}
|
||||
@@ -93,5 +93,9 @@
|
||||
{% endif %}
|
||||
{% endfor %}</p>
|
||||
{% 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>
|
||||
</html>
|
||||
@@ -92,14 +92,12 @@ def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None
|
||||
check = set(ext_kit_rtypes) == set(reagenttypes)
|
||||
logger.debug(f"Checking if reagents match kit contents: {check}")
|
||||
# what reagent types are in both lists?
|
||||
# common = list(set(ext_kit_rtypes).intersection(reagenttypes))
|
||||
missing = list(set(ext_kit_rtypes).difference(reagenttypes))
|
||||
logger.debug(f"Missing reagents types: {missing}")
|
||||
# if lists are equal return no problem
|
||||
if len(missing)==0:
|
||||
result = None
|
||||
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}
|
||||
return result
|
||||
|
||||
@@ -168,7 +166,6 @@ class RSLNamer(object):
|
||||
Object that will enforce proper formatting on RSL plate names.
|
||||
"""
|
||||
def __init__(self, instr:str):
|
||||
# self.parsed_name, self.submission_type = self.retrieve_rsl_number(instr)
|
||||
self.retrieve_rsl_number(in_str=instr)
|
||||
if self.submission_type != None:
|
||||
parser = getattr(self, f"enforce_{self.submission_type}")
|
||||
@@ -195,12 +192,13 @@ class RSLNamer(object):
|
||||
return
|
||||
logger.debug(f"Attempting match of {in_str}")
|
||||
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})
|
||||
""", flags = re.IGNORECASE | re.VERBOSE)
|
||||
m = regex.search(in_str)
|
||||
try:
|
||||
self.parsed_name = m.group().upper()
|
||||
logger.debug(f"Got parsed submission name: {self.parsed_name}")
|
||||
self.submission_type = m.lastgroup
|
||||
except AttributeError as e:
|
||||
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
|
||||
"""
|
||||
# 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 = 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"(\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
|
||||
"""
|
||||
# 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})(\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)
|
||||
|
||||
Reference in New Issue
Block a user