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
|
## 202305.03
|
||||||
|
|
||||||
- Added a detailed tab to the cost report.
|
- 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:
|
## 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.
|
||||||
|
|||||||
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 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.
|
||||||
@@ -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
|
# 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.
|
||||||
@@ -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
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 *
|
||||||
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>
|
<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> <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> <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>
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user