Various bug fixes for new forms.
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## 202404.04
|
||||
|
||||
- Storing of default values in db rather than hardcoded.
|
||||
|
||||
## 202404.03
|
||||
|
||||
- Package updates.
|
||||
|
||||
7
TODO.md
7
TODO.md
@@ -1,4 +1,7 @@
|
||||
- [ ] Critical: Convert Json lits to dicts so I can have them update properly without using crashy Sqlalchemy-json
|
||||
- [ ] Put "Not applicable" reagents in to_dict() method.
|
||||
- Currently in to_pydantic().
|
||||
- [x] Critical: Convert Json lits to dicts so I can have them update properly without using crashy Sqlalchemy-json
|
||||
- Was actually not necessary.
|
||||
- [ ] Fix Parsed/Missing mix ups.
|
||||
- [x] Have sample parser check for controls and add to reagents?
|
||||
- [x] Update controls to NestedMutableJson
|
||||
@@ -6,7 +9,7 @@
|
||||
- Possibly due to immutable JSON? But... it's worked before... Right?
|
||||
- Based on research, if a top-level JSON field is not changed, SQLalchemy will not detect changes.
|
||||
- Using sqlalchemy-json module seems to have helped.
|
||||
- [ ] Add Bead basher and Assit to DB.
|
||||
- [ ] Add Bead basher and Assist to DB.
|
||||
- [x] Artic not creating right plate name.
|
||||
- [ ] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass
|
||||
- [x] Fix updating of Extraction Kit in submission form widget.
|
||||
|
||||
@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-demo.db
|
||||
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\mytests\test_assets\submissions-test.db
|
||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\mytests\test_assets\submissions-test.db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""adding cost centre storage to basicsubmission
|
||||
|
||||
Revision ID: 6d2a357860ef
|
||||
Revises: e6647bd661d9
|
||||
Create Date: 2024-04-24 13:01:14.923814
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6d2a357860ef'
|
||||
down_revision = 'e6647bd661d9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# op.drop_table('_alembic_tmp__submissionsampleassociation')
|
||||
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('cost_centre', sa.String(length=64), nullable=True))
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
|
||||
batch_op.drop_column('used_cost_centre')
|
||||
|
||||
op.create_table('_alembic_tmp__submissionsampleassociation',
|
||||
sa.Column('sample_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('row', sa.INTEGER(), nullable=False),
|
||||
sa.Column('column', sa.INTEGER(), nullable=False),
|
||||
sa.Column('base_sub_type', sa.VARCHAR(), nullable=True),
|
||||
sa.Column('id', sa.INTEGER(), server_default=sa.text('1'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
|
||||
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
|
||||
sa.UniqueConstraint('id', name='ssa_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,33 @@
|
||||
"""adding default info to submissiontype
|
||||
|
||||
Revision ID: e6647bd661d9
|
||||
Revises: f18487b41f45
|
||||
Create Date: 2024-04-22 12:02:21.512781
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e6647bd661d9'
|
||||
down_revision = 'f18487b41f45'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('defaults', sa.JSON(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
|
||||
batch_op.drop_column('defaults')
|
||||
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -374,11 +374,15 @@ class Reagent(BaseClass):
|
||||
except (TypeError, AttributeError) as e:
|
||||
place_holder = date.today()
|
||||
logger.debug(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing")
|
||||
if self.expiry.year == 1970:
|
||||
place_holder = "NA"
|
||||
else:
|
||||
place_holder = place_holder.strftime("%Y-%m-%d")
|
||||
return dict(
|
||||
name=self.name,
|
||||
type=rtype,
|
||||
lot=self.lot,
|
||||
expiry=place_holder.strftime("%Y-%m-%d")
|
||||
expiry=place_holder
|
||||
)
|
||||
|
||||
def update_last_used(self, kit:KitType) -> Report:
|
||||
@@ -410,6 +414,7 @@ class Reagent(BaseClass):
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
id:int|None=None,
|
||||
reagent_type:str|ReagentType|None=None,
|
||||
lot_number:str|None=None,
|
||||
name:str|None=None,
|
||||
@@ -428,6 +433,12 @@ class Reagent(BaseClass):
|
||||
models.Reagent | List[models.Reagent]: reagent or list of reagents matching filter.
|
||||
"""
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
match id:
|
||||
case int():
|
||||
query = query.filter(cls.id==id)
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
match reagent_type:
|
||||
case str():
|
||||
# logger.debug(f"Looking up reagents by reagent type str: {reagent_type}")
|
||||
@@ -535,6 +546,7 @@ class SubmissionType(BaseClass):
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(128), unique=True) #: name of submission type
|
||||
info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type.
|
||||
defaults = Column(JSON) #: Basic information about this submission type
|
||||
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
|
||||
template_file = Column(BLOB) #: Blank form for this type stored as binary.
|
||||
processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes) #: Relation to equipment processes used for this type.
|
||||
@@ -653,6 +665,10 @@ class SubmissionType(BaseClass):
|
||||
raise TypeError(f"Type {type(equipment_role)} is not allowed")
|
||||
return list(set([item for items in relevant for item in items if item != None ]))
|
||||
|
||||
def get_submission_class(self):
|
||||
from .submissions import BasicSubmission
|
||||
return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name)
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
|
||||
@@ -65,8 +65,8 @@ class Organization(BaseClass):
|
||||
pass
|
||||
match name:
|
||||
case str():
|
||||
# logger.debug(f"Looking up organization with name: {name}")
|
||||
query = query.filter(cls.name==name)
|
||||
# logger.debug(f"Looking up organization with name starting with: {name}")
|
||||
query = query.filter(cls.name.startswith(name))
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
|
||||
@@ -3,12 +3,12 @@ Models for the main submission types.
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from getpass import getuser
|
||||
import logging, uuid, tempfile, re, yaml, base64
|
||||
import logging, uuid, tempfile, re, yaml, base64, sys
|
||||
from zipfile import ZipFile
|
||||
from tempfile import TemporaryDirectory
|
||||
from reportlab.graphics.barcode import createBarcodeImageInMemory
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib.units import mm
|
||||
# from reportlab.graphics.barcode import createBarcodeImageInMemory
|
||||
# from reportlab.graphics.shapes import Drawing
|
||||
# from reportlab.lib.units import mm
|
||||
from operator import attrgetter, itemgetter
|
||||
from pprint import pformat
|
||||
from . import BaseClass, Reagent, SubmissionType, KitType, Organization
|
||||
@@ -18,9 +18,6 @@ from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLO
|
||||
from sqlalchemy.orm import relationship, validates, Query
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
# from sqlalchemy.ext.declarative import declared_attr
|
||||
# from sqlalchemy_json import NestedMutableJson
|
||||
# from sqlalchemy.ext.mutable import MutableDict
|
||||
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
|
||||
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||
import pandas as pd
|
||||
@@ -59,9 +56,10 @@ class BasicSubmission(BaseClass):
|
||||
reagents_id = Column(String, ForeignKey("_reagent.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents
|
||||
extraction_info = Column(JSON) #: unstructured output from the extraction table logger.
|
||||
run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation.
|
||||
uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database.
|
||||
signed_by = Column(String(32)) #: user name of person who submitted the submission to the database.
|
||||
comment = Column(JSON) #: user notes
|
||||
submission_category = Column(String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name
|
||||
cost_centre = Column(String(64)) #: Permanent storage of used cost centre in case organization field changed in the future.
|
||||
|
||||
submission_sample_associations = relationship(
|
||||
"SubmissionSampleAssociation",
|
||||
@@ -103,12 +101,59 @@ class BasicSubmission(BaseClass):
|
||||
return f"{submission_type}Submission({self.rsl_plate_num})"
|
||||
|
||||
@classmethod
|
||||
def jsons(cls):
|
||||
def jsons(cls) -> List[str]:
|
||||
output = [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
|
||||
if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission":
|
||||
output += BasicSubmission.jsons()
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_default_info(cls, *args):
|
||||
# Create defaults for all submission_types
|
||||
# print(args)
|
||||
recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
|
||||
dicto = dict(
|
||||
details_ignore = ['excluded', 'reagents', 'samples',
|
||||
'extraction_info', 'comment', 'barcode',
|
||||
'platemap', 'export_map', 'equipment'],
|
||||
form_recover = recover,
|
||||
form_ignore = ['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by'] + recover,
|
||||
parser_ignore = ['samples', 'signed_by'] + cls.jsons(),
|
||||
excel_ignore = []
|
||||
)
|
||||
# Grab subtype specific info.
|
||||
st = cls.get_submission_type()
|
||||
if st is None:
|
||||
logger.error("No default info for BasicSubmission.")
|
||||
return dicto
|
||||
else:
|
||||
dicto['submission_type'] = st.name
|
||||
output = {}
|
||||
for k,v in dicto.items():
|
||||
if len(args) > 0 and k not in args:
|
||||
logger.debug(f"Don't want {k}")
|
||||
continue
|
||||
else:
|
||||
output[k] = v
|
||||
for k,v in st.defaults.items():
|
||||
if len(args) > 0 and k not in args:
|
||||
logger.debug(f"Don't want {k}")
|
||||
continue
|
||||
else:
|
||||
match v:
|
||||
case list():
|
||||
output[k] += v
|
||||
case _:
|
||||
output[k] = v
|
||||
if len(args) == 1:
|
||||
return output[args[0]]
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_submission_type(cls):
|
||||
name = cls.__mapper_args__['polymorphic_identity']
|
||||
return SubmissionType.query(name=name)
|
||||
|
||||
def to_dict(self, full_data:bool=False, backup:bool=False, report:bool=False) -> dict:
|
||||
"""
|
||||
Constructs dictionary used in submissions summary
|
||||
@@ -168,6 +213,11 @@ class BasicSubmission(BaseClass):
|
||||
logger.debug(f"Attempting reagents.")
|
||||
try:
|
||||
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.submission_reagent_associations]
|
||||
for k in self.extraction_kit.construct_xl_map_for_use(self.submission_type):
|
||||
if k == 'info':
|
||||
continue
|
||||
if not any([item['type']==k for item in reagents]):
|
||||
reagents.append(dict(type=k, name="Not Applicable", lot="NA", expiry=date(year=1970, month=1, day=1), missing=True))
|
||||
except Exception as e:
|
||||
logger.error(f"We got an error retrieving reagents: {e}")
|
||||
reagents = None
|
||||
@@ -181,10 +231,12 @@ class BasicSubmission(BaseClass):
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting equipment: {e}")
|
||||
equipment = None
|
||||
cost_centre = self.cost_centre
|
||||
else:
|
||||
reagents = None
|
||||
samples = None
|
||||
equipment = None
|
||||
cost_centre = None
|
||||
# logger.debug("Getting comments")
|
||||
try:
|
||||
comments = self.comment
|
||||
@@ -198,6 +250,8 @@ class BasicSubmission(BaseClass):
|
||||
output["extraction_info"] = ext_info
|
||||
output["comment"] = comments
|
||||
output["equipment"] = equipment
|
||||
output["Cost Centre"] = cost_centre
|
||||
output["Signed By"] = self.signed_by
|
||||
return output
|
||||
|
||||
def calculate_column_count(self) -> int:
|
||||
@@ -293,18 +347,18 @@ class BasicSubmission(BaseClass):
|
||||
"""
|
||||
return [item.role for item in self.submission_equipment_associations]
|
||||
|
||||
def make_plate_barcode(self, width:int=100, height:int=25) -> Drawing:
|
||||
"""
|
||||
Creates a barcode image for this BasicSubmission.
|
||||
# def make_plate_barcode(self, width:int=100, height:int=25) -> Drawing:
|
||||
# """
|
||||
# Creates a barcode image for this BasicSubmission.
|
||||
|
||||
Args:
|
||||
width (int, optional): Width (pixels) of image. Defaults to 100.
|
||||
height (int, optional): Height (pixels) of image. Defaults to 25.
|
||||
# Args:
|
||||
# width (int, optional): Width (pixels) of image. Defaults to 100.
|
||||
# height (int, optional): Height (pixels) of image. Defaults to 25.
|
||||
|
||||
Returns:
|
||||
Drawing: image object
|
||||
"""
|
||||
return createBarcodeImageInMemory('Code128', value=self.rsl_plate_num, width=width*mm, height=height*mm, humanReadable=True, format="png")
|
||||
# Returns:
|
||||
# Drawing: image object
|
||||
# """
|
||||
# return createBarcodeImageInMemory('Code128', value=self.rsl_plate_num, width=width*mm, height=height*mm, humanReadable=True, format="png")
|
||||
|
||||
@classmethod
|
||||
def submissions_to_df(cls, submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
|
||||
@@ -384,11 +438,17 @@ class BasicSubmission(BaseClass):
|
||||
case item if item in self.jsons():
|
||||
logger.debug(f"Setting JSON attribute.")
|
||||
existing = self.__getattribute__(key)
|
||||
if value == "" or value is None or value == 'null':
|
||||
logger.error(f"No value given, not setting.")
|
||||
return
|
||||
if existing is None:
|
||||
existing = []
|
||||
if value in existing:
|
||||
logger.warning("Value already exists. Preventing duplicate addition.")
|
||||
return
|
||||
else:
|
||||
if isinstance(value, list):
|
||||
existing += value
|
||||
else:
|
||||
existing.append(value)
|
||||
self.__setattr__(key, existing)
|
||||
@@ -634,7 +694,7 @@ class BasicSubmission(BaseClass):
|
||||
from backend.validators import RSLNamer
|
||||
logger.debug(f"instr coming into {cls}: {instr}")
|
||||
logger.debug(f"data coming into {cls}: {data}")
|
||||
defaults = cls.get_default_info()
|
||||
defaults = cls.get_default_info("abbreviation", "submission_type")
|
||||
data['abbreviation'] = defaults['abbreviation']
|
||||
if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]:
|
||||
data['submission_type'] = defaults['submission_type']
|
||||
@@ -737,9 +797,7 @@ class BasicSubmission(BaseClass):
|
||||
Returns:
|
||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
base_dict['excluded'] = ['excluded', 'reagents', 'samples', 'controls',
|
||||
'extraction_info', 'pcr_info', 'comment',
|
||||
'barcode', 'platemap', 'export_map', 'equipment']
|
||||
base_dict['excluded'] = cls.get_default_info('details_ignore')
|
||||
env = jinja_template_loading()
|
||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||
logger.debug(f"Returning template: {temp_name}")
|
||||
@@ -1067,9 +1125,9 @@ class BacterialCulture(BasicSubmission):
|
||||
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_default_info(cls) -> dict:
|
||||
return dict(abbreviation="BC", submission_type="Bacterial Culture")
|
||||
# @classmethod
|
||||
# def get_default_info(cls) -> dict:
|
||||
# return dict(abbreviation="BC", submission_type="Bacterial Culture")
|
||||
|
||||
@classmethod
|
||||
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
|
||||
@@ -1214,13 +1272,19 @@ class Wastewater(BasicSubmission):
|
||||
output['pcr_info'] = self.pcr_info
|
||||
except TypeError as e:
|
||||
pass
|
||||
ext_tech = self.ext_technician or self.technician
|
||||
pcr_tech = self.pcr_technician or self.technician
|
||||
output['Technician'] = f"Enr: {self.technician}, Ext: {ext_tech}, PCR: {pcr_tech}"
|
||||
if self.ext_technician is None or self.ext_technician == "None":
|
||||
output['Ext Technician'] = self.technician
|
||||
else:
|
||||
output["Ext Technician"] = self.ext_technician
|
||||
if self.pcr_technician is None or self.pcr_technician == "None":
|
||||
output["PCR Technician"] = self.technician
|
||||
else:
|
||||
output['PCR Technician'] = self.pcr_technician
|
||||
# output['Technician'] = self.technician}, Ext: {ext_tech}, PCR: {pcr_tech}"
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_default_info(cls) -> dict:
|
||||
# @classmethod
|
||||
# def get_default_info(cls) -> dict:
|
||||
return dict(abbreviation="WW", submission_type="Wastewater")
|
||||
|
||||
@classmethod
|
||||
@@ -1334,36 +1398,6 @@ class Wastewater(BasicSubmission):
|
||||
from frontend.widgets import select_open_file
|
||||
fname = select_open_file(obj=obj, file_extension="xlsx")
|
||||
parser = PCRParser(filepath=fname)
|
||||
# Check if PCR info already exists
|
||||
# if hasattr(self, 'pcr_info') and self.pcr_info != None:
|
||||
# # existing = json.loads(sub.pcr_info)
|
||||
# existing = self.pcr_info
|
||||
# logger.debug(f"Found existing pcr info: {pformat(self.pcr_info)}")
|
||||
# else:
|
||||
# existing = None
|
||||
# if existing != None:
|
||||
# # update pcr_info
|
||||
# try:
|
||||
# logger.debug(f"Updating {type(existing)}:\n {pformat(existing)} with {type(parser.pcr)}:\n {pformat(parser.pcr)}")
|
||||
# # if json.dumps(parser.pcr) not in sub.pcr_info:
|
||||
# if parser.pcr not in self.pcr_info:
|
||||
# logger.debug(f"This is new pcr info, appending to existing")
|
||||
# existing.append(parser.pcr)
|
||||
# else:
|
||||
# logger.debug("This info already exists, skipping.")
|
||||
# # logger.debug(f"Setting {self.rsl_plate_num} PCR to:\n {pformat(existing)}")
|
||||
# # sub.pcr_info = json.dumps(existing)
|
||||
# self.pcr_info = existing
|
||||
# except TypeError:
|
||||
# logger.error(f"Error updating!")
|
||||
# # sub.pcr_info = json.dumps([parser.pcr])
|
||||
# self.pcr_info = [parser.pcr]
|
||||
# logger.debug(f"Final pcr info for {self.rsl_plate_num}:\n {pformat(self.pcr_info)}")
|
||||
# else:
|
||||
# # sub.pcr_info = json.dumps([parser.pcr])
|
||||
# self.pcr_info = [parser.pcr]
|
||||
# # logger.debug(f"Existing {type(self.pcr_info)}: {self.pcr_info}")
|
||||
# # logger.debug(f"Inserting {type(parser.pcr)}: {parser.pcr}")
|
||||
self.set_attribute("pcr_info", parser.pcr)
|
||||
self.save(original=False)
|
||||
logger.debug(f"Got {len(parser.samples)} samples to update!")
|
||||
@@ -1390,7 +1424,6 @@ class WastewaterArtic(BasicSubmission):
|
||||
gel_controls = Column(JSON) #: locations of controls on the gel
|
||||
source_plates = Column(JSON) #: wastewater plates that samples come from
|
||||
|
||||
|
||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSubmission.id))
|
||||
@@ -1411,9 +1444,9 @@ class WastewaterArtic(BasicSubmission):
|
||||
output['source_plates'] = self.source_plates
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_default_info(cls) -> str:
|
||||
return dict(abbreviation="AR", submission_type="Wastewater Artic")
|
||||
# @classmethod
|
||||
# def get_default_info(cls) -> str:
|
||||
# return dict(abbreviation="AR", submission_type="Wastewater Artic")
|
||||
|
||||
@classmethod
|
||||
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||
@@ -1429,12 +1462,16 @@ class WastewaterArtic(BasicSubmission):
|
||||
"""
|
||||
# from backend.validators import RSLNamer
|
||||
input_dict = super().parse_info(input_dict)
|
||||
ws = load_workbook(xl.io, data_only=True)['Egel results']
|
||||
data = [ws.cell(row=jj,column=ii) for ii in range(15,27) for jj in range(10,18)]
|
||||
workbook = load_workbook(xl.io, data_only=True)
|
||||
ws = workbook['Egel results']
|
||||
data = [ws.cell(row=ii,column=jj) for jj in range(15,27) for ii in range(10,18)]
|
||||
data = [cell for cell in data if cell.value is not None and "NTC" in cell.value]
|
||||
input_dict['gel_controls'] = [dict(sample_id=cell.value, location=f"{row_map[cell.row-9]}{str(cell.column-14).zfill(2)}") for cell in data]
|
||||
# df = xl.parse("Egel results").iloc[7:16, 13:26]
|
||||
# df = df.set_index(df.columns[0])
|
||||
ws = workbook['First Strand List']
|
||||
data = [dict(plate=ws.cell(row=ii, column=3).value, starting_sample=ws.cell(row=ii, column=4).value) for ii in range(8,11)]
|
||||
input_dict['source_plates'] = data
|
||||
return input_dict
|
||||
|
||||
@classmethod
|
||||
@@ -1500,34 +1537,38 @@ class WastewaterArtic(BasicSubmission):
|
||||
Returns:
|
||||
str: output name
|
||||
"""
|
||||
logger.debug(f"input string raw: {input_str}")
|
||||
# Remove letters.
|
||||
processed = re.sub(r"[A-Z]", "", input_str)
|
||||
processed = re.sub(r"[A-QS-Z]+\d*", "", input_str)
|
||||
# Remove trailing '-' if any
|
||||
processed = processed.strip("-")
|
||||
logger.debug(f"Processed after stripping letters: {processed}")
|
||||
try:
|
||||
en_num = re.search(r"\-\d{1}$", processed).group()
|
||||
processed = rreplace(processed, en_num, "")
|
||||
except AttributeError:
|
||||
en_num = "1"
|
||||
en_num = en_num.strip("-")
|
||||
# logger.debug(f"Processed after en-num: {processed}")
|
||||
logger.debug(f"Processed after en-num: {processed}")
|
||||
try:
|
||||
plate_num = re.search(r"\-\d{1}$", processed).group()
|
||||
plate_num = re.search(r"\-\d{1}R?\d?$", processed).group()
|
||||
processed = rreplace(processed, plate_num, "")
|
||||
except AttributeError:
|
||||
plate_num = "1"
|
||||
plate_num = plate_num.strip("-")
|
||||
# logger.debug(f"Processed after plate-num: {processed}")
|
||||
logger.debug(f"Processed after plate-num: {processed}")
|
||||
day = re.search(r"\d{2}$", processed).group()
|
||||
processed = rreplace(processed, day, "")
|
||||
# logger.debug(f"Processed after day: {processed}")
|
||||
logger.debug(f"Processed after day: {processed}")
|
||||
month = re.search(r"\d{2}$", processed).group()
|
||||
processed = rreplace(processed, month, "")
|
||||
processed = processed.replace("--", "")
|
||||
# logger.debug(f"Processed after month: {processed}")
|
||||
logger.debug(f"Processed after month: {processed}")
|
||||
year = re.search(r'^(?:\d{2})?\d{2}', processed).group()
|
||||
year = f"20{year}"
|
||||
return f"EN{year}{month}{day}-{en_num}"
|
||||
final_en_name = f"EN{year}{month}{day}-{en_num}"
|
||||
logger.debug(f"Final EN name: {final_en_name}")
|
||||
return final_en_name
|
||||
|
||||
@classmethod
|
||||
def get_regex(cls) -> str:
|
||||
@@ -1599,7 +1640,23 @@ class WastewaterArtic(BasicSubmission):
|
||||
# for jjj, value in enumerate(plate, start=3):
|
||||
# worksheet.cell(row=iii, column=jjj, value=value)
|
||||
logger.debug(f"Info:\n{pformat(info)}")
|
||||
check = 'gel_info' in info.keys() and info['gel_info']['value'] != None
|
||||
check = 'source_plates' in info.keys() and info['source_plates'] is not None
|
||||
if check:
|
||||
worksheet = input_excel['First Strand List']
|
||||
start_row = 8
|
||||
for iii, plate in enumerate(info['source_plates']['value']):
|
||||
logger.debug(f"Plate: {plate}")
|
||||
row = start_row + iii
|
||||
try:
|
||||
worksheet.cell(row=row, column=3, value=plate['plate'])
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
worksheet.cell(row=row, column=4, value=plate['starting_sample'])
|
||||
except TypeError:
|
||||
pass
|
||||
# sys.exit(f"Hardcoded stop: backend.models.submissions:1629")
|
||||
check = 'gel_info' in info.keys() and info['gel_info']['value'] is not None
|
||||
if check:
|
||||
# logger.debug(f"Gel info check passed.")
|
||||
if info['gel_info'] != None:
|
||||
@@ -1618,7 +1675,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
column = start_column + 2 + jjj
|
||||
worksheet.cell(row=start_row, column=column, value=kj['name'])
|
||||
worksheet.cell(row=row, column=column, value=kj['value'])
|
||||
check = 'gel_image' in info.keys() and info['gel_image']['value'] != None
|
||||
check = 'gel_image' in info.keys() and info['gel_image']['value'] is not None
|
||||
if check:
|
||||
if info['gel_image'] != None:
|
||||
worksheet = input_excel['Egel results']
|
||||
|
||||
@@ -62,9 +62,11 @@ class SheetParser(object):
|
||||
parser = InfoParser(xl=self.xl, submission_type=self.sub['submission_type']['value'])
|
||||
info = parser.parse_info()
|
||||
self.info_map = parser.map
|
||||
# exclude_from_info = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.sub['submission_type']).exclude_from_info_parser()
|
||||
for k,v in info.items():
|
||||
match k:
|
||||
case "sample":
|
||||
# case item if
|
||||
pass
|
||||
case _:
|
||||
self.sub[k] = v
|
||||
@@ -97,9 +99,9 @@ class SheetParser(object):
|
||||
"""
|
||||
Enforce that the parser has an extraction kit
|
||||
"""
|
||||
from frontend.widgets.pop_ups import KitSelector
|
||||
from frontend.widgets.pop_ups import ObjectSelector
|
||||
if not check_not_nan(self.sub['extraction_kit']['value']):
|
||||
dlg = KitSelector(title="Kit Needed", message="At minimum a kit is needed. Please select one.")
|
||||
dlg = ObjectSelector(title="Kit Needed", message="At minimum a kit is needed. Please select one.", obj_type=KitType)
|
||||
if dlg.exec():
|
||||
self.sub['extraction_kit'] = dict(value=dlg.getValues(), missing=True)
|
||||
else:
|
||||
@@ -133,7 +135,11 @@ class SheetParser(object):
|
||||
"""
|
||||
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
|
||||
logger.debug(f"Equipment: {self.sub['equipment']}")
|
||||
if len(self.sub['equipment']) == 0:
|
||||
try:
|
||||
check = len(self.sub['equipment']) == 0
|
||||
except TypeError:
|
||||
check = True
|
||||
if check:
|
||||
self.sub['equipment'] = None
|
||||
psm = PydSubmission(filepath=self.filepath, **self.sub)
|
||||
return psm
|
||||
@@ -142,11 +148,12 @@ class InfoParser(object):
|
||||
|
||||
def __init__(self, xl:pd.ExcelFile, submission_type:str):
|
||||
logger.info(f"\n\Hello from InfoParser!\n\n")
|
||||
self.map = self.fetch_submission_info_map(submission_type=submission_type)
|
||||
self.submission_type = submission_type
|
||||
self.map = self.fetch_submission_info_map()
|
||||
self.xl = xl
|
||||
logger.debug(f"Info map for InfoParser: {pformat(self.map)}")
|
||||
|
||||
def fetch_submission_info_map(self, submission_type:str|dict) -> dict:
|
||||
def fetch_submission_info_map(self) -> dict:
|
||||
"""
|
||||
Gets location of basic info from the submission_type object in the database.
|
||||
|
||||
@@ -156,10 +163,10 @@ class InfoParser(object):
|
||||
Returns:
|
||||
dict: Location map of all info for this submission type
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = dict(value=submission_type, missing=True)
|
||||
logger.debug(f"Looking up submission type: {submission_type['value']}")
|
||||
submission_type = SubmissionType.query(name=submission_type['value'])
|
||||
if isinstance(self.submission_type, str):
|
||||
self.submission_type = dict(value=self.submission_type, missing=True)
|
||||
logger.debug(f"Looking up submission type: {self.submission_type['value']}")
|
||||
submission_type = SubmissionType.query(name=self.submission_type['value'])
|
||||
info_map = submission_type.info_map
|
||||
# Get the parse_info method from the submission type specified
|
||||
self.custom_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name).parse_info
|
||||
@@ -172,16 +179,25 @@ class InfoParser(object):
|
||||
Returns:
|
||||
dict: key:value of basic info
|
||||
"""
|
||||
if isinstance(self.submission_type, str):
|
||||
self.submission_type = dict(value=self.submission_type, missing=True)
|
||||
dicto = {}
|
||||
exclude_from_generic = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type['value']).get_default_info("parser_ignore")
|
||||
# This loop parses generic info
|
||||
logger.debug(f"Map: {self.map}")
|
||||
# time.sleep(5)
|
||||
for sheet in self.xl.sheet_names:
|
||||
df = self.xl.parse(sheet, header=None)
|
||||
relevant = {}
|
||||
for k, v in self.map.items():
|
||||
# exclude from generic parsing
|
||||
if k in exclude_from_generic:
|
||||
continue
|
||||
# If the value is hardcoded put it in the dictionary directly.
|
||||
if isinstance(v, str):
|
||||
dicto[k] = dict(value=v, missing=False)
|
||||
continue
|
||||
if k in ["samples", "all_sheets"]:
|
||||
continue
|
||||
logger.debug(f"Looking for {k} in self.map")
|
||||
if sheet in self.map[k]['sheets']:
|
||||
relevant[k] = v
|
||||
logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
|
||||
@@ -252,6 +268,7 @@ class ReagentParser(object):
|
||||
lot = df.iat[relevant[item]['lot']['row']-1, relevant[item]['lot']['column']-1]
|
||||
expiry = df.iat[relevant[item]['expiry']['row']-1, relevant[item]['expiry']['column']-1]
|
||||
if 'comment' in relevant[item].keys():
|
||||
logger.debug(f"looking for {relevant[item]} comment.")
|
||||
comment = df.iat[relevant[item]['comment']['row']-1, relevant[item]['comment']['column']-1]
|
||||
else:
|
||||
comment = ""
|
||||
@@ -294,7 +311,7 @@ class SampleParser(object):
|
||||
sample_info_map = self.fetch_sample_info_map(submission_type=submission_type, sample_map=sample_map)
|
||||
logger.debug(f"sample_info_map: {sample_info_map}")
|
||||
self.plate_map = self.construct_plate_map(plate_map_location=sample_info_map['plate_map'])
|
||||
logger.debug(f"plate_map: {self.plate_map}")
|
||||
# logger.debug(f"plate_map: {self.plate_map}")
|
||||
self.lookup_table = self.construct_lookup_table(lookup_table_location=sample_info_map['lookup_table'])
|
||||
if "plates" in sample_info_map:
|
||||
self.plates = sample_info_map['plates']
|
||||
@@ -439,7 +456,7 @@ class SampleParser(object):
|
||||
"""
|
||||
result = None
|
||||
new_samples = []
|
||||
logger.debug(f"Starting samples: {pformat(self.samples)}")
|
||||
# logger.debug(f"Starting samples: {pformat(self.samples)}")
|
||||
for sample in self.samples:
|
||||
translated_dict = {}
|
||||
for k, v in sample.items():
|
||||
|
||||
@@ -74,8 +74,8 @@ class RSLNamer(object):
|
||||
check = True
|
||||
if check:
|
||||
# logger.debug("Final option, ask the user for submission type")
|
||||
from frontend.widgets import SubmissionTypeSelector
|
||||
dlg = SubmissionTypeSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.")
|
||||
from frontend.widgets import ObjectSelector
|
||||
dlg = ObjectSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.", obj_type=SubmissionType)
|
||||
if dlg.exec():
|
||||
submission_type = dlg.parse_form()
|
||||
submission_type = submission_type.replace("_", " ")
|
||||
|
||||
@@ -8,13 +8,13 @@ from pydantic import BaseModel, field_validator, Field
|
||||
from datetime import date, datetime, timedelta
|
||||
from dateutil.parser import parse
|
||||
from dateutil.parser._parser import ParserError
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Literal
|
||||
from . import RSLNamer
|
||||
from pathlib import Path
|
||||
from tools import check_not_nan, convert_nans_to_nones, Report, Result, row_map
|
||||
from backend.db.models import *
|
||||
from sqlalchemy.exc import StatementError, IntegrityError
|
||||
from PyQt6.QtWidgets import QComboBox, QWidget
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
from openpyxl import load_workbook, Workbook
|
||||
from io import BytesIO
|
||||
|
||||
@@ -24,7 +24,7 @@ class PydReagent(BaseModel):
|
||||
|
||||
lot: str|None
|
||||
type: str|None
|
||||
expiry: date|None
|
||||
expiry: date|Literal['NA']|None
|
||||
name: str|None
|
||||
missing: bool = Field(default=True)
|
||||
comment: str|None = Field(default="", validate_default=True)
|
||||
@@ -77,6 +77,8 @@ class PydReagent(BaseModel):
|
||||
match value:
|
||||
case int():
|
||||
return datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value - 2).date()
|
||||
case 'NA':
|
||||
return value
|
||||
case str():
|
||||
return parse(value)
|
||||
case date():
|
||||
@@ -87,6 +89,13 @@ class PydReagent(BaseModel):
|
||||
value = date.today()
|
||||
return value
|
||||
|
||||
@field_validator("expiry")
|
||||
@classmethod
|
||||
def date_na(cls, value):
|
||||
if isinstance(value, date) and value.year == 1970:
|
||||
value = "NA"
|
||||
return value
|
||||
|
||||
@field_validator("name", mode="before")
|
||||
@classmethod
|
||||
def enforce_name(cls, value, values):
|
||||
@@ -125,6 +134,10 @@ class PydReagent(BaseModel):
|
||||
reagent.type.append(reagent_type)
|
||||
case "comment":
|
||||
continue
|
||||
case "expiry":
|
||||
if isinstance(value, str):
|
||||
value = date(year=1970, month=1, day=1)
|
||||
reagent.expiry = value
|
||||
case _:
|
||||
try:
|
||||
reagent.__setattr__(key, value)
|
||||
@@ -271,6 +284,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
reagents: List[dict]|List[PydReagent] = []
|
||||
samples: List[PydSample]
|
||||
equipment: List[PydEquipment]|None =[]
|
||||
cost_centre: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
||||
|
||||
@field_validator('equipment', mode='before')
|
||||
@classmethod
|
||||
@@ -332,10 +346,28 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
@field_validator("submitting_lab", mode="before")
|
||||
@classmethod
|
||||
def rescue_submitting_lab(cls, value):
|
||||
if value == None:
|
||||
if value is None:
|
||||
return dict(value=None, missing=True)
|
||||
return value
|
||||
|
||||
@field_validator("submitting_lab")
|
||||
@classmethod
|
||||
def lookup_submitting_lab(cls, value):
|
||||
if isinstance(value['value'], str):
|
||||
try:
|
||||
value['value'] = Organization.query(name=value['value']).name
|
||||
except AttributeError:
|
||||
value['value'] = None
|
||||
if value['value'] is None:
|
||||
value['missing'] = True
|
||||
from frontend.widgets.pop_ups import ObjectSelector
|
||||
dlg = ObjectSelector(title="Missing Submitting Lab", message="We need a submitting lab. Please select from the list.", obj_type=Organization)
|
||||
if dlg.exec():
|
||||
value['value'] = dlg.getValues()
|
||||
else:
|
||||
value['value'] = None
|
||||
return value
|
||||
|
||||
@field_validator("rsl_plate_num", mode='before')
|
||||
@classmethod
|
||||
def rescue_rsl_number(cls, value):
|
||||
@@ -427,6 +459,30 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
output.append(sample)
|
||||
return output
|
||||
|
||||
@field_validator("cost_centre", mode="before")
|
||||
@classmethod
|
||||
def rescue_cost_centre(cls, value):
|
||||
match value:
|
||||
case dict():
|
||||
return value
|
||||
case _:
|
||||
return dict(value=value, missing=True)
|
||||
|
||||
@field_validator("cost_centre")
|
||||
@classmethod
|
||||
def get_cost_centre(cls, value, values):
|
||||
# logger.debug(f"Value coming in for cost_centre: {value}")
|
||||
match value['value']:
|
||||
case None:
|
||||
from backend.db.models import Organization
|
||||
org = Organization.query(name=values.data['submitting_lab']['value'])
|
||||
try:
|
||||
return dict(value=org.cost_centre, missing=True)
|
||||
except AttributeError:
|
||||
return dict(value="xxx", missing=True)
|
||||
case _:
|
||||
return value
|
||||
|
||||
def set_attribute(self, key, value):
|
||||
self.__setattr__(name=key, value=value)
|
||||
|
||||
@@ -599,6 +655,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
else:
|
||||
info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
|
||||
reagents = self.reagents
|
||||
|
||||
if len(reagents + list(info.keys())) == 0:
|
||||
# logger.warning("No info to fill in, returning")
|
||||
return None
|
||||
@@ -616,14 +673,14 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
new_reagent = {}
|
||||
new_reagent['type'] = reagent.type
|
||||
new_reagent['lot'] = excel_map[new_reagent['type']]['lot']
|
||||
new_reagent['lot']['value'] = reagent.lot
|
||||
new_reagent['lot']['value'] = reagent.lot or "NA"
|
||||
new_reagent['expiry'] = excel_map[new_reagent['type']]['expiry']
|
||||
new_reagent['expiry']['value'] = reagent.expiry
|
||||
new_reagent['expiry']['value'] = reagent.expiry or "NA"
|
||||
new_reagent['sheet'] = excel_map[new_reagent['type']]['sheet']
|
||||
# name is only present for Bacterial Culture
|
||||
try:
|
||||
new_reagent['name'] = excel_map[new_reagent['type']]['name']
|
||||
new_reagent['name']['value'] = reagent.name
|
||||
new_reagent['name']['value'] = reagent.name or "Not Applicable"
|
||||
except Exception as e:
|
||||
logger.error(f"Couldn't get name due to {e}")
|
||||
new_reagents.append(new_reagent)
|
||||
@@ -657,6 +714,8 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
# logger.debug(f"Attempting to write lot {reagent['lot']['value']} in: row {reagent['lot']['row']}, column {reagent['lot']['column']}")
|
||||
worksheet.cell(row=reagent['lot']['row'], column=reagent['lot']['column'], value=reagent['lot']['value'])
|
||||
# logger.debug(f"Attempting to write expiry {reagent['expiry']['value']} in: row {reagent['expiry']['row']}, column {reagent['expiry']['column']}")
|
||||
if reagent['expiry']['value'].year == 1970:
|
||||
reagent['expiry']['value'] = "NA"
|
||||
worksheet.cell(row=reagent['expiry']['row'], column=reagent['expiry']['column'], value=reagent['expiry']['value'])
|
||||
try:
|
||||
# logger.debug(f"Attempting to write name {reagent['name']['value']} in: row {reagent['name']['row']}, column {reagent['name']['column']}")
|
||||
@@ -790,14 +849,13 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
logger.debug(f"Extraction kit: {extraction_kit}. Is it a string? {isinstance(extraction_kit, str)}")
|
||||
if isinstance(extraction_kit, str):
|
||||
extraction_kit = dict(value=extraction_kit)
|
||||
if extraction_kit is not None:
|
||||
if extraction_kit != self.extraction_kit['value']:
|
||||
if extraction_kit is not None and extraction_kit != self.extraction_kit['value']:
|
||||
self.extraction_kit['value'] = extraction_kit['value']
|
||||
reagenttypes = []
|
||||
else:
|
||||
reagenttypes = [item.type for item in self.reagents]
|
||||
else:
|
||||
reagenttypes = [item.type for item in self.reagents]
|
||||
# reagenttypes = []
|
||||
# else:
|
||||
# reagenttypes = [item.type for item in self.reagents]
|
||||
# else:
|
||||
# reagenttypes = [item.type for item in self.reagents]
|
||||
logger.debug(f"Looking up {self.extraction_kit['value']}")
|
||||
ext_kit = KitType.query(name=self.extraction_kit['value'])
|
||||
ext_kit_rtypes = [item.to_pydantic() for item in ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])]
|
||||
@@ -808,21 +866,26 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
# logger.debug(f"Checking if reagents match kit contents: {check}")
|
||||
# # what reagent types are in both lists?
|
||||
# missing = list(set(ext_kit_rtypes).difference(reagenttypes))
|
||||
missing = []
|
||||
output_reagents = self.reagents
|
||||
# output_reagents = ext_kit_rtypes
|
||||
logger.debug(f"Already have these reagent types: {reagenttypes}")
|
||||
for rt in ext_kit_rtypes:
|
||||
if rt.type not in reagenttypes:
|
||||
missing.append(rt)
|
||||
if rt.type not in [item.type for item in output_reagents]:
|
||||
output_reagents.append(rt)
|
||||
logger.debug(f"Missing reagents types: {missing}")
|
||||
# missing = []
|
||||
# Exclude any reagenttype found in this pyd not expected in kit.
|
||||
expected_check = [item.type for item in ext_kit_rtypes]
|
||||
output_reagents = [rt for rt in self.reagents if rt.type in expected_check]
|
||||
logger.debug(f"Already have these reagent types: {output_reagents}")
|
||||
missing_check = [item.type for item in output_reagents]
|
||||
missing_reagents = [rt for rt in ext_kit_rtypes if rt.type not in missing_check]
|
||||
missing_reagents += [rt for rt in output_reagents if rt.missing]
|
||||
# for rt in ext_kit_rtypes:
|
||||
# if rt.type not in [item.type for item in output_reagents]:
|
||||
# missing.append(rt)
|
||||
# if rt.type not in [item.type for item in output_reagents]:
|
||||
# output_reagents.append(rt)
|
||||
output_reagents += [rt for rt in missing_reagents if rt not in output_reagents]
|
||||
logger.debug(f"Missing reagents types: {missing_reagents}")
|
||||
# if lists are equal return no problem
|
||||
if len(missing)==0:
|
||||
if len(missing_reagents)==0:
|
||||
result = None
|
||||
else:
|
||||
result = Result(msg=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.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!", status="Warning")
|
||||
result = Result(msg=f"The excel sheet you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in missing_reagents]}\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!", status="Warning")
|
||||
report.add_result(result)
|
||||
return output_reagents, report
|
||||
|
||||
|
||||
@@ -85,10 +85,10 @@ class AddReagentForm(QDialog):
|
||||
Returns:
|
||||
dict: Output info
|
||||
"""
|
||||
return dict(name=self.name_input.currentText(),
|
||||
lot=self.lot_input.text(),
|
||||
return dict(name=self.name_input.currentText().strip(),
|
||||
lot=self.lot_input.text().strip(),
|
||||
expiry=self.exp_input.date().toPyDate(),
|
||||
type=self.type_input.currentText())
|
||||
type=self.type_input.currentText().strip())
|
||||
|
||||
def update_names(self):
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from tools import jinja_template_loading
|
||||
import logging
|
||||
from backend.db.models import KitType, SubmissionType
|
||||
from backend.db import models
|
||||
from typing import Literal
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -45,16 +45,18 @@ class AlertPop(QMessageBox):
|
||||
self.setInformativeText(message)
|
||||
self.setWindowTitle(f"{owner} - {status.title()}")
|
||||
|
||||
class KitSelector(QDialog):
|
||||
class ObjectSelector(QDialog):
|
||||
"""
|
||||
dialog to input KitType manually
|
||||
dialog to input BaseClass type manually
|
||||
"""
|
||||
def __init__(self, title:str, message:str) -> QDialog:
|
||||
def __init__(self, title:str, message:str, obj_type:str|models.BaseClass) -> QDialog:
|
||||
super().__init__()
|
||||
self.setWindowTitle(title)
|
||||
self.widget = QComboBox()
|
||||
kits = [item.name for item in KitType.query()]
|
||||
self.widget.addItems(kits)
|
||||
if isinstance(obj_type, str):
|
||||
obj_type: models.BaseClass = getattr(models, obj_type)
|
||||
items = [item.name for item in obj_type.query()]
|
||||
self.widget.addItems(items)
|
||||
self.widget.setEditable(False)
|
||||
# set yes/no buttons
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
@@ -78,36 +80,69 @@ class KitSelector(QDialog):
|
||||
"""
|
||||
return self.widget.currentText()
|
||||
|
||||
class SubmissionTypeSelector(QDialog):
|
||||
"""
|
||||
dialog to input SubmissionType manually
|
||||
"""
|
||||
def __init__(self, title:str, message:str) -> QDialog:
|
||||
super().__init__()
|
||||
self.setWindowTitle(title)
|
||||
self.widget = QComboBox()
|
||||
# sub_type = [item.name for item in lookup_submission_type(ctx=ctx)]
|
||||
sub_type = [item.name for item in SubmissionType.query()]
|
||||
self.widget.addItems(sub_type)
|
||||
self.widget.setEditable(False)
|
||||
# set yes/no buttons
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
self.layout = QVBoxLayout()
|
||||
# Text for the yes/no question
|
||||
message = QLabel(message)
|
||||
self.layout.addWidget(message)
|
||||
self.layout.addWidget(self.widget)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
# class KitSelector(QDialog):
|
||||
# """
|
||||
# dialog to input KitType manually
|
||||
# """
|
||||
# def __init__(self, title:str, message:str) -> QDialog:
|
||||
# super().__init__()
|
||||
# self.setWindowTitle(title)
|
||||
# self.widget = QComboBox()
|
||||
# kits = [item.name for item in KitType.query()]
|
||||
# self.widget.addItems(kits)
|
||||
# self.widget.setEditable(False)
|
||||
# # set yes/no buttons
|
||||
# QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
# self.buttonBox = QDialogButtonBox(QBtn)
|
||||
# self.buttonBox.accepted.connect(self.accept)
|
||||
# self.buttonBox.rejected.connect(self.reject)
|
||||
# self.layout = QVBoxLayout()
|
||||
# # Text for the yes/no question
|
||||
# message = QLabel(message)
|
||||
# self.layout.addWidget(message)
|
||||
# self.layout.addWidget(self.widget)
|
||||
# self.layout.addWidget(self.buttonBox)
|
||||
# self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self) -> str:
|
||||
"""
|
||||
Pulls SubmissionType(str) from widget
|
||||
# def getValues(self) -> str:
|
||||
# """
|
||||
# Get KitType(str) from widget
|
||||
|
||||
Returns:
|
||||
str: SubmissionType as str
|
||||
"""
|
||||
return self.widget.currentText()
|
||||
# Returns:
|
||||
# str: KitType as str
|
||||
# """
|
||||
# return self.widget.currentText()
|
||||
|
||||
# class SubmissionTypeSelector(QDialog):
|
||||
# """
|
||||
# dialog to input SubmissionType manually
|
||||
# """
|
||||
# def __init__(self, title:str, message:str) -> QDialog:
|
||||
# super().__init__()
|
||||
# self.setWindowTitle(title)
|
||||
# self.widget = QComboBox()
|
||||
# # sub_type = [item.name for item in lookup_submission_type(ctx=ctx)]
|
||||
# sub_type = [item.name for item in SubmissionType.query()]
|
||||
# self.widget.addItems(sub_type)
|
||||
# self.widget.setEditable(False)
|
||||
# # set yes/no buttons
|
||||
# QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
# self.buttonBox = QDialogButtonBox(QBtn)
|
||||
# self.buttonBox.accepted.connect(self.accept)
|
||||
# self.buttonBox.rejected.connect(self.reject)
|
||||
# self.layout = QVBoxLayout()
|
||||
# # Text for the yes/no question
|
||||
# message = QLabel(message)
|
||||
# self.layout.addWidget(message)
|
||||
# self.layout.addWidget(self.widget)
|
||||
# self.layout.addWidget(self.buttonBox)
|
||||
# self.setLayout(self.layout)
|
||||
|
||||
# def parse_form(self) -> str:
|
||||
# """
|
||||
# Pulls SubmissionType(str) from widget
|
||||
|
||||
# Returns:
|
||||
# str: SubmissionType as str
|
||||
# """
|
||||
# return self.widget.currentText()
|
||||
|
||||
@@ -5,12 +5,12 @@ from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtCore import Qt, pyqtSlot
|
||||
|
||||
from backend.db.models import BasicSubmission, BasicSample
|
||||
from tools import check_if_app, check_authorization, is_power_user
|
||||
from tools import is_power_user, html_to_pdf
|
||||
from .functions import select_save_file
|
||||
from io import BytesIO
|
||||
from tempfile import TemporaryFile, TemporaryDirectory
|
||||
from pathlib import Path
|
||||
from xhtml2pdf import pisa
|
||||
# from xhtml2pdf import pisa
|
||||
import logging, base64
|
||||
from getpass import getuser
|
||||
from datetime import datetime
|
||||
@@ -54,6 +54,7 @@ class SubmissionDetails(QDialog):
|
||||
self.channel = QWebChannel()
|
||||
self.channel.registerObject('backend', self)
|
||||
self.submission_details(submission=sub)
|
||||
self.rsl_plate_num = sub.rsl_plate_num
|
||||
self.webview.page().setWebChannel(self.channel)
|
||||
|
||||
@pyqtSlot(str)
|
||||
@@ -86,9 +87,9 @@ class SubmissionDetails(QDialog):
|
||||
logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k != 'samples'})}")
|
||||
# don't want id
|
||||
del self.base_dict['id']
|
||||
logger.debug(f"Creating barcode.")
|
||||
if not check_if_app():
|
||||
self.base_dict['barcode'] = base64.b64encode(submission.make_plate_barcode(width=120, height=30)).decode('utf-8')
|
||||
# logger.debug(f"Creating barcode.")
|
||||
# if not check_if_app():
|
||||
# self.base_dict['barcode'] = base64.b64encode(submission.make_plate_barcode(width=120, height=30)).decode('utf-8')
|
||||
logger.debug(f"Making platemap...")
|
||||
self.base_dict['platemap'] = submission.make_plate_map()
|
||||
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict)
|
||||
@@ -103,8 +104,9 @@ class SubmissionDetails(QDialog):
|
||||
logger.debug(f"Signing off on {submission} - ({getuser()})")
|
||||
if isinstance(submission, str):
|
||||
submission = BasicSubmission.query(rsl_number=submission)
|
||||
submission.uploaded_by = getuser()
|
||||
submission.signed_by = getuser()
|
||||
submission.save()
|
||||
self.submission_details(submission=self.rsl_plate_num)
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
@@ -113,7 +115,7 @@ class SubmissionDetails(QDialog):
|
||||
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
|
||||
image_io = BytesIO()
|
||||
temp_dir = Path(TemporaryDirectory().name)
|
||||
hti = Html2Image(output_path=temp_dir, size=(1200, 750))
|
||||
hti = Html2Image(output_path=temp_dir, size=(2400, 1500))
|
||||
temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name)
|
||||
screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
|
||||
export_map = Image.open(screenshot[0])
|
||||
@@ -126,8 +128,9 @@ class SubmissionDetails(QDialog):
|
||||
del self.base_dict['platemap']
|
||||
self.html2 = self.template.render(sub=self.base_dict)
|
||||
try:
|
||||
with open(fname, "w+b") as f:
|
||||
pisa.CreatePDF(self.html2, dest=f)
|
||||
# with open(fname, "w+b") as f:
|
||||
# pisa.CreatePDF(self.html2, dest=f)
|
||||
html_to_pdf(html=self.html2, output_file=fname)
|
||||
except PermissionError as e:
|
||||
logger.error(f"Error saving pdf: {e}")
|
||||
msg = QMessageBox()
|
||||
|
||||
@@ -8,8 +8,8 @@ from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||
from PyQt6.QtGui import QAction, QCursor
|
||||
from backend.db.models import BasicSubmission
|
||||
from backend.excel import make_report_html, make_report_xlsx
|
||||
from tools import Report, Result, row_map, get_first_blank_df_row
|
||||
from xhtml2pdf import pisa
|
||||
from tools import Report, Result, row_map, get_first_blank_df_row, html_to_pdf
|
||||
# from xhtml2pdf import pisa
|
||||
from .functions import select_save_file, select_open_file
|
||||
from .misc import ReportDatePicker
|
||||
import pandas as pd
|
||||
@@ -324,8 +324,9 @@ class SubmissionsSheet(QTableView):
|
||||
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
||||
# get save location of report
|
||||
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf", extension="pdf")
|
||||
with open(fname, "w+b") as f:
|
||||
pisa.CreatePDF(html, dest=f)
|
||||
# with open(fname, "w+b") as f:
|
||||
# pisa.CreatePDF(html, dest=f)
|
||||
html_to_pdf(html=html, output_file=fname)
|
||||
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
||||
summary_df.to_excel(writer, sheet_name="Report")
|
||||
detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
||||
|
||||
@@ -135,7 +135,7 @@ class SubmissionFormContainer(QWidget):
|
||||
info = dlg.parse_form()
|
||||
logger.debug(f"Reagent info: {info}")
|
||||
# create reagent object
|
||||
reagent = PydReagent(ctx=self.app.ctx, **info)
|
||||
reagent = PydReagent(ctx=self.app.ctx, **info, missing=False)
|
||||
# send reagent to db
|
||||
sqlobj, result = reagent.toSQL()
|
||||
sqlobj.save()
|
||||
@@ -150,17 +150,18 @@ class SubmissionFormWidget(QWidget):
|
||||
# self.report = Report()
|
||||
self.app = parent.app
|
||||
self.pyd = submission
|
||||
# self.input = [{k:v} for k,v in kwargs.items()]
|
||||
# self.samples = []
|
||||
self.missing_info = []
|
||||
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment',
|
||||
'equipment', 'gel_controls', 'id', 'cost', 'extraction_info',
|
||||
'controls', 'pcr_info', 'gel_info', 'gel_image']
|
||||
self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
|
||||
st = SubmissionType.query(name=self.pyd.submission_type['value']).get_submission_class()
|
||||
defaults = st.get_default_info("form_recover", "form_ignore")
|
||||
self.recover = defaults['form_recover']
|
||||
self.ignore = defaults['form_ignore']
|
||||
# self.ignore += self.recover
|
||||
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
|
||||
self.layout = QVBoxLayout()
|
||||
# for k, v in kwargs.items():
|
||||
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
||||
if k not in self.ignore:
|
||||
if k in self.ignore:
|
||||
continue
|
||||
try:
|
||||
value = self.pyd.__getattribute__(k)
|
||||
except AttributeError:
|
||||
@@ -171,24 +172,7 @@ class SubmissionFormWidget(QWidget):
|
||||
self.layout.addWidget(add_widget)
|
||||
if k == "extraction_kit":
|
||||
add_widget.input.currentTextChanged.connect(self.scrape_reagents)
|
||||
# else:
|
||||
# self.__setattr__(k, v)
|
||||
# self.scrape_reagents(self.extraction_kit['value'])
|
||||
self.scrape_reagents(self.pyd.extraction_kit)
|
||||
# extraction kit must be added last so widget order makes sense.
|
||||
# self.layout.addWidget(self.create_widget(key="extraction_kit", value=self.extraction_kit, submission_type=self.submission_type))
|
||||
# if hasattr(self.pyd, "csv"):
|
||||
# export_csv_btn = QPushButton("Export CSV")
|
||||
# export_csv_btn.setObjectName("export_csv_btn")
|
||||
# self.layout.addWidget(export_csv_btn)
|
||||
# export_csv_btn.clicked.connect(self.export_csv_function)
|
||||
# submit_btn = QPushButton("Submit")
|
||||
# submit_btn.setObjectName("submit_btn")
|
||||
# self.layout.addWidget(submit_btn)
|
||||
# submit_btn.clicked.connect(self.submit_new_sample_function)
|
||||
# self.setLayout(self.layout)
|
||||
# self.app.report.add_result(self.report)
|
||||
# self.app.result_reporter()
|
||||
|
||||
def create_widget(self, key:str, value:dict|PydReagent, submission_type:str|None=None, extraction_kit:str|None=None) -> "self.InfoItem":
|
||||
"""
|
||||
@@ -633,6 +617,11 @@ class SubmissionFormWidget(QWidget):
|
||||
self.reagent = reagent
|
||||
self.extraction_kit = extraction_kit
|
||||
layout = QVBoxLayout()
|
||||
# layout = QGridLayout()
|
||||
# self.check_box = QCheckBox(self)
|
||||
# self.check_box.setChecked(True)
|
||||
# self.check_box.stateChanged.connect(self.check_uncheck)
|
||||
# layout.addWidget(self.check_box, 0,0)
|
||||
self.label = self.ReagentParsedLabel(reagent=reagent)
|
||||
layout.addWidget(self.label)
|
||||
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
||||
@@ -645,6 +634,14 @@ class SubmissionFormWidget(QWidget):
|
||||
# If changed set self.missing to True and update self.label
|
||||
self.lot.currentTextChanged.connect(self.updated)
|
||||
|
||||
# def check_uncheck(self):
|
||||
# if self.check_box.isChecked():
|
||||
# self.lot.setCurrentIndex(0)
|
||||
# self.lot.setEnabled(True)
|
||||
# else:
|
||||
# self.lot.setCurrentText("Not Applicable")
|
||||
# self.lot.setEnabled(False)
|
||||
|
||||
def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||
"""
|
||||
Pulls form info into PydReagent
|
||||
@@ -652,6 +649,8 @@ class SubmissionFormWidget(QWidget):
|
||||
Returns:
|
||||
Tuple[PydReagent, dict]: PydReagent and Report(?)
|
||||
"""
|
||||
# if not self.check_box.isChecked():
|
||||
# return None, None
|
||||
lot = self.lot.currentText()
|
||||
logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
|
||||
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
||||
@@ -671,7 +670,7 @@ class SubmissionFormWidget(QWidget):
|
||||
rt = ReagentType.query(name=self.reagent.type)
|
||||
if rt == None:
|
||||
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
|
||||
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, missing=False), None
|
||||
|
||||
def updated(self):
|
||||
"""
|
||||
@@ -736,8 +735,11 @@ class SubmissionFormWidget(QWidget):
|
||||
looked_up_reg = None
|
||||
# logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
|
||||
if looked_up_reg != None:
|
||||
try:
|
||||
relevant_reagents.remove(str(looked_up_reg.lot))
|
||||
relevant_reagents.insert(0, str(looked_up_reg.lot))
|
||||
except ValueError as e:
|
||||
logger.error(f"Error reordering relevant reagents: {e}")
|
||||
else:
|
||||
if len(relevant_reagents) > 1:
|
||||
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{% endif %}
|
||||
{% if sub['export_map'] %}
|
||||
<h3><u>Plate map:</u></h3>
|
||||
<img height="300px" width="650px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}">
|
||||
<img height="600px" width="1300px" src="data:image/jpeg;base64,{{ sub['export_map'] | safe }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% if signing_permission %}
|
||||
|
||||
@@ -14,6 +14,11 @@ from sqlalchemy import create_engine
|
||||
from pydantic import field_validator, BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import Any, Tuple, Literal, List
|
||||
from PyQt6.QtGui import QTextDocument, QPageSize
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
# from PyQt6 import QtPrintSupport, QtCore, QtWebEngineWidgets
|
||||
from PyQt6.QtPrintSupport import QPrinter
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -535,6 +540,18 @@ class Report(BaseModel):
|
||||
def rreplace(s, old, new):
|
||||
return (s[::-1].replace(old[::-1],new[::-1], 1))[::-1]
|
||||
|
||||
def html_to_pdf(html, output_file:Path|str):
|
||||
if isinstance(output_file, str):
|
||||
output_file = Path(output_file)
|
||||
# document = QTextDocument()
|
||||
document = QWebEngineView()
|
||||
document.setHtml(html)
|
||||
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
|
||||
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
|
||||
printer.setOutputFileName(output_file.absolute().__str__())
|
||||
printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
|
||||
document.print(printer)
|
||||
|
||||
ctx = get_config(None)
|
||||
|
||||
def is_power_user() -> bool:
|
||||
|
||||
Reference in New Issue
Block a user