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
|
## 202404.03
|
||||||
|
|
||||||
- Package updates.
|
- 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.
|
- [ ] Fix Parsed/Missing mix ups.
|
||||||
- [x] Have sample parser check for controls and add to reagents?
|
- [x] Have sample parser check for controls and add to reagents?
|
||||||
- [x] Update controls to NestedMutableJson
|
- [x] Update controls to NestedMutableJson
|
||||||
@@ -6,7 +9,7 @@
|
|||||||
- Possibly due to immutable JSON? But... it's worked before... Right?
|
- 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.
|
- 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.
|
- 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.
|
- [x] Artic not creating right plate name.
|
||||||
- [ ] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass
|
- [ ] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass
|
||||||
- [x] Fix updating of Extraction Kit in submission form widget.
|
- [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
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# 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\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]
|
[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:
|
except (TypeError, AttributeError) as e:
|
||||||
place_holder = date.today()
|
place_holder = date.today()
|
||||||
logger.debug(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing")
|
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(
|
return dict(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
type=rtype,
|
type=rtype,
|
||||||
lot=self.lot,
|
lot=self.lot,
|
||||||
expiry=place_holder.strftime("%Y-%m-%d")
|
expiry=place_holder
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_last_used(self, kit:KitType) -> Report:
|
def update_last_used(self, kit:KitType) -> Report:
|
||||||
@@ -410,6 +414,7 @@ class Reagent(BaseClass):
|
|||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
|
id:int|None=None,
|
||||||
reagent_type:str|ReagentType|None=None,
|
reagent_type:str|ReagentType|None=None,
|
||||||
lot_number:str|None=None,
|
lot_number:str|None=None,
|
||||||
name: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.
|
models.Reagent | List[models.Reagent]: reagent or list of reagents matching filter.
|
||||||
"""
|
"""
|
||||||
query: Query = cls.__database_session__.query(cls)
|
query: Query = cls.__database_session__.query(cls)
|
||||||
|
match id:
|
||||||
|
case int():
|
||||||
|
query = query.filter(cls.id==id)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
match reagent_type:
|
match reagent_type:
|
||||||
case str():
|
case str():
|
||||||
# logger.debug(f"Looking up reagents by reagent type str: {reagent_type}")
|
# 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
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
name = Column(String(128), unique=True) #: name of submission type
|
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.
|
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.
|
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
|
||||||
template_file = Column(BLOB) #: Blank form for this type stored as binary.
|
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.
|
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")
|
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 ]))
|
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
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class Organization(BaseClass):
|
|||||||
pass
|
pass
|
||||||
match name:
|
match name:
|
||||||
case str():
|
case str():
|
||||||
# logger.debug(f"Looking up organization with name: {name}")
|
# logger.debug(f"Looking up organization with name starting with: {name}")
|
||||||
query = query.filter(cls.name==name)
|
query = query.filter(cls.name.startswith(name))
|
||||||
limit = 1
|
limit = 1
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ Models for the main submission types.
|
|||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
import logging, uuid, tempfile, re, yaml, base64
|
import logging, uuid, tempfile, re, yaml, base64, sys
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from reportlab.graphics.barcode import createBarcodeImageInMemory
|
# from reportlab.graphics.barcode import createBarcodeImageInMemory
|
||||||
from reportlab.graphics.shapes import Drawing
|
# from reportlab.graphics.shapes import Drawing
|
||||||
from reportlab.lib.units import mm
|
# from reportlab.lib.units import mm
|
||||||
from operator import attrgetter, itemgetter
|
from operator import attrgetter, itemgetter
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from . import BaseClass, Reagent, SubmissionType, KitType, Organization
|
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 import relationship, validates, Query
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
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 sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
|
||||||
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||||
import pandas as pd
|
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
|
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.
|
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.
|
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
|
comment = Column(JSON) #: user notes
|
||||||
submission_category = Column(String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name
|
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(
|
submission_sample_associations = relationship(
|
||||||
"SubmissionSampleAssociation",
|
"SubmissionSampleAssociation",
|
||||||
@@ -103,12 +101,59 @@ class BasicSubmission(BaseClass):
|
|||||||
return f"{submission_type}Submission({self.rsl_plate_num})"
|
return f"{submission_type}Submission({self.rsl_plate_num})"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def jsons(cls):
|
def jsons(cls) -> List[str]:
|
||||||
output = [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
|
output = [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
|
||||||
if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission":
|
if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission":
|
||||||
output += BasicSubmission.jsons()
|
output += BasicSubmission.jsons()
|
||||||
return output
|
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:
|
def to_dict(self, full_data:bool=False, backup:bool=False, report:bool=False) -> dict:
|
||||||
"""
|
"""
|
||||||
Constructs dictionary used in submissions summary
|
Constructs dictionary used in submissions summary
|
||||||
@@ -168,6 +213,11 @@ class BasicSubmission(BaseClass):
|
|||||||
logger.debug(f"Attempting reagents.")
|
logger.debug(f"Attempting reagents.")
|
||||||
try:
|
try:
|
||||||
reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.submission_reagent_associations]
|
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:
|
except Exception as e:
|
||||||
logger.error(f"We got an error retrieving reagents: {e}")
|
logger.error(f"We got an error retrieving reagents: {e}")
|
||||||
reagents = None
|
reagents = None
|
||||||
@@ -181,10 +231,12 @@ class BasicSubmission(BaseClass):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting equipment: {e}")
|
logger.error(f"Error setting equipment: {e}")
|
||||||
equipment = None
|
equipment = None
|
||||||
|
cost_centre = self.cost_centre
|
||||||
else:
|
else:
|
||||||
reagents = None
|
reagents = None
|
||||||
samples = None
|
samples = None
|
||||||
equipment = None
|
equipment = None
|
||||||
|
cost_centre = None
|
||||||
# logger.debug("Getting comments")
|
# logger.debug("Getting comments")
|
||||||
try:
|
try:
|
||||||
comments = self.comment
|
comments = self.comment
|
||||||
@@ -198,6 +250,8 @@ class BasicSubmission(BaseClass):
|
|||||||
output["extraction_info"] = ext_info
|
output["extraction_info"] = ext_info
|
||||||
output["comment"] = comments
|
output["comment"] = comments
|
||||||
output["equipment"] = equipment
|
output["equipment"] = equipment
|
||||||
|
output["Cost Centre"] = cost_centre
|
||||||
|
output["Signed By"] = self.signed_by
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def calculate_column_count(self) -> int:
|
def calculate_column_count(self) -> int:
|
||||||
@@ -293,18 +347,18 @@ class BasicSubmission(BaseClass):
|
|||||||
"""
|
"""
|
||||||
return [item.role for item in self.submission_equipment_associations]
|
return [item.role for item in self.submission_equipment_associations]
|
||||||
|
|
||||||
def make_plate_barcode(self, width:int=100, height:int=25) -> Drawing:
|
# def make_plate_barcode(self, width:int=100, height:int=25) -> Drawing:
|
||||||
"""
|
# """
|
||||||
Creates a barcode image for this BasicSubmission.
|
# Creates a barcode image for this BasicSubmission.
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
width (int, optional): Width (pixels) of image. Defaults to 100.
|
# width (int, optional): Width (pixels) of image. Defaults to 100.
|
||||||
height (int, optional): Height (pixels) of image. Defaults to 25.
|
# height (int, optional): Height (pixels) of image. Defaults to 25.
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
Drawing: image object
|
# Drawing: image object
|
||||||
"""
|
# """
|
||||||
return createBarcodeImageInMemory('Code128', value=self.rsl_plate_num, width=width*mm, height=height*mm, humanReadable=True, format="png")
|
# return createBarcodeImageInMemory('Code128', value=self.rsl_plate_num, width=width*mm, height=height*mm, humanReadable=True, format="png")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def submissions_to_df(cls, submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
|
def submissions_to_df(cls, submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
|
||||||
@@ -384,13 +438,19 @@ class BasicSubmission(BaseClass):
|
|||||||
case item if item in self.jsons():
|
case item if item in self.jsons():
|
||||||
logger.debug(f"Setting JSON attribute.")
|
logger.debug(f"Setting JSON attribute.")
|
||||||
existing = self.__getattribute__(key)
|
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:
|
if existing is None:
|
||||||
existing = []
|
existing = []
|
||||||
if value in existing:
|
if value in existing:
|
||||||
logger.warning("Value already exists. Preventing duplicate addition.")
|
logger.warning("Value already exists. Preventing duplicate addition.")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
existing.append(value)
|
if isinstance(value, list):
|
||||||
|
existing += value
|
||||||
|
else:
|
||||||
|
existing.append(value)
|
||||||
self.__setattr__(key, existing)
|
self.__setattr__(key, existing)
|
||||||
flag_modified(self, key)
|
flag_modified(self, key)
|
||||||
return
|
return
|
||||||
@@ -634,7 +694,7 @@ class BasicSubmission(BaseClass):
|
|||||||
from backend.validators import RSLNamer
|
from backend.validators import RSLNamer
|
||||||
logger.debug(f"instr coming into {cls}: {instr}")
|
logger.debug(f"instr coming into {cls}: {instr}")
|
||||||
logger.debug(f"data coming into {cls}: {data}")
|
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']
|
data['abbreviation'] = defaults['abbreviation']
|
||||||
if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]:
|
if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]:
|
||||||
data['submission_type'] = defaults['submission_type']
|
data['submission_type'] = defaults['submission_type']
|
||||||
@@ -737,9 +797,7 @@ class BasicSubmission(BaseClass):
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||||
"""
|
"""
|
||||||
base_dict['excluded'] = ['excluded', 'reagents', 'samples', 'controls',
|
base_dict['excluded'] = cls.get_default_info('details_ignore')
|
||||||
'extraction_info', 'pcr_info', 'comment',
|
|
||||||
'barcode', 'platemap', 'export_map', 'equipment']
|
|
||||||
env = jinja_template_loading()
|
env = jinja_template_loading()
|
||||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||||
logger.debug(f"Returning template: {temp_name}")
|
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]
|
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@classmethod
|
# @classmethod
|
||||||
def get_default_info(cls) -> dict:
|
# def get_default_info(cls) -> dict:
|
||||||
return dict(abbreviation="BC", submission_type="Bacterial Culture")
|
# return dict(abbreviation="BC", submission_type="Bacterial Culture")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
|
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
|
output['pcr_info'] = self.pcr_info
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
pass
|
pass
|
||||||
ext_tech = self.ext_technician or self.technician
|
if self.ext_technician is None or self.ext_technician == "None":
|
||||||
pcr_tech = self.pcr_technician or self.technician
|
output['Ext Technician'] = self.technician
|
||||||
output['Technician'] = f"Enr: {self.technician}, Ext: {ext_tech}, PCR: {pcr_tech}"
|
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
|
return output
|
||||||
|
|
||||||
@classmethod
|
# @classmethod
|
||||||
def get_default_info(cls) -> dict:
|
# def get_default_info(cls) -> dict:
|
||||||
return dict(abbreviation="WW", submission_type="Wastewater")
|
return dict(abbreviation="WW", submission_type="Wastewater")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1334,36 +1398,6 @@ class Wastewater(BasicSubmission):
|
|||||||
from frontend.widgets import select_open_file
|
from frontend.widgets import select_open_file
|
||||||
fname = select_open_file(obj=obj, file_extension="xlsx")
|
fname = select_open_file(obj=obj, file_extension="xlsx")
|
||||||
parser = PCRParser(filepath=fname)
|
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.set_attribute("pcr_info", parser.pcr)
|
||||||
self.save(original=False)
|
self.save(original=False)
|
||||||
logger.debug(f"Got {len(parser.samples)} samples to update!")
|
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
|
gel_controls = Column(JSON) #: locations of controls on the gel
|
||||||
source_plates = Column(JSON) #: wastewater plates that samples come from
|
source_plates = Column(JSON) #: wastewater plates that samples come from
|
||||||
|
|
||||||
|
|
||||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
||||||
polymorphic_load="inline",
|
polymorphic_load="inline",
|
||||||
inherit_condition=(id == BasicSubmission.id))
|
inherit_condition=(id == BasicSubmission.id))
|
||||||
@@ -1411,9 +1444,9 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
output['source_plates'] = self.source_plates
|
output['source_plates'] = self.source_plates
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@classmethod
|
# @classmethod
|
||||||
def get_default_info(cls) -> str:
|
# def get_default_info(cls) -> str:
|
||||||
return dict(abbreviation="AR", submission_type="Wastewater Artic")
|
# return dict(abbreviation="AR", submission_type="Wastewater Artic")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
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
|
# from backend.validators import RSLNamer
|
||||||
input_dict = super().parse_info(input_dict)
|
input_dict = super().parse_info(input_dict)
|
||||||
ws = load_workbook(xl.io, data_only=True)['Egel results']
|
workbook = load_workbook(xl.io, data_only=True)
|
||||||
data = [ws.cell(row=jj,column=ii) for ii in range(15,27) for jj in range(10,18)]
|
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]
|
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]
|
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 = xl.parse("Egel results").iloc[7:16, 13:26]
|
||||||
# df = df.set_index(df.columns[0])
|
# 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
|
return input_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1500,34 +1537,38 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
Returns:
|
Returns:
|
||||||
str: output name
|
str: output name
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"input string raw: {input_str}")
|
||||||
# Remove letters.
|
# Remove letters.
|
||||||
processed = re.sub(r"[A-Z]", "", input_str)
|
processed = re.sub(r"[A-QS-Z]+\d*", "", input_str)
|
||||||
# Remove trailing '-' if any
|
# Remove trailing '-' if any
|
||||||
processed = processed.strip("-")
|
processed = processed.strip("-")
|
||||||
|
logger.debug(f"Processed after stripping letters: {processed}")
|
||||||
try:
|
try:
|
||||||
en_num = re.search(r"\-\d{1}$", processed).group()
|
en_num = re.search(r"\-\d{1}$", processed).group()
|
||||||
processed = rreplace(processed, en_num, "")
|
processed = rreplace(processed, en_num, "")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
en_num = "1"
|
en_num = "1"
|
||||||
en_num = en_num.strip("-")
|
en_num = en_num.strip("-")
|
||||||
# logger.debug(f"Processed after en-num: {processed}")
|
logger.debug(f"Processed after en-num: {processed}")
|
||||||
try:
|
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, "")
|
processed = rreplace(processed, plate_num, "")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
plate_num = "1"
|
plate_num = "1"
|
||||||
plate_num = plate_num.strip("-")
|
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()
|
day = re.search(r"\d{2}$", processed).group()
|
||||||
processed = rreplace(processed, day, "")
|
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()
|
month = re.search(r"\d{2}$", processed).group()
|
||||||
processed = rreplace(processed, month, "")
|
processed = rreplace(processed, month, "")
|
||||||
processed = processed.replace("--", "")
|
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 = re.search(r'^(?:\d{2})?\d{2}', processed).group()
|
||||||
year = f"20{year}"
|
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
|
@classmethod
|
||||||
def get_regex(cls) -> str:
|
def get_regex(cls) -> str:
|
||||||
@@ -1599,7 +1640,23 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
# for jjj, value in enumerate(plate, start=3):
|
# for jjj, value in enumerate(plate, start=3):
|
||||||
# worksheet.cell(row=iii, column=jjj, value=value)
|
# worksheet.cell(row=iii, column=jjj, value=value)
|
||||||
logger.debug(f"Info:\n{pformat(info)}")
|
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:
|
if check:
|
||||||
# logger.debug(f"Gel info check passed.")
|
# logger.debug(f"Gel info check passed.")
|
||||||
if info['gel_info'] != None:
|
if info['gel_info'] != None:
|
||||||
@@ -1618,7 +1675,7 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
column = start_column + 2 + jjj
|
column = start_column + 2 + jjj
|
||||||
worksheet.cell(row=start_row, column=column, value=kj['name'])
|
worksheet.cell(row=start_row, column=column, value=kj['name'])
|
||||||
worksheet.cell(row=row, column=column, value=kj['value'])
|
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 check:
|
||||||
if info['gel_image'] != None:
|
if info['gel_image'] != None:
|
||||||
worksheet = input_excel['Egel results']
|
worksheet = input_excel['Egel results']
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ class SheetParser(object):
|
|||||||
parser = InfoParser(xl=self.xl, submission_type=self.sub['submission_type']['value'])
|
parser = InfoParser(xl=self.xl, submission_type=self.sub['submission_type']['value'])
|
||||||
info = parser.parse_info()
|
info = parser.parse_info()
|
||||||
self.info_map = parser.map
|
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():
|
for k,v in info.items():
|
||||||
match k:
|
match k:
|
||||||
case "sample":
|
case "sample":
|
||||||
|
# case item if
|
||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
self.sub[k] = v
|
self.sub[k] = v
|
||||||
@@ -97,9 +99,9 @@ class SheetParser(object):
|
|||||||
"""
|
"""
|
||||||
Enforce that the parser has an extraction kit
|
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']):
|
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():
|
if dlg.exec():
|
||||||
self.sub['extraction_kit'] = dict(value=dlg.getValues(), missing=True)
|
self.sub['extraction_kit'] = dict(value=dlg.getValues(), missing=True)
|
||||||
else:
|
else:
|
||||||
@@ -133,7 +135,11 @@ class SheetParser(object):
|
|||||||
"""
|
"""
|
||||||
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
|
# logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
|
||||||
logger.debug(f"Equipment: {self.sub['equipment']}")
|
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
|
self.sub['equipment'] = None
|
||||||
psm = PydSubmission(filepath=self.filepath, **self.sub)
|
psm = PydSubmission(filepath=self.filepath, **self.sub)
|
||||||
return psm
|
return psm
|
||||||
@@ -142,11 +148,12 @@ class InfoParser(object):
|
|||||||
|
|
||||||
def __init__(self, xl:pd.ExcelFile, submission_type:str):
|
def __init__(self, xl:pd.ExcelFile, submission_type:str):
|
||||||
logger.info(f"\n\Hello from InfoParser!\n\n")
|
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
|
self.xl = xl
|
||||||
logger.debug(f"Info map for InfoParser: {pformat(self.map)}")
|
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.
|
Gets location of basic info from the submission_type object in the database.
|
||||||
|
|
||||||
@@ -156,10 +163,10 @@ class InfoParser(object):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Location map of all info for this submission type
|
dict: Location map of all info for this submission type
|
||||||
"""
|
"""
|
||||||
if isinstance(submission_type, str):
|
if isinstance(self.submission_type, str):
|
||||||
submission_type = dict(value=submission_type, missing=True)
|
self.submission_type = dict(value=self.submission_type, missing=True)
|
||||||
logger.debug(f"Looking up submission type: {submission_type['value']}")
|
logger.debug(f"Looking up submission type: {self.submission_type['value']}")
|
||||||
submission_type = SubmissionType.query(name=submission_type['value'])
|
submission_type = SubmissionType.query(name=self.submission_type['value'])
|
||||||
info_map = submission_type.info_map
|
info_map = submission_type.info_map
|
||||||
# Get the parse_info method from the submission type specified
|
# Get the parse_info method from the submission type specified
|
||||||
self.custom_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name).parse_info
|
self.custom_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name).parse_info
|
||||||
@@ -172,16 +179,25 @@ class InfoParser(object):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: key:value of basic info
|
dict: key:value of basic info
|
||||||
"""
|
"""
|
||||||
|
if isinstance(self.submission_type, str):
|
||||||
|
self.submission_type = dict(value=self.submission_type, missing=True)
|
||||||
dicto = {}
|
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:
|
for sheet in self.xl.sheet_names:
|
||||||
df = self.xl.parse(sheet, header=None)
|
df = self.xl.parse(sheet, header=None)
|
||||||
relevant = {}
|
relevant = {}
|
||||||
for k, v in self.map.items():
|
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):
|
if isinstance(v, str):
|
||||||
dicto[k] = dict(value=v, missing=False)
|
dicto[k] = dict(value=v, missing=False)
|
||||||
continue
|
continue
|
||||||
if k in ["samples", "all_sheets"]:
|
logger.debug(f"Looking for {k} in self.map")
|
||||||
continue
|
|
||||||
if sheet in self.map[k]['sheets']:
|
if sheet in self.map[k]['sheets']:
|
||||||
relevant[k] = v
|
relevant[k] = v
|
||||||
logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
|
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]
|
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]
|
expiry = df.iat[relevant[item]['expiry']['row']-1, relevant[item]['expiry']['column']-1]
|
||||||
if 'comment' in relevant[item].keys():
|
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]
|
comment = df.iat[relevant[item]['comment']['row']-1, relevant[item]['comment']['column']-1]
|
||||||
else:
|
else:
|
||||||
comment = ""
|
comment = ""
|
||||||
@@ -294,7 +311,7 @@ class SampleParser(object):
|
|||||||
sample_info_map = self.fetch_sample_info_map(submission_type=submission_type, sample_map=sample_map)
|
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}")
|
logger.debug(f"sample_info_map: {sample_info_map}")
|
||||||
self.plate_map = self.construct_plate_map(plate_map_location=sample_info_map['plate_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'])
|
self.lookup_table = self.construct_lookup_table(lookup_table_location=sample_info_map['lookup_table'])
|
||||||
if "plates" in sample_info_map:
|
if "plates" in sample_info_map:
|
||||||
self.plates = sample_info_map['plates']
|
self.plates = sample_info_map['plates']
|
||||||
@@ -439,7 +456,7 @@ class SampleParser(object):
|
|||||||
"""
|
"""
|
||||||
result = None
|
result = None
|
||||||
new_samples = []
|
new_samples = []
|
||||||
logger.debug(f"Starting samples: {pformat(self.samples)}")
|
# logger.debug(f"Starting samples: {pformat(self.samples)}")
|
||||||
for sample in self.samples:
|
for sample in self.samples:
|
||||||
translated_dict = {}
|
translated_dict = {}
|
||||||
for k, v in sample.items():
|
for k, v in sample.items():
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class RSLNamer(object):
|
|||||||
check = True
|
check = True
|
||||||
if check:
|
if check:
|
||||||
# logger.debug("Final option, ask the user for submission type")
|
# logger.debug("Final option, ask the user for submission type")
|
||||||
from frontend.widgets import SubmissionTypeSelector
|
from frontend.widgets import ObjectSelector
|
||||||
dlg = SubmissionTypeSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.")
|
dlg = ObjectSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.", obj_type=SubmissionType)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
submission_type = dlg.parse_form()
|
submission_type = dlg.parse_form()
|
||||||
submission_type = submission_type.replace("_", " ")
|
submission_type = submission_type.replace("_", " ")
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ from pydantic import BaseModel, field_validator, Field
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from dateutil.parser._parser import ParserError
|
from dateutil.parser._parser import ParserError
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple, Literal
|
||||||
from . import RSLNamer
|
from . import RSLNamer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools import check_not_nan, convert_nans_to_nones, Report, Result, row_map
|
from tools import check_not_nan, convert_nans_to_nones, Report, Result, row_map
|
||||||
from backend.db.models import *
|
from backend.db.models import *
|
||||||
from sqlalchemy.exc import StatementError, IntegrityError
|
from sqlalchemy.exc import StatementError, IntegrityError
|
||||||
from PyQt6.QtWidgets import QComboBox, QWidget
|
from PyQt6.QtWidgets import QWidget
|
||||||
from openpyxl import load_workbook, Workbook
|
from openpyxl import load_workbook, Workbook
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class PydReagent(BaseModel):
|
|||||||
|
|
||||||
lot: str|None
|
lot: str|None
|
||||||
type: str|None
|
type: str|None
|
||||||
expiry: date|None
|
expiry: date|Literal['NA']|None
|
||||||
name: str|None
|
name: str|None
|
||||||
missing: bool = Field(default=True)
|
missing: bool = Field(default=True)
|
||||||
comment: str|None = Field(default="", validate_default=True)
|
comment: str|None = Field(default="", validate_default=True)
|
||||||
@@ -77,6 +77,8 @@ class PydReagent(BaseModel):
|
|||||||
match value:
|
match value:
|
||||||
case int():
|
case int():
|
||||||
return datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value - 2).date()
|
return datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value - 2).date()
|
||||||
|
case 'NA':
|
||||||
|
return value
|
||||||
case str():
|
case str():
|
||||||
return parse(value)
|
return parse(value)
|
||||||
case date():
|
case date():
|
||||||
@@ -87,6 +89,13 @@ class PydReagent(BaseModel):
|
|||||||
value = date.today()
|
value = date.today()
|
||||||
return value
|
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")
|
@field_validator("name", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def enforce_name(cls, value, values):
|
def enforce_name(cls, value, values):
|
||||||
@@ -125,6 +134,10 @@ class PydReagent(BaseModel):
|
|||||||
reagent.type.append(reagent_type)
|
reagent.type.append(reagent_type)
|
||||||
case "comment":
|
case "comment":
|
||||||
continue
|
continue
|
||||||
|
case "expiry":
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = date(year=1970, month=1, day=1)
|
||||||
|
reagent.expiry = value
|
||||||
case _:
|
case _:
|
||||||
try:
|
try:
|
||||||
reagent.__setattr__(key, value)
|
reagent.__setattr__(key, value)
|
||||||
@@ -271,6 +284,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
reagents: List[dict]|List[PydReagent] = []
|
reagents: List[dict]|List[PydReagent] = []
|
||||||
samples: List[PydSample]
|
samples: List[PydSample]
|
||||||
equipment: List[PydEquipment]|None =[]
|
equipment: List[PydEquipment]|None =[]
|
||||||
|
cost_centre: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
||||||
|
|
||||||
@field_validator('equipment', mode='before')
|
@field_validator('equipment', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -332,10 +346,28 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
@field_validator("submitting_lab", mode="before")
|
@field_validator("submitting_lab", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def rescue_submitting_lab(cls, value):
|
def rescue_submitting_lab(cls, value):
|
||||||
if value == None:
|
if value is None:
|
||||||
return dict(value=None, missing=True)
|
return dict(value=None, missing=True)
|
||||||
return value
|
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')
|
@field_validator("rsl_plate_num", mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def rescue_rsl_number(cls, value):
|
def rescue_rsl_number(cls, value):
|
||||||
@@ -427,6 +459,30 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
output.append(sample)
|
output.append(sample)
|
||||||
return output
|
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):
|
def set_attribute(self, key, value):
|
||||||
self.__setattr__(name=key, value=value)
|
self.__setattr__(name=key, value=value)
|
||||||
|
|
||||||
@@ -599,6 +655,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
else:
|
else:
|
||||||
info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
|
info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
|
||||||
reagents = self.reagents
|
reagents = self.reagents
|
||||||
|
|
||||||
if len(reagents + list(info.keys())) == 0:
|
if len(reagents + list(info.keys())) == 0:
|
||||||
# logger.warning("No info to fill in, returning")
|
# logger.warning("No info to fill in, returning")
|
||||||
return None
|
return None
|
||||||
@@ -616,14 +673,14 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
new_reagent = {}
|
new_reagent = {}
|
||||||
new_reagent['type'] = reagent.type
|
new_reagent['type'] = reagent.type
|
||||||
new_reagent['lot'] = excel_map[new_reagent['type']]['lot']
|
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'] = 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']
|
new_reagent['sheet'] = excel_map[new_reagent['type']]['sheet']
|
||||||
# name is only present for Bacterial Culture
|
# name is only present for Bacterial Culture
|
||||||
try:
|
try:
|
||||||
new_reagent['name'] = excel_map[new_reagent['type']]['name']
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Couldn't get name due to {e}")
|
logger.error(f"Couldn't get name due to {e}")
|
||||||
new_reagents.append(new_reagent)
|
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']}")
|
# 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'])
|
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']}")
|
# 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'])
|
worksheet.cell(row=reagent['expiry']['row'], column=reagent['expiry']['column'], value=reagent['expiry']['value'])
|
||||||
try:
|
try:
|
||||||
# logger.debug(f"Attempting to write name {reagent['name']['value']} in: row {reagent['name']['row']}, column {reagent['name']['column']}")
|
# 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)}")
|
logger.debug(f"Extraction kit: {extraction_kit}. Is it a string? {isinstance(extraction_kit, str)}")
|
||||||
if isinstance(extraction_kit, str):
|
if isinstance(extraction_kit, str):
|
||||||
extraction_kit = dict(value=extraction_kit)
|
extraction_kit = dict(value=extraction_kit)
|
||||||
if extraction_kit is not None:
|
if extraction_kit is not None and extraction_kit != self.extraction_kit['value']:
|
||||||
if extraction_kit != self.extraction_kit['value']:
|
|
||||||
self.extraction_kit['value'] = extraction_kit['value']
|
self.extraction_kit['value'] = extraction_kit['value']
|
||||||
reagenttypes = []
|
# reagenttypes = []
|
||||||
else:
|
# else:
|
||||||
reagenttypes = [item.type for item in self.reagents]
|
# reagenttypes = [item.type for item in self.reagents]
|
||||||
else:
|
# else:
|
||||||
reagenttypes = [item.type for item in self.reagents]
|
# reagenttypes = [item.type for item in self.reagents]
|
||||||
logger.debug(f"Looking up {self.extraction_kit['value']}")
|
logger.debug(f"Looking up {self.extraction_kit['value']}")
|
||||||
ext_kit = KitType.query(name=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'])]
|
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}")
|
# logger.debug(f"Checking if reagents match kit contents: {check}")
|
||||||
# # what reagent types are in both lists?
|
# # what reagent types are in both lists?
|
||||||
# missing = list(set(ext_kit_rtypes).difference(reagenttypes))
|
# missing = list(set(ext_kit_rtypes).difference(reagenttypes))
|
||||||
missing = []
|
# missing = []
|
||||||
output_reagents = self.reagents
|
# Exclude any reagenttype found in this pyd not expected in kit.
|
||||||
# output_reagents = ext_kit_rtypes
|
expected_check = [item.type for item in ext_kit_rtypes]
|
||||||
logger.debug(f"Already have these reagent types: {reagenttypes}")
|
output_reagents = [rt for rt in self.reagents if rt.type in expected_check]
|
||||||
for rt in ext_kit_rtypes:
|
logger.debug(f"Already have these reagent types: {output_reagents}")
|
||||||
if rt.type not in reagenttypes:
|
missing_check = [item.type for item in output_reagents]
|
||||||
missing.append(rt)
|
missing_reagents = [rt for rt in ext_kit_rtypes if rt.type not in missing_check]
|
||||||
if rt.type not in [item.type for item in output_reagents]:
|
missing_reagents += [rt for rt in output_reagents if rt.missing]
|
||||||
output_reagents.append(rt)
|
# for rt in ext_kit_rtypes:
|
||||||
logger.debug(f"Missing reagents types: {missing}")
|
# 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 lists are equal return no problem
|
||||||
if len(missing)==0:
|
if len(missing_reagents)==0:
|
||||||
result = None
|
result = None
|
||||||
else:
|
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)
|
report.add_result(result)
|
||||||
return output_reagents, report
|
return output_reagents, report
|
||||||
|
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ class AddReagentForm(QDialog):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Output info
|
dict: Output info
|
||||||
"""
|
"""
|
||||||
return dict(name=self.name_input.currentText(),
|
return dict(name=self.name_input.currentText().strip(),
|
||||||
lot=self.lot_input.text(),
|
lot=self.lot_input.text().strip(),
|
||||||
expiry=self.exp_input.date().toPyDate(),
|
expiry=self.exp_input.date().toPyDate(),
|
||||||
type=self.type_input.currentText())
|
type=self.type_input.currentText().strip())
|
||||||
|
|
||||||
def update_names(self):
|
def update_names(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from tools import jinja_template_loading
|
from tools import jinja_template_loading
|
||||||
import logging
|
import logging
|
||||||
from backend.db.models import KitType, SubmissionType
|
from backend.db import models
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
@@ -45,16 +45,18 @@ class AlertPop(QMessageBox):
|
|||||||
self.setInformativeText(message)
|
self.setInformativeText(message)
|
||||||
self.setWindowTitle(f"{owner} - {status.title()}")
|
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__()
|
super().__init__()
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.widget = QComboBox()
|
self.widget = QComboBox()
|
||||||
kits = [item.name for item in KitType.query()]
|
if isinstance(obj_type, str):
|
||||||
self.widget.addItems(kits)
|
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)
|
self.widget.setEditable(False)
|
||||||
# set yes/no buttons
|
# set yes/no buttons
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
@@ -78,36 +80,69 @@ class KitSelector(QDialog):
|
|||||||
"""
|
"""
|
||||||
return self.widget.currentText()
|
return self.widget.currentText()
|
||||||
|
|
||||||
class SubmissionTypeSelector(QDialog):
|
# class KitSelector(QDialog):
|
||||||
"""
|
# """
|
||||||
dialog to input SubmissionType manually
|
# dialog to input KitType manually
|
||||||
"""
|
# """
|
||||||
def __init__(self, title:str, message:str) -> QDialog:
|
# def __init__(self, title:str, message:str) -> QDialog:
|
||||||
super().__init__()
|
# super().__init__()
|
||||||
self.setWindowTitle(title)
|
# self.setWindowTitle(title)
|
||||||
self.widget = QComboBox()
|
# self.widget = QComboBox()
|
||||||
# sub_type = [item.name for item in lookup_submission_type(ctx=ctx)]
|
# kits = [item.name for item in KitType.query()]
|
||||||
sub_type = [item.name for item in SubmissionType.query()]
|
# self.widget.addItems(kits)
|
||||||
self.widget.addItems(sub_type)
|
# self.widget.setEditable(False)
|
||||||
self.widget.setEditable(False)
|
# # set yes/no buttons
|
||||||
# set yes/no buttons
|
# QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
# self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
# self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
# self.buttonBox.rejected.connect(self.reject)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
# self.layout = QVBoxLayout()
|
||||||
self.layout = QVBoxLayout()
|
# # Text for the yes/no question
|
||||||
# Text for the yes/no question
|
# message = QLabel(message)
|
||||||
message = QLabel(message)
|
# self.layout.addWidget(message)
|
||||||
self.layout.addWidget(message)
|
# self.layout.addWidget(self.widget)
|
||||||
self.layout.addWidget(self.widget)
|
# self.layout.addWidget(self.buttonBox)
|
||||||
self.layout.addWidget(self.buttonBox)
|
# self.setLayout(self.layout)
|
||||||
self.setLayout(self.layout)
|
|
||||||
|
|
||||||
def parse_form(self) -> str:
|
# def getValues(self) -> str:
|
||||||
"""
|
# """
|
||||||
Pulls SubmissionType(str) from widget
|
# Get KitType(str) from widget
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
str: SubmissionType as str
|
# str: KitType as str
|
||||||
"""
|
# """
|
||||||
return self.widget.currentText()
|
# 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 PyQt6.QtCore import Qt, pyqtSlot
|
||||||
|
|
||||||
from backend.db.models import BasicSubmission, BasicSample
|
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 .functions import select_save_file
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from tempfile import TemporaryFile, TemporaryDirectory
|
from tempfile import TemporaryFile, TemporaryDirectory
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from xhtml2pdf import pisa
|
# from xhtml2pdf import pisa
|
||||||
import logging, base64
|
import logging, base64
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -54,6 +54,7 @@ class SubmissionDetails(QDialog):
|
|||||||
self.channel = QWebChannel()
|
self.channel = QWebChannel()
|
||||||
self.channel.registerObject('backend', self)
|
self.channel.registerObject('backend', self)
|
||||||
self.submission_details(submission=sub)
|
self.submission_details(submission=sub)
|
||||||
|
self.rsl_plate_num = sub.rsl_plate_num
|
||||||
self.webview.page().setWebChannel(self.channel)
|
self.webview.page().setWebChannel(self.channel)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@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'})}")
|
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
|
# don't want id
|
||||||
del self.base_dict['id']
|
del self.base_dict['id']
|
||||||
logger.debug(f"Creating barcode.")
|
# logger.debug(f"Creating barcode.")
|
||||||
if not check_if_app():
|
# if not check_if_app():
|
||||||
self.base_dict['barcode'] = base64.b64encode(submission.make_plate_barcode(width=120, height=30)).decode('utf-8')
|
# self.base_dict['barcode'] = base64.b64encode(submission.make_plate_barcode(width=120, height=30)).decode('utf-8')
|
||||||
logger.debug(f"Making platemap...")
|
logger.debug(f"Making platemap...")
|
||||||
self.base_dict['platemap'] = submission.make_plate_map()
|
self.base_dict['platemap'] = submission.make_plate_map()
|
||||||
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict)
|
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()})")
|
logger.debug(f"Signing off on {submission} - ({getuser()})")
|
||||||
if isinstance(submission, str):
|
if isinstance(submission, str):
|
||||||
submission = BasicSubmission.query(rsl_number=submission)
|
submission = BasicSubmission.query(rsl_number=submission)
|
||||||
submission.uploaded_by = getuser()
|
submission.signed_by = getuser()
|
||||||
submission.save()
|
submission.save()
|
||||||
|
self.submission_details(submission=self.rsl_plate_num)
|
||||||
|
|
||||||
def export(self):
|
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")
|
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
|
||||||
image_io = BytesIO()
|
image_io = BytesIO()
|
||||||
temp_dir = Path(TemporaryDirectory().name)
|
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)
|
temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name)
|
||||||
screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
|
screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
|
||||||
export_map = Image.open(screenshot[0])
|
export_map = Image.open(screenshot[0])
|
||||||
@@ -126,8 +128,9 @@ class SubmissionDetails(QDialog):
|
|||||||
del self.base_dict['platemap']
|
del self.base_dict['platemap']
|
||||||
self.html2 = self.template.render(sub=self.base_dict)
|
self.html2 = self.template.render(sub=self.base_dict)
|
||||||
try:
|
try:
|
||||||
with open(fname, "w+b") as f:
|
# with open(fname, "w+b") as f:
|
||||||
pisa.CreatePDF(self.html2, dest=f)
|
# pisa.CreatePDF(self.html2, dest=f)
|
||||||
|
html_to_pdf(html=self.html2, output_file=fname)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
logger.error(f"Error saving pdf: {e}")
|
logger.error(f"Error saving pdf: {e}")
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
|||||||
from PyQt6.QtGui import QAction, QCursor
|
from PyQt6.QtGui import QAction, QCursor
|
||||||
from backend.db.models import BasicSubmission
|
from backend.db.models import BasicSubmission
|
||||||
from backend.excel import make_report_html, make_report_xlsx
|
from backend.excel import make_report_html, make_report_xlsx
|
||||||
from tools import Report, Result, row_map, get_first_blank_df_row
|
from tools import Report, Result, row_map, get_first_blank_df_row, html_to_pdf
|
||||||
from xhtml2pdf import pisa
|
# from xhtml2pdf import pisa
|
||||||
from .functions import select_save_file, select_open_file
|
from .functions import select_save_file, select_open_file
|
||||||
from .misc import ReportDatePicker
|
from .misc import ReportDatePicker
|
||||||
import pandas as pd
|
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'])
|
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
||||||
# get save location of report
|
# 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")
|
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:
|
# with open(fname, "w+b") as f:
|
||||||
pisa.CreatePDF(html, dest=f)
|
# pisa.CreatePDF(html, dest=f)
|
||||||
|
html_to_pdf(html=html, output_file=fname)
|
||||||
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
||||||
summary_df.to_excel(writer, sheet_name="Report")
|
summary_df.to_excel(writer, sheet_name="Report")
|
||||||
detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
info = dlg.parse_form()
|
info = dlg.parse_form()
|
||||||
logger.debug(f"Reagent info: {info}")
|
logger.debug(f"Reagent info: {info}")
|
||||||
# create reagent object
|
# create reagent object
|
||||||
reagent = PydReagent(ctx=self.app.ctx, **info)
|
reagent = PydReagent(ctx=self.app.ctx, **info, missing=False)
|
||||||
# send reagent to db
|
# send reagent to db
|
||||||
sqlobj, result = reagent.toSQL()
|
sqlobj, result = reagent.toSQL()
|
||||||
sqlobj.save()
|
sqlobj.save()
|
||||||
@@ -150,45 +150,29 @@ class SubmissionFormWidget(QWidget):
|
|||||||
# self.report = Report()
|
# self.report = Report()
|
||||||
self.app = parent.app
|
self.app = parent.app
|
||||||
self.pyd = submission
|
self.pyd = submission
|
||||||
# self.input = [{k:v} for k,v in kwargs.items()]
|
|
||||||
# self.samples = []
|
|
||||||
self.missing_info = []
|
self.missing_info = []
|
||||||
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment',
|
st = SubmissionType.query(name=self.pyd.submission_type['value']).get_submission_class()
|
||||||
'equipment', 'gel_controls', 'id', 'cost', 'extraction_info',
|
defaults = st.get_default_info("form_recover", "form_ignore")
|
||||||
'controls', 'pcr_info', 'gel_info', 'gel_image']
|
self.recover = defaults['form_recover']
|
||||||
self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
|
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()
|
self.layout = QVBoxLayout()
|
||||||
# for k, v in kwargs.items():
|
# for k, v in kwargs.items():
|
||||||
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
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:
|
||||||
try:
|
continue
|
||||||
value = self.pyd.__getattribute__(k)
|
try:
|
||||||
except AttributeError:
|
value = self.pyd.__getattribute__(k)
|
||||||
logger.error(f"Couldn't get attribute from pyd: {k}")
|
except AttributeError:
|
||||||
value = dict(value=None, missing=True)
|
logger.error(f"Couldn't get attribute from pyd: {k}")
|
||||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'])
|
value = dict(value=None, missing=True)
|
||||||
if add_widget != None:
|
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'])
|
||||||
self.layout.addWidget(add_widget)
|
if add_widget != None:
|
||||||
if k == "extraction_kit":
|
self.layout.addWidget(add_widget)
|
||||||
add_widget.input.currentTextChanged.connect(self.scrape_reagents)
|
if k == "extraction_kit":
|
||||||
# else:
|
add_widget.input.currentTextChanged.connect(self.scrape_reagents)
|
||||||
# self.__setattr__(k, v)
|
|
||||||
# self.scrape_reagents(self.extraction_kit['value'])
|
|
||||||
self.scrape_reagents(self.pyd.extraction_kit)
|
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":
|
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.reagent = reagent
|
||||||
self.extraction_kit = extraction_kit
|
self.extraction_kit = extraction_kit
|
||||||
layout = QVBoxLayout()
|
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)
|
self.label = self.ReagentParsedLabel(reagent=reagent)
|
||||||
layout.addWidget(self.label)
|
layout.addWidget(self.label)
|
||||||
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
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
|
# If changed set self.missing to True and update self.label
|
||||||
self.lot.currentTextChanged.connect(self.updated)
|
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]:
|
def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||||
"""
|
"""
|
||||||
Pulls form info into PydReagent
|
Pulls form info into PydReagent
|
||||||
@@ -652,6 +649,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple[PydReagent, dict]: PydReagent and Report(?)
|
Tuple[PydReagent, dict]: PydReagent and Report(?)
|
||||||
"""
|
"""
|
||||||
|
# if not self.check_box.isChecked():
|
||||||
|
# return None, None
|
||||||
lot = self.lot.currentText()
|
lot = self.lot.currentText()
|
||||||
logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
|
logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
|
||||||
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
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)
|
rt = ReagentType.query(name=self.reagent.type)
|
||||||
if rt == None:
|
if rt == None:
|
||||||
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
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):
|
def updated(self):
|
||||||
"""
|
"""
|
||||||
@@ -736,8 +735,11 @@ class SubmissionFormWidget(QWidget):
|
|||||||
looked_up_reg = None
|
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}")
|
# 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:
|
if looked_up_reg != None:
|
||||||
relevant_reagents.remove(str(looked_up_reg.lot))
|
try:
|
||||||
relevant_reagents.insert(0, str(looked_up_reg.lot))
|
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:
|
else:
|
||||||
if len(relevant_reagents) > 1:
|
if len(relevant_reagents) > 1:
|
||||||
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub['export_map'] %}
|
{% if sub['export_map'] %}
|
||||||
<h3><u>Plate map:</u></h3>
|
<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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% if signing_permission %}
|
{% if signing_permission %}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ from sqlalchemy import create_engine
|
|||||||
from pydantic import field_validator, BaseModel, Field
|
from pydantic import field_validator, BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing import Any, Tuple, Literal, List
|
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__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -535,6 +540,18 @@ class Report(BaseModel):
|
|||||||
def rreplace(s, old, new):
|
def rreplace(s, old, new):
|
||||||
return (s[::-1].replace(old[::-1],new[::-1], 1))[::-1]
|
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)
|
ctx = get_config(None)
|
||||||
|
|
||||||
def is_power_user() -> bool:
|
def is_power_user() -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user