Midway through disaster of changing table names.

This commit is contained in:
Landon Wark
2024-01-19 15:17:07 -06:00
parent d66d861262
commit 319f72cab2
31 changed files with 1040 additions and 1457 deletions

View File

@@ -1,46 +0,0 @@
"""Adding in processes
Revision ID: 10c47a04559d
Revises: 94289d4e63e6
Create Date: 2024-01-05 13:25:02.468436
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '10c47a04559d'
down_revision = '94289d4e63e6'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_process',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_equipmentroles_processes',
sa.Column('process_id', sa.INTEGER(), nullable=True),
sa.Column('equipmentroles_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_equipment_roles.id'], ),
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
)
op.create_table('_submissiontypes_processes',
sa.Column('process_id', sa.INTEGER(), nullable=True),
sa.Column('equipmentroles_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_submission_types.id'], ),
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_submissiontypes_processes')
op.drop_table('_equipmentroles_processes')
op.drop_table('_process')
# ### end Alembic commands ###

View File

@@ -1,44 +0,0 @@
"""SubmissionReagentAssociations added
Revision ID: 238c3c3e5863
Revises: 2684f065037c
Create Date: 2023-12-05 12:57:17.446606
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '238c3c3e5863'
down_revision = '2684f065037c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_reagents_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('comments', sa.String(length=1024), nullable=True))
batch_op.alter_column('reagent_id',
existing_type=sa.INTEGER(),
nullable=False)
batch_op.alter_column('submission_id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_reagents_submissions', schema=None) as batch_op:
batch_op.alter_column('submission_id',
existing_type=sa.INTEGER(),
nullable=True)
batch_op.alter_column('reagent_id',
existing_type=sa.INTEGER(),
nullable=True)
batch_op.drop_column('comments')
# ### end Alembic commands ###

View File

@@ -1,34 +0,0 @@
"""link controls with Bacterial Culture Samples
Revision ID: 2684f065037c
Revises: 7e7b6eeca468
Create Date: 2023-12-05 10:29:31.126732
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2684f065037c'
down_revision = '7e7b6eeca468'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_control_samples', schema=None) as batch_op:
batch_op.add_column(sa.Column('sample_id', sa.INTEGER(), nullable=True))
batch_op.create_foreign_key('cont_BCS_id', '_samples', ['sample_id'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_control_samples', schema=None) as batch_op:
batch_op.drop_constraint('cont_BCS_id', type_='foreignkey')
batch_op.drop_column('sample_id')
# ### end Alembic commands ###

View File

@@ -1,40 +0,0 @@
"""Adding role tag to SubmissionEquipmentAssociation
Revision ID: 30aab47d6f12
Revises: bc7a74476609
Create Date: 2023-12-27 09:00:26.262904
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '30aab47d6f12'
down_revision = 'bc7a74476609'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# op.drop_table('_alembic_tmp__equipment')
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('role', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.drop_column('role')
# op.create_table('_alembic_tmp__equipment',
# sa.Column('id', sa.INTEGER(), nullable=False),
# sa.Column('name', sa.VARCHAR(length=64), nullable=True),
# sa.Column('nickname', sa.VARCHAR(length=64), nullable=True),
# sa.Column('asset_number', sa.VARCHAR(length=16), nullable=True),
# sa.PrimaryKeyConstraint('id')
# )
# ### end Alembic commands ###

View File

@@ -1,52 +0,0 @@
"""Adding in Equipment
Revision ID: 36a47d8837ca
Revises: 238c3c3e5863
Create Date: 2023-12-12 09:16:09.559753
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '36a47d8837ca'
down_revision = '238c3c3e5863'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_equipment',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('nickname', sa.String(length=64), nullable=True),
sa.Column('asset_number', sa.String(length=16), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_submissiontype_equipment',
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submission_types.id'], ),
sa.PrimaryKeyConstraint('equipment_id', 'submission_id')
)
op.create_table('_equipment_submissions',
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('comments', sa.String(length=1024), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submissions.id'], ),
sa.PrimaryKeyConstraint('equipment_id', 'submission_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_equipment_submissions')
op.drop_table('_submissiontype_equipment')
op.drop_table('_equipment')
# ### end Alembic commands ###

View File

@@ -1,34 +0,0 @@
"""Adding times to equipSubAssoc
Revision ID: 3e94fecbbe91
Revises: cd5c225b5d2a
Create Date: 2023-12-15 09:38:33.931976
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3e94fecbbe91'
down_revision = 'cd5c225b5d2a'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('start_time', sa.TIMESTAMP(), nullable=True))
batch_op.add_column(sa.Column('end_time', sa.TIMESTAMP(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.drop_column('end_time')
batch_op.drop_column('start_time')
# ### end Alembic commands ###

View File

@@ -1,34 +0,0 @@
"""Adding SubmissionType to KitReagentTypeAssociations
Revision ID: 4606a7be32e8
Revises: 67fa77849024
Create Date: 2024-01-08 09:04:46.917615
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4606a7be32e8'
down_revision = '67fa77849024'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_reagenttypes_kittypes', schema=None) as batch_op:
batch_op.add_column(sa.Column('submission_type_id', sa.INTEGER(), nullable=False))
batch_op.create_foreign_key("st_kt_rt_assoc", '_submission_types', ['submission_type_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_reagenttypes_kittypes', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_column('submission_type_id')
# ### end Alembic commands ###

View File

@@ -1,38 +0,0 @@
"""Adjusting process-submissionequipassoc
Revision ID: 67fa77849024
Revises: e08a69a0f381
Create Date: 2024-01-05 15:06:24.305945
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '67fa77849024'
down_revision = 'e08a69a0f381'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('process_id', sa.INTEGER(), nullable=True))
# batch_op.drop_constraint(None, type_='foreignkey')
batch_op.create_foreign_key('SEA_Process_id', '_process', ['process_id'], ['id'], ondelete='SET NULL')
batch_op.drop_column('process')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('process', sa.VARCHAR(length=64), nullable=True))
batch_op.drop_constraint('SEA_Process_id', type_='foreignkey')
batch_op.create_foreign_key(None, '_process', ['process'], ['id'], ondelete='SET NULL')
batch_op.drop_column('process_id')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""Adding equipment clustering
Revision ID: 761baf9d7842
Revises: 3e94fecbbe91
Create Date: 2023-12-18 14:31:21.533319
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '761baf9d7842'
down_revision = '3e94fecbbe91'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment', schema=None) as batch_op:
batch_op.add_column(sa.Column('cluster_name', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment', schema=None) as batch_op:
batch_op.drop_column('cluster_name')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""add templates to submission types
Revision ID: 7e7b6eeca468
Revises:
Create Date: 2023-11-23 08:07:51.103392
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7e7b6eeca468'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submission_types', schema=None) as batch_op:
batch_op.add_column(sa.Column('template_file', sa.BLOB(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submission_types', schema=None) as batch_op:
batch_op.drop_column('template_file')
# ### end Alembic commands ###

View File

@@ -1,36 +0,0 @@
"""Updating primary key for SubmissionEquipmentAssociation
Revision ID: 94289d4e63e6
Revises: 30aab47d6f12
Create Date: 2024-01-03 15:14:21.156127
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '94289d4e63e6'
down_revision = '30aab47d6f12'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.alter_column('role',
existing_type=sa.VARCHAR(length=64),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.alter_column('role',
existing_type=sa.VARCHAR(length=64),
nullable=True)
# ### end Alembic commands ###

View File

@@ -1,59 +0,0 @@
"""Adding equipment roles
Revision ID: bc7a74476609
Revises: 761baf9d7842
Create Date: 2023-12-21 10:44:23.520392
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = 'bc7a74476609'
down_revision = '761baf9d7842'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_equipment_roles',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_equipmentroles_equipment',
sa.Column('equipment_id', sa.INTEGER(), nullable=True),
sa.Column('equipmentroles_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_equipment_roles.id'], )
)
op.create_table('_submissiontype_equipmentrole',
sa.Column('equipmentrole_id', sa.INTEGER(), nullable=False),
sa.Column('submissiontype_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.Column('static', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipmentrole_id'], ['_equipment_roles.id'], ),
sa.ForeignKeyConstraint(['submissiontype_id'], ['_submission_types.id'], ),
sa.PrimaryKeyConstraint('equipmentrole_id', 'submissiontype_id')
)
op.drop_table('_submissiontype_equipment')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_submissiontype_equipment',
sa.Column('equipment_id', sa.INTEGER(), nullable=False),
sa.Column('submissiontype_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sqlite.JSON(), nullable=True),
sa.Column('static', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['submissiontype_id'], ['_submission_types.id'], ),
sa.PrimaryKeyConstraint('equipment_id', 'submissiontype_id')
)
op.drop_table('_submissiontype_equipmentrole')
op.drop_table('_equipmentroles_equipment')
op.drop_table('_equipment_roles')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""Adding static option to equipSTASsoc
Revision ID: cd11db3794ed
Revises: 36a47d8837ca
Create Date: 2023-12-12 14:47:20.924443
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cd11db3794ed'
down_revision = '36a47d8837ca'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype_equipment', schema=None) as batch_op:
batch_op.add_column(sa.Column('static', sa.INTEGER(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype_equipment', schema=None) as batch_op:
batch_op.drop_column('static')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""Adding process to equipSubAssoc
Revision ID: cd5c225b5d2a
Revises: cd11db3794ed
Create Date: 2023-12-15 09:13:23.492512
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cd5c225b5d2a'
down_revision = 'cd11db3794ed'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('process', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.drop_column('process')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""Attaching Process to SubmissionEquipmentAssociation
Revision ID: e08a69a0f381
Revises: 10c47a04559d
Create Date: 2024-01-05 14:50:55.681167
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e08a69a0f381'
down_revision = '10c47a04559d'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.create_foreign_key('SEA_Process_id', '_process', ['process'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
batch_op.drop_constraint('SEA_Process_id', type_='foreignkey')
# ### end Alembic commands ###

View File

@@ -4,7 +4,7 @@ from pathlib import Path
# Version of the realpython-reader package # Version of the realpython-reader package
__project__ = "submissions" __project__ = "submissions"
__version__ = "202401.1b" __version__ = "202401.2b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2024, Government of Canada" __copyright__ = "2022-2024, Government of Canada"

View File

@@ -21,6 +21,10 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} __table_args__ = {'extend_existing': True}
@declared_attr
def __tablename__(cls):
return f"_{cls.__name__.lower()}"
@declared_attr @declared_attr
def __database_session__(cls): def __database_session__(cls):
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
@@ -45,6 +49,15 @@ class BaseClass(Base):
from test_settings import ctx from test_settings import ctx
return ctx.backup_path return ctx.backup_path
def save(self):
logger.debug(f"Saving {self}")
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except Exception as e:
logger.critical(f"Problem saving object: {e}")
self.__database_session__.rollback()
from .controls import * from .controls import *
# import order must go: orgs, kit, subs due to circular import issues # import order must go: orgs, kit, subs due to circular import issues
from .organizations import * from .organizations import *

View File

@@ -18,7 +18,7 @@ class ControlType(BaseClass):
""" """
Base class of a control archetype. Base class of a control archetype.
""" """
__tablename__ = '_control_types' # __tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS) name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -75,7 +75,7 @@ class Control(BaseClass):
Base class of a control sample. Base class of a control sample.
""" """
__tablename__ = '_control_samples' # __tablename__ = '_control_samples'
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type
@@ -265,6 +265,3 @@ class Control(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def save(self):
self.__database_session__.add(self)
self.__database_session__.commit()

View File

@@ -12,7 +12,6 @@ from typing import List
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path from pathlib import Path
from . import Base, BaseClass, Organization from . import Base, BaseClass, Organization
from tools import Settings
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
@@ -32,11 +31,19 @@ equipmentroles_equipment = Table(
extend_existing=True extend_existing=True
) )
equipment_processes = Table(
"_equipment_processes",
Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
extend_existing=True
)
equipmentroles_processes = Table( equipmentroles_processes = Table(
"_equipmentroles_processes", "_equipmentroles_processes",
Base.metadata, Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")), Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipmentroles_id", INTEGER, ForeignKey("_equipment_roles.id")), Column("equipmentrole_id", INTEGER, ForeignKey("_equipment_roles.id")),
extend_existing=True extend_existing=True
) )
@@ -48,16 +55,24 @@ submissiontypes_processes = Table(
extend_existing=True extend_existing=True
) )
kittypes_processes = Table(
"_kittypes_processes",
Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("kit_id", INTEGER, ForeignKey("_kits.id")),
extend_existing=True
)
class KitType(BaseClass): class KitType(BaseClass):
""" """
Base of kits used in submission processing Base of kits used in submission processing
""" """
__tablename__ = "_kits" # __tablename__ = "_kits"
# __table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
processes = relationship("Process", back_populates="kit_types", secondary=kittypes_processes)
kit_reagenttype_associations = relationship( kit_reagenttype_associations = relationship(
"KitTypeReagentTypeAssociation", "KitTypeReagentTypeAssociation",
@@ -87,7 +102,7 @@ class KitType(BaseClass):
Args: Args:
required (bool, optional): If true only return required types. Defaults to False. required (bool, optional): If true only return required types. Defaults to False.
submission_type (str | None, optional): Submission type to narrow results. Defaults to None. submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None.
Returns: Returns:
list: List of reagent types list: List of reagent types
@@ -109,12 +124,13 @@ class KitType(BaseClass):
Creates map of locations in excel workbook for a SubmissionType Creates map of locations in excel workbook for a SubmissionType
Args: Args:
use (str): Submissiontype.name use (str | SubmissionType): Submissiontype.name
Returns: Returns:
dict: Dictionary containing information locations. dict: Dictionary containing information locations.
""" """
map = {} map = {}
# Account for submission_type variable type.
match submission_type: match submission_type:
case str(): case str():
assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name==submission_type] assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name==submission_type]
@@ -125,7 +141,6 @@ class KitType(BaseClass):
case _: case _:
raise ValueError(f"Wrong variable type: {type(submission_type)} used!") raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
# Get all KitTypeReagentTypeAssociation for SubmissionType # Get all KitTypeReagentTypeAssociation for SubmissionType
# assocs = [item for item in self.kit_reagenttype_associations if item.submission_type==submission_type]
for assoc in assocs: for assoc in assocs:
try: try:
map[assoc.reagent_type.name] = assoc.uses map[assoc.reagent_type.name] = assoc.uses
@@ -133,7 +148,6 @@ class KitType(BaseClass):
continue continue
# Get SubmissionType info map # Get SubmissionType info map
try: try:
# st_assoc = [item for item in self.used_for if use == item.name][0]
map['info'] = st_assoc.info_map map['info'] = st_assoc.info_map
except IndexError as e: except IndexError as e:
map['info'] = {} map['info'] = {}
@@ -152,7 +166,7 @@ class KitType(BaseClass):
Args: Args:
name (str, optional): Name of desired kit (returns single instance). Defaults to None. name (str, optional): Name of desired kit (returns single instance). Defaults to None.
used_for (str | models.Submissiontype | None, optional): Submission type the kit is used for. Defaults to None. used_for (str | Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
id (int | None, optional): Kit id in the database. Defaults to None. id (int | None, optional): Kit id in the database. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -190,17 +204,13 @@ class KitType(BaseClass):
@check_authorization @check_authorization
def save(self, ctx:Settings): def save(self, ctx:Settings):
""" super().save()
Add this instance to database and commit
"""
self.__database_session__.add(self)
self.__database_session__.commit()
class ReagentType(BaseClass): class ReagentType(BaseClass):
""" """
Base of reagent type abstract Base of reagent type abstract
""" """
__tablename__ = "_reagent_types" # __tablename__ = "_reagent_types"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type name = Column(String(64)) #: name of reagent type
@@ -282,129 +292,15 @@ class ReagentType(BaseClass):
from backend.validators.pydant import PydReagent from backend.validators.pydant import PydReagent
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today()) return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
# class KitTypeReagentTypeAssociation(BaseClass): @check_authorization
# """ def save(self, ctx:Settings):
# table containing reagenttype/kittype associations super().save()
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
# """
# __tablename__ = "_reagenttypes_kittypes"
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
# submission_type_id = (Column(INTEGER), ForeignKey("_submission_types.id"), primary_key=True)
# uses = Column(JSON) #: map to location on excel sheets of different submission types
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
# last_used = Column(String(32)) #: last used lot number of this type of reagent
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
# # reference to the "ReagentType" object
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
# submission_type = relationship(SubmissionType, back_populates="submissiontype_kit_rt_associations")
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
# self.kit_type = kit_type
# self.reagent_type = reagent_type
# self.uses = uses
# self.required = required
# def __repr__(self) -> str:
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
# @validates('required')
# def validate_age(self, key, value):
# """
# Ensures only 1 & 0 used in 'required'
# Args:
# key (str): name of attribute
# value (_type_): value of attribute
# Raises:
# ValueError: Raised if bad value given
# Returns:
# _type_: value
# """
# if not 0 <= value < 2:
# raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
# return value
# @validates('reagenttype')
# def validate_reagenttype(self, key, value):
# """
# Ensures reagenttype is an actual ReagentType
# Args:
# key (str)): name of attribute
# value (_type_): value of attribute
# Raises:
# ValueError: raised if reagenttype is not a ReagentType
# Returns:
# _type_: ReagentType
# """
# if not isinstance(value, ReagentType):
# raise ValueError(f'{value} is not a reagenttype')
# return value
# @classmethod
# @setup_lookup
# def query(cls,
# kit_type:KitType|str|None=None,
# reagent_type:ReagentType|str|None=None,
# limit:int=0
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
# """
# Lookup junction of ReagentType and KitType
# Args:
# kit_type (models.KitType | str | None): KitType of interest.
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
# Returns:
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
# """
# query: Query = cls.__database_session__.query(cls)
# match kit_type:
# case KitType():
# query = query.filter(cls.kit_type==kit_type)
# case str():
# query = query.join(KitType).filter(KitType.name==kit_type)
# case _:
# pass
# match reagent_type:
# case ReagentType():
# query = query.filter(cls.reagent_type==reagent_type)
# case str():
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
# case _:
# pass
# if kit_type != None and reagent_type != None:
# limit = 1
# return query_return(query=query, limit=limit)
# def save(self) -> Report:
# """
# Adds this instance to the database and commits.
# Returns:
# Report: Result of save action
# """
# report = Report()
# self.__database_session__.add(self)
# self.__database_session__.commit()
# return report
class Reagent(BaseClass): class Reagent(BaseClass):
""" """
Concrete reagent instance Concrete reagent instance
""" """
__tablename__ = "_reagents" # __tablename__ = "_reagents"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type
@@ -412,7 +308,6 @@ class Reagent(BaseClass):
name = Column(String(64)) #: reagent name name = Column(String(64)) #: reagent name
lot = Column(String(64)) #: lot number of reagent lot = Column(String(64)) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
# submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in
reagent_submission_associations = relationship( reagent_submission_associations = relationship(
"SubmissionReagentAssociation", "SubmissionReagentAssociation",
@@ -497,6 +392,7 @@ class Reagent(BaseClass):
def query(cls, def query(cls,
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,
limit:int=0 limit:int=0
) -> Reagent|List[Reagent]: ) -> Reagent|List[Reagent]:
""" """
@@ -505,6 +401,7 @@ class Reagent(BaseClass):
Args: Args:
reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None. reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None.
lot_number (str | None, optional): Reagent lot number. Defaults to None. lot_number (str | None, optional): Reagent lot number. Defaults to None.
name (str | None, optional): Reagent name. Defaults to None.
limit (int, optional): limit of results returned. Defaults to 0. limit (int, optional): limit of results returned. Defaults to 0.
Returns: Returns:
@@ -521,6 +418,12 @@ class Reagent(BaseClass):
query = query.filter(cls.type.contains(reagent_type)) query = query.filter(cls.type.contains(reagent_type))
case _: case _:
pass pass
match name:
case str():
logger.debug(f"Looking up reagent by name: {name}")
query = query.filter(cls.name==name)
case _:
pass
match lot_number: match lot_number:
case str(): case str():
logger.debug(f"Looking up reagent by lot number: {lot_number}") logger.debug(f"Looking up reagent by lot number: {lot_number}")
@@ -531,18 +434,11 @@ class Reagent(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def save(self):
"""
Add this instance to the database and commit
"""
self.__database_session__.add(self)
self.__database_session__.commit()
class Discount(BaseClass): class Discount(BaseClass):
""" """
Relationship table for client labs for certain kits. Relationship table for client labs for certain kits.
""" """
__tablename__ = "_discounts" # __tablename__ = "_discounts"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
kit = relationship("KitType") #: joined parent reagent type kit = relationship("KitType") #: joined parent reagent type
@@ -604,17 +500,20 @@ class Discount(BaseClass):
pass pass
return query.all() return query.all()
@check_authorization
def save(self, ctx:Settings):
super().save()
class SubmissionType(BaseClass): class SubmissionType(BaseClass):
""" """
Abstract of types of submissions. Abstract of types of submissions.
""" """
__tablename__ = "_submission_types" # __tablename__ = "_submission_types"
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.
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type. instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
# regex = Column(String(512))
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) processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes)
@@ -664,20 +563,25 @@ class SubmissionType(BaseClass):
output = [] output = []
for item in self.submissiontype_equipmentrole_associations: for item in self.submissiontype_equipmentrole_associations:
map = item.uses map = item.uses
if map == None:
map = {}
try:
map['role'] = item.equipment_role.name map['role'] = item.equipment_role.name
except TypeError:
pass
output.append(map) output.append(map)
return output return output
# return [item.uses for item in self.submissiontype_equipmentrole_associations] # return [item.uses for item in self.submissiontype_equipmentrole_associations]
def get_equipment(self) -> List['PydEquipmentRole']: def get_equipment(self, extraction_kit:str|KitType|None=None) -> List['PydEquipmentRole']:
return [item.to_pydantic(submission_type=self) for item in self.equipment] return [item.to_pydantic(submission_type=self, extraction_kit=extraction_kit) for item in self.equipment]
def get_processes_for_role(self, equipment_role:str|EquipmentRole): def get_processes_for_role(self, equipment_role:str|EquipmentRole, kit:str|KitType|None=None):
match equipment_role: match equipment_role:
case str(): case str():
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role] relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
case EquipmentRole(): case EquipmentRole():
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role] relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
case _: case _:
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 ]))
@@ -728,7 +632,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
""" """
Abstract of relationship between kits and their submission type. Abstract of relationship between kits and their submission type.
""" """
__tablename__ = "_submissiontypes_kittypes" # __tablename__ = "_submissiontypes_kittypes"
submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of joined submission type submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of joined submission type
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of joined kit kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of joined kit
@@ -801,7 +705,7 @@ class KitTypeReagentTypeAssociation(BaseClass):
table containing reagenttype/kittype associations table containing reagenttype/kittype associations
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
__tablename__ = "_reagenttypes_kittypes" # __tablename__ = "_reagenttypes_kittypes"
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
@@ -902,22 +806,9 @@ class KitTypeReagentTypeAssociation(BaseClass):
limit = 1 limit = 1
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def save(self) -> Report:
"""
Adds this instance to the database and commits.
Returns:
Report: Result of save action
"""
report = Report()
self.__database_session__.add(self)
self.__database_session__.commit()
return report
class SubmissionReagentAssociation(BaseClass): class SubmissionReagentAssociation(BaseClass):
__tablename__ = "_reagents_submissions" # __tablename__ = "_reagents_submissions"
reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True)
@@ -961,8 +852,6 @@ class SubmissionReagentAssociation(BaseClass):
# logger.debug(f"Filtering query with reagent: {reagent}") # logger.debug(f"Filtering query with reagent: {reagent}")
reagent = Reagent.query(lot_number=reagent) reagent = Reagent.query(lot_number=reagent)
query = query.filter(cls.reagent==reagent) query = query.filter(cls.reagent==reagent)
# logger.debug([item.reagent.lot for item in query.all()])
# query = query.join(Reagent).filter(Reagent.lot==reagent)
case _: case _:
pass pass
# logger.debug(f"Result of query after reagent: {query.all()}") # logger.debug(f"Result of query after reagent: {query.all()}")
@@ -976,7 +865,6 @@ class SubmissionReagentAssociation(BaseClass):
case _: case _:
pass pass
# logger.debug(f"Result of query after submission: {query.all()}") # logger.debug(f"Result of query after submission: {query.all()}")
# limit = query.count()
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def to_sub_dict(self, extraction_kit): def to_sub_dict(self, extraction_kit):
@@ -989,13 +877,14 @@ class Equipment(BaseClass):
# Currently abstract until ready to implement # Currently abstract until ready to implement
# __abstract__ = True # __abstract__ = True
__tablename__ = "_equipment" # __tablename__ = "_equipment"
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
name = Column(String(64)) name = Column(String(64))
nickname = Column(String(64)) nickname = Column(String(64))
asset_number = Column(String(16)) asset_number = Column(String(16))
roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment) roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment)
processes = relationship("Process", back_populates="equipment", secondary=equipment_processes)
equipment_submission_associations = relationship( equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation", "SubmissionEquipmentAssociation",
@@ -1008,10 +897,30 @@ class Equipment(BaseClass):
def __repr__(self): def __repr__(self):
return f"<Equipment({self.name})>" return f"<Equipment({self.name})>"
def get_processes(self, submission_type:SubmissionType): def to_dict(self, processes:bool=False):
processes = [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name] if not processes:
return {k:v for k,v in self.__dict__.items() if k != 'processes'}
else:
return {k:v for k,v in self.__dict__.items()}
def get_processes(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
processes = [process for process in self.processes if submission_type in process.submission_types]
match extraction_kit:
case str():
processes = [process for process in processes if extraction_kit in [kit.name for kit in process.kit_types]]
case KitType():
processes = [process for process in processes if extraction_kit in process.kit_types]
case _:
pass
processes = [process.name for process in processes]
# try:
assert all([isinstance(process, str) for process in processes])
# except AssertionError as e:
# logger.error(processes)
# raise e
if len(processes) == 0: if len(processes) == 0:
processes = [''] processes = ['']
# logger.debug(f"Processes: {processes}")
return processes return processes
@classmethod @classmethod
@@ -1043,10 +952,10 @@ class Equipment(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def to_pydantic(self, submission_type:SubmissionType): def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipment from backend.validators.pydant import PydEquipment
# return PydEquipment(process=self.get_processes(submission_type=submission_type), role=None, **self.__dict__) return PydEquipment(processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=None, **self.to_dict(processes=False))
return PydEquipment(process=None, role=None, **self.__dict__) # return PydEquipment(process=None, role=None, **self.__dict__)
def save(self): def save(self):
self.__database_session__.add(self) self.__database_session__.add(self)
@@ -1064,12 +973,12 @@ class Equipment(BaseClass):
class EquipmentRole(BaseClass): class EquipmentRole(BaseClass):
__tablename__ = "_equipment_roles" # __tablename__ = "_equipment_roles"
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
name = Column(String(32)) name = Column(String(32))
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment) instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
processes = relationship("Process", back_populates="equipment_roles", secondary=equipmentroles_processes) processes = relationship("Process", back_populates='equipment_roles', secondary=equipmentroles_processes)
equipmentrole_submissiontype_associations = relationship( equipmentrole_submissiontype_associations = relationship(
"SubmissionTypeEquipmentRoleAssociation", "SubmissionTypeEquipmentRoleAssociation",
@@ -1082,11 +991,23 @@ class EquipmentRole(BaseClass):
def __repr__(self): def __repr__(self):
return f"<EquipmentRole({self.name})>" return f"<EquipmentRole({self.name})>"
def to_pydantic(self, submission_type:SubmissionType): def to_dict(self):
output = {}
for key, value in self.__dict__.items():
match key:
case "processes":
pass
case _:
value = value
output[key] = value
return output
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipmentRole from backend.validators.pydant import PydEquipmentRole
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances] equipment = [item.to_pydantic(submission_type=submission_type, extraction_kit=extraction_kit) for item in self.instances]
pyd_dict = self.__dict__ # processes = [item.name for item in self.processes]
pyd_dict['processes'] = self.get_processes(submission_type=submission_type) pyd_dict = self.to_dict()
pyd_dict['processes'] = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit)
return PydEquipmentRole(equipment=equipment, **pyd_dict) return PydEquipmentRole(equipment=equipment, **pyd_dict)
@classmethod @classmethod
@@ -1107,31 +1028,36 @@ class EquipmentRole(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def get_processes(self, submission_type:str|SubmissionType|None) -> List[Process]: def get_processes(self, submission_type:str|SubmissionType|None, extraction_kit:str|KitType|None=None) -> List[Process]:
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
# assert all([isinstance(process, Process) for process in self.processes])
# logger.debug(self.processes)
if submission_type != None: if submission_type != None:
output = [process.name for process in self.processes if submission_type in process.submission_types] # for process in self.processes:
# logger.debug(f"Process: {type(process)}: {process}")
processes = [process for process in self.processes if submission_type in process.submission_types]
else: else:
output = [process.name for process in self.processes] processes = self.processes
match extraction_kit:
case str():
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
case KitType():
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
case _:
pass
output = [item.name for item in processes]
if len(output) == 0: if len(output) == 0:
return [''] return ['']
else: else:
return output return output
def save(self):
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except:
self.__database_session__.rollback()
class SubmissionEquipmentAssociation(BaseClass): class SubmissionEquipmentAssociation(BaseClass):
# Currently abstract until ready to implement # Currently abstract until ready to implement
# __abstract__ = True # __abstract__ = True
__tablename__ = "_equipment_submissions" # __tablename__ = "_equipment_submissions"
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
@@ -1144,14 +1070,14 @@ class SubmissionEquipmentAssociation(BaseClass):
submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated submission equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
def __init__(self, submission, equipment): def __init__(self, submission, equipment):
self.submission = submission self.submission = submission
self.equipment = equipment self.equipment = equipment
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=self.process.name, role=self.role, nickname=self.equipment.nickname) output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, processes=[self.process.name], role=self.role, nickname=self.equipment.nickname)
return output return output
def save(self): def save(self):
@@ -1162,7 +1088,7 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
# __abstract__ = True # __abstract__ = True
__tablename__ = "_submissiontype_equipmentrole" # __tablename__ = "_submissiontype_equipmentrole"
equipmentrole_id = Column(INTEGER, ForeignKey("_equipment_roles.id"), primary_key=True) #: id of associated equipment equipmentrole_id = Column(INTEGER, ForeignKey("_equipment_roles.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
@@ -1192,25 +1118,38 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value return value
def get_all_processes(self): def get_all_processes(self, extraction_kit:KitType|str|None=None):
processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances] processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances]
# flatten list
processes = [item for items in processes for item in items if item != None ] processes = [item for items in processes for item in items if item != None ]
match extraction_kit:
case str():
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
case KitType():
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
case _:
pass
return processes return processes
@check_authorization @check_authorization
def save(self, ctx:Settings): def save(self, ctx:Settings):
self.__database_session__.add(self) # self.__database_session__.add(self)
self.__database_session__.commit() # self.__database_session__.commit()
super().save()
class Process(BaseClass): class Process(BaseClass):
"""
__tablename__ = "_process" A Process is a method used by a piece of equipment.
"""
# __tablename__ = "_process"
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
name = Column(String(64)) name = Column(String(64))
submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes) submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes)
equipment = relationship("Equipment", back_populates='processes', secondary=equipment_processes)
equipment_roles = relationship("EquipmentRole", back_populates='processes', secondary=equipmentroles_processes) equipment_roles = relationship("EquipmentRole", back_populates='processes', secondary=equipmentroles_processes)
submissions = relationship("SubmissionEquipmentAssociation", backref='process') submissions = relationship("SubmissionEquipmentAssociation", backref='process')
kit_types = relationship("KitType", back_populates='processes', secondary=kittypes_processes)
def __repr__(self): def __repr__(self):
return f"<Process({self.name})" return f"<Process({self.name})"
@@ -1227,124 +1166,3 @@ class Process(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def save(self):
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except:
self.__database_session__.rollback()
# class KitTypeReagentTypeAssociation(BaseClass):
# """
# table containing reagenttype/kittype associations
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
# """
# __tablename__ = "_reagenttypes_kittypes"
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
# uses = Column(JSON) #: map to location on excel sheets of different submission types
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
# last_used = Column(String(32)) #: last used lot number of this type of reagent
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
# # reference to the "ReagentType" object
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
# self.kit_type = kit_type
# self.reagent_type = reagent_type
# self.uses = uses
# self.required = required
# def __repr__(self) -> str:
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
# @validates('required')
# def validate_age(self, key, value):
# """
# Ensures only 1 & 0 used in 'required'
# Args:
# key (str): name of attribute
# value (_type_): value of attribute
# Raises:
# ValueError: Raised if bad value given
# Returns:
# _type_: value
# """
# if not 0 <= value < 2:
# raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
# return value
# @validates('reagenttype')
# def validate_reagenttype(self, key, value):
# """
# Ensures reagenttype is an actual ReagentType
# Args:
# key (str)): name of attribute
# value (_type_): value of attribute
# Raises:
# ValueError: raised if reagenttype is not a ReagentType
# Returns:
# _type_: ReagentType
# """
# if not isinstance(value, ReagentType):
# raise ValueError(f'{value} is not a reagenttype')
# return value
# @classmethod
# @setup_lookup
# def query(cls,
# kit_type:KitType|str|None=None,
# reagent_type:ReagentType|str|None=None,
# limit:int=0
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
# """
# Lookup junction of ReagentType and KitType
# Args:
# kit_type (models.KitType | str | None): KitType of interest.
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
# Returns:
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
# """
# query: Query = cls.__database_session__.query(cls)
# match kit_type:
# case KitType():
# query = query.filter(cls.kit_type==kit_type)
# case str():
# query = query.join(KitType).filter(KitType.name==kit_type)
# case _:
# pass
# match reagent_type:
# case ReagentType():
# query = query.filter(cls.reagent_type==reagent_type)
# case str():
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
# case _:
# pass
# if kit_type != None and reagent_type != None:
# limit = 1
# return query_return(query=query, limit=limit)
# def save(self) -> Report:
# """
# Adds this instance to the database and commits.
# Returns:
# Report: Result of save action
# """
# report = Report()
# self.__database_session__.add(self)
# self.__database_session__.commit()
# return report

View File

@@ -25,7 +25,7 @@ class Organization(BaseClass):
""" """
Base of organization Base of organization
""" """
__tablename__ = "_organizations" # __tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name name = Column(String(64)) #: organization name
@@ -73,14 +73,13 @@ class Organization(BaseClass):
Args: Args:
ctx (Settings): Settings object passed down from GUI. Necessary to check authorization ctx (Settings): Settings object passed down from GUI. Necessary to check authorization
""" """
ctx.database_session.add(self) super().save()
ctx.database_session.commit()
class Contact(BaseClass): class Contact(BaseClass):
""" """
Base of Contact Base of Contact
""" """
__tablename__ = "_contacts" # __tablename__ = "_contacts"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name name = Column(String(64)) #: contact name

View File

@@ -13,23 +13,24 @@ from json.decoder import JSONDecodeError
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
import pandas as pd import pandas as pd
from openpyxl import Workbook from openpyxl import Workbook
from . import BaseClass from . import BaseClass, Equipment
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
from datetime import datetime, date, time from datetime import datetime, date, time
from typing import List from typing import List, Any
from dateutil.parser import parse from dateutil.parser import parse
from dateutil.parser._parser import ParserError from dateutil.parser._parser import ParserError
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
from pathlib import Path from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class BasicSubmission(BaseClass): class BasicSubmission(BaseClass):
""" """
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
""" """
__tablename__ = "_submissions" # __tablename__ = "_submissions"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012) rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
@@ -96,7 +97,7 @@ class BasicSubmission(BaseClass):
""" """
return f"{self.rsl_plate_num} - {self.submitter_plate_num}" return f"{self.rsl_plate_num} - {self.submitter_plate_num}"
def to_dict(self, full_data:bool=False) -> dict: def to_dict(self, full_data:bool=False, backup:bool=False) -> dict:
""" """
Constructs dictionary used in submissions summary Constructs dictionary used in submissions summary
@@ -137,7 +138,7 @@ class BasicSubmission(BaseClass):
logger.error(f"We got an error retrieving reagents: {e}") logger.error(f"We got an error retrieving reagents: {e}")
reagents = None reagents = None
# samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations] # samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations]
samples = [item.to_sub_dict() for item in self.submission_sample_associations] samples = self.adjust_to_dict_samples(backup=backup)
try: try:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations] equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if len(equipment) == 0: if len(equipment) == 0:
@@ -255,18 +256,7 @@ class BasicSubmission(BaseClass):
Returns: Returns:
list: list of htipick dictionaries for each sample list: list of htipick dictionaries for each sample
""" """
output_list = [] output_list = [assoc.to_hitpick() for assoc in self.submission_sample_associations]
for assoc in self.submission_sample_associations:
samp = assoc.sample.to_hitpick(submission_rsl=self.rsl_plate_num)
if samp != None:
if plate_number != None:
samp['plate_number'] = plate_number
samp['row'] = assoc.row
samp['column'] = assoc.column
samp['plate_name'] = self.rsl_plate_num
output_list.append(samp)
else:
continue
return output_list return output_list
@classmethod @classmethod
@@ -548,7 +538,7 @@ class BasicSubmission(BaseClass):
result = assoc.save() result = assoc.save()
return result return result
def to_pydantic(self): def to_pydantic(self, backup:bool=False):
""" """
Converts this instance into a PydSubmission Converts this instance into a PydSubmission
@@ -556,7 +546,7 @@ class BasicSubmission(BaseClass):
PydSubmission: converted object. PydSubmission: converted object.
""" """
from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment
dicto = self.to_dict(full_data=True) dicto = self.to_dict(full_data=True, backup=backup)
logger.debug(f"Backup dictionary: {pformat(dicto)}") logger.debug(f"Backup dictionary: {pformat(dicto)}")
# dicto['filepath'] = Path(tempfile.TemporaryFile().name) # dicto['filepath'] = Path(tempfile.TemporaryFile().name)
new_dict = {} new_dict = {}
@@ -567,7 +557,11 @@ class BasicSubmission(BaseClass):
case "samples": case "samples":
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']] new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
case "equipment": case "equipment":
# logger.debug(f"\n\nEquipment: {dicto['equipment']}\n\n")
try:
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']] new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
except TypeError as e:
logger.error(f"Possible no equipment error: {e}")
case "Plate Number": case "Plate Number":
new_dict['rsl_plate_num'] = dict(value=value, missing=True) new_dict['rsl_plate_num'] = dict(value=value, missing=True)
case "Submitter Plate Number": case "Submitter Plate Number":
@@ -582,25 +576,6 @@ class BasicSubmission(BaseClass):
# sys.exit() # sys.exit()
return PydSubmission(**new_dict) return PydSubmission(**new_dict)
def backup(self, fname:Path, full_backup:bool=True):
"""
Exports xlsx and yml info files for this instance.
Args:
fname (Path): Filename of xlsx file.
"""
if full_backup:
backup = self.to_dict(full_data=True)
try:
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
yaml.dump(backup, f)
except KeyError as e:
logger.error(f"Problem saving yml backup file: {e}")
pyd = self.to_pydantic()
wb = pyd.autofill_excel()
wb = pyd.autofill_samples(wb)
wb.save(filename=fname.with_suffix(".xlsx"))
def save(self, original:bool=True): def save(self, original:bool=True):
""" """
Adds this instance to database and commits. Adds this instance to database and commits.
@@ -610,24 +585,7 @@ class BasicSubmission(BaseClass):
""" """
if original: if original:
self.uploaded_by = getuser() self.uploaded_by = getuser()
self.__database_session__.add(self) super().save()
self.__database_session__.commit()
def delete(self):
"""
Performs backup and deletes this instance from database.
Raises:
e: Raised in something goes wrong.
"""
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
self.backup(fname=fname)
self.__database_session__.delete(self)
try:
self.__database_session__.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
self.__database_session__.rollback()
raise e
@classmethod @classmethod
@setup_lookup @setup_lookup
@@ -782,15 +740,121 @@ class BasicSubmission(BaseClass):
def get_used_equipment(self) -> List[str]: def get_used_equipment(self) -> List[str]:
return [item.role for item in self.submission_equipment_associations] return [item.role for item in self.submission_equipment_associations]
@classmethod
def adjust_autofill_samples(cls, samples:List[Any]) -> List[Any]:
logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler")
return samples
def adjust_to_dict_samples(self, backup:bool=False):
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
return [item.to_sub_dict() for item in self.submission_sample_associations]
# Custom context events for the ui
def custom_context_events(self):
names = ["Delete", "Details", "Add Comment", "Add Equipment", "Export"]
funcs = [self.delete, self.show_details, self.add_comment, self.add_equipment, self.backup]
dicto = {item[0]:item[1] for item in zip(names, funcs)}
return dicto
def delete(self, obj=None):
"""
Performs backup and deletes this instance from database.
Raises:
e: Raised in something goes wrong.
"""
logger.debug("Hello from delete")
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
self.backup(fname=fname, full_backup=True)
self.__database_session__.delete(self)
try:
self.__database_session__.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
self.__database_session__.rollback()
raise e
def show_details(self, obj):
logger.debug("Hello from details")
from frontend.widgets.submission_details import SubmissionDetails
dlg = SubmissionDetails(parent=obj, sub=self)
if dlg.exec():
pass
def add_comment(self, obj):
from frontend.widgets.submission_details import SubmissionComment
dlg = SubmissionComment(parent=obj, submission=self)
if dlg.exec():
comment = dlg.parse_form()
try:
# For some reason .append results in new comment being ignored, so have to concatenate lists.
self.comment = self.comment + comment
except (AttributeError, TypeError) as e:
logger.error(f"Hit error ({e}) creating comment")
self.comment = comment
logger.debug(self.comment)
self.save(original=False)
# logger.debug(f"Save result: {result}")
def add_equipment(self, obj):
# submission_type = submission.submission_type_name
from frontend.widgets.equipment_usage import EquipmentUsage
dlg = EquipmentUsage(parent=obj, submission_type=self.submission_type_name, submission=self)
if dlg.exec():
equipment = dlg.parse_form()
logger.debug(f"We've got equipment: {equipment}")
for equip in equipment:
# e = Equipment.query(name=equip.name)
# assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# process = Process.query(name=equip.processes)
# assoc.process = process
# assoc.role = equip.role
_, assoc = equip.toSQL(submission=self)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
else:
pass
def backup(self, obj=None, fname:Path|None=None, full_backup:bool=False):
"""
Exports xlsx and yml info files for this instance.
Args:
fname (Path): Filename of xlsx file.
"""
logger.debug("Hello from backup.")
if fname == None:
from frontend.widgets.functions import select_save_file
from backend.validators import RSLNamer
abbreviation = self.get_abbreviation()
file_data = dict(rsl_plate_num=self.rsl_plate_num, submission_type=self.submission_type_name, submitted_date=self.submitted_date, abbreviation=abbreviation)
fname = select_save_file(default_name=RSLNamer.construct_new_plate_name(data=file_data), extension="xlsx", obj=obj)
if full_backup:
backup = self.to_dict(full_data=True)
try:
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
yaml.dump(backup, f)
except KeyError as e:
logger.error(f"Problem saving yml backup file: {e}")
pyd = self.to_pydantic(backup=True)
wb = pyd.autofill_excel()
wb = pyd.autofill_samples(wb)
wb = pyd.autofill_equipment(wb)
wb.save(filename=fname.with_suffix(".xlsx"))
# Below are the custom submission types # Below are the custom submission types
class BacterialCulture(BasicSubmission): class BacterialCulture(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
""" """
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True) id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission
__mapper_args__ = {"polymorphic_identity": "Bacterial Culture", "polymorphic_load": "inline"} __mapper_args__ = dict(polymorphic_identity="Bacterial Culture",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
def to_dict(self, full_data:bool=False) -> dict: def to_dict(self, full_data:bool=False) -> dict:
""" """
@@ -804,6 +868,10 @@ 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
def get_abbreviation(cls):
return "BC"
@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:
""" """
@@ -853,7 +921,7 @@ class BacterialCulture(BasicSubmission):
Extends parent Extends parent
""" """
from backend.validators import RSLNamer from backend.validators import RSLNamer
data['abbreviation'] = "BC" data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data) outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None) -> str: # def construct(data:dict|None=None) -> str:
# """ # """
@@ -932,10 +1000,12 @@ class Wastewater(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
""" """
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True) id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
ext_technician = Column(String(64)) ext_technician = Column(String(64))
pcr_technician = Column(String(64)) pcr_technician = Column(String(64))
__mapper_args__ = {"polymorphic_identity": "Wastewater", "polymorphic_load": "inline"} __mapper_args__ = __mapper_args__ = dict(polymorphic_identity="Wastewater",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
def to_dict(self, full_data:bool=False) -> dict: def to_dict(self, full_data:bool=False) -> dict:
""" """
@@ -952,6 +1022,10 @@ class Wastewater(BasicSubmission):
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}" output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
return output return output
@classmethod
def get_abbreviation(cls):
return "WW"
@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:
""" """
@@ -1012,7 +1086,7 @@ class Wastewater(BasicSubmission):
Extends parent Extends parent
""" """
from backend.validators import RSLNamer from backend.validators import RSLNamer
data['abbreviation'] = "WW" data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data) outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None): # def construct(data:dict|None=None):
# if "submitted_date" in data.keys(): # if "submitted_date" in data.keys():
@@ -1066,13 +1140,21 @@ class Wastewater(BasicSubmission):
# return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)" # return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)"
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)" return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
@classmethod
def adjust_autofill_samples(cls, samples: List[Any]) -> List[Any]:
samples = super().adjust_autofill_samples(samples)
return [item for item in samples if not item.submitter_id.startswith("EN")]
class WastewaterArtic(BasicSubmission): class WastewaterArtic(BasicSubmission):
""" """
derivative submission type for artic wastewater derivative submission type for artic wastewater
""" """
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True) id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
__mapper_args__ = {"polymorphic_identity": "Wastewater Artic", "polymorphic_load": "inline"} __mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
artic_technician = Column(String(64)) artic_technician = Column(String(64))
dna_core_submission_number = Column(String(64))
def calculate_base_cost(self): def calculate_base_cost(self):
""" """
@@ -1093,6 +1175,10 @@ class WastewaterArtic(BasicSubmission):
except Exception as e: except Exception as e:
logger.error(f"Calculation error: {e}") logger.error(f"Calculation error: {e}")
@classmethod
def get_abbreviation(cls):
return "AR"
@classmethod @classmethod
def parse_samples(cls, input_dict: dict) -> dict: def parse_samples(cls, input_dict: dict) -> dict:
""" """
@@ -1109,15 +1195,45 @@ class WastewaterArtic(BasicSubmission):
# Because generate_sample_object needs the submitter_id and the artic has the "({origin well})" # Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
# at the end, this has to be done here. No moving to sqlalchemy object :( # at the end, this has to be done here. No moving to sqlalchemy object :(
input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip() input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip()
if "ENC" in input_dict['submitter_id']:
input_dict['submitter_id'] = cls.en_adapter(input_str=input_dict['submitter_id'])
return input_dict return input_dict
@classmethod
def en_adapter(cls, input_str) -> str:
processed = re.sub(r"[A-Z]", "", input_str)
try:
en_num = re.search(r"\-\d{1}$", processed).group()
processed = processed.replace(en_num, "", -1)
except AttributeError:
en_num = "1"
en_num = en_num.strip("-")
logger.debug(f"Processed after en-num: {processed}")
try:
plate_num = re.search(r"\-\d{1}$", processed).group()
processed = processed.replace(plate_num, "", -1)
except AttributeError:
plate_num = "1"
plate_num = plate_num.strip("-")
logger.debug(f"Processed after plate-num: {processed}")
day = re.search(r"\d{2}$", processed).group()
processed = processed.replace(day, "", -1)
logger.debug(f"Processed after day: {processed}")
month = re.search(r"\d{2}$", processed).group()
processed = processed.replace(month, "", -1)
processed = processed.replace("--", "")
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}"
@classmethod @classmethod
def enforce_name(cls, instr:str, data:dict|None=None) -> str: def enforce_name(cls, instr:str, data:dict|None=None) -> str:
""" """
Extends parent Extends parent
""" """
from backend.validators import RSLNamer from backend.validators import RSLNamer
data['abbreviation'] = "AR" data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data) outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None): # def construct(data:dict|None=None):
# today = datetime.now() # today = datetime.now()
@@ -1240,6 +1356,36 @@ class WastewaterArtic(BasicSubmission):
worksheet.cell(row=iii, column=jjj, value=value) worksheet.cell(row=iii, column=jjj, value=value)
return input_excel return input_excel
def adjust_to_dict_samples(self, backup:bool=False):
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
if backup:
output = []
for assoc in self.submission_sample_associations:
dicto = assoc.to_sub_dict()
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
try:
dicto['plate_name'] = old_sub.rsl_plate_num
except AttributeError:
dicto['plate_name'] = ""
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
output.append(dicto)
else:
output = super().adjust_to_dict_samples(backup=False)
return output
def custom_context_events(self):
events = super().custom_context_events()
events['Gel Box'] = self.gel_box
return events
def gel_box(self, obj):
from frontend.widgets.gel_checker import GelBox
dlg = GelBox(parent=obj)
if dlg.exec():
output = dlg.parse_form()
print(output)
# Sample Classes # Sample Classes
class BasicSample(BaseClass): class BasicSample(BaseClass):
@@ -1247,7 +1393,7 @@ class BasicSample(BaseClass):
Base of basic sample which polymorphs into BCSample and WWSample Base of basic sample which polymorphs into BCSample and WWSample
""" """
__tablename__ = "_samples" # __tablename__ = "_samples"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
@@ -1295,6 +1441,18 @@ class BasicSample(BaseClass):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.sample_type.replace('_', ' ').title().replace(' ', '')}({self.submitter_id})>" return f"<{self.sample_type.replace('_', ' ').title().replace(' ', '')}({self.submitter_id})>"
def to_sub_dict(self, submission_rsl:str) -> dict:
"""
gui friendly dictionary, extends parent method.
Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""
sample = {}
sample['submitter_id'] = self.submitter_id
sample['sample_type'] = self.sample_type
return sample
def set_attribute(self, name:str, value): def set_attribute(self, name:str, value):
""" """
Custom attribute setter Custom attribute setter
@@ -1308,59 +1466,6 @@ class BasicSample(BaseClass):
except AttributeError: except AttributeError:
logger.error(f"Attribute {name} not found") logger.error(f"Attribute {name} not found")
def to_sub_dict(self, submission_rsl:str|BasicSubmission) -> dict:
"""
Returns a dictionary of locations.
Args:
submission_rsl (str): Submission RSL number.
Returns:
dict: 'well' and sample submitter_id as 'name'
"""
match submission_rsl:
case BasicSubmission():
assoc = [item for item in self.sample_submission_associations if item.submission==submission_rsl][0]
case str():
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
sample = {}
try:
sample['well'] = f"{row_map[assoc.row]}{assoc.column}"
except KeyError as e:
logger.error(f"Unable to find row {assoc.row} in row_map.")
sample['well'] = None
sample['name'] = self.submitter_id
sample['submitter_id'] = self.submitter_id
sample['sample_type'] = self.sample_type
if isinstance(assoc.row, list):
sample['row'] = assoc.row[0]
else:
sample['row'] = assoc.row
if isinstance(assoc.column, list):
sample['column'] = assoc.column[0]
else:
sample['column'] = assoc.column
return sample
def to_hitpick(self, submission_rsl:str|None=None) -> dict|None:
"""
Outputs a dictionary usable for html plate maps.
Returns:
dict: dictionary of sample id, row and column in elution plate
"""
# Since there is no PCR, negliable result is necessary.
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
fields = self.to_sub_dict(submission_rsl=submission_rsl)
env = jinja_template_loading()
template = env.get_template("tooltip.html")
tooltip_text = template.render(fields=fields)
# tooltip_text = f"""
# Sample name: {self.submitter_id}<br>
# Well: {row_map[assoc.row]}{assoc.column}
# """
return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
@classmethod @classmethod
def find_subclasses(cls, attrs:dict|None=None, sample_type:str|None=None) -> BasicSample: def find_subclasses(cls, attrs:dict|None=None, sample_type:str|None=None) -> BasicSample:
""" """
@@ -1507,11 +1612,6 @@ class BasicSample(BaseClass):
logger.debug(f"Creating instance: {instance}") logger.debug(f"Creating instance: {instance}")
return instance return instance
def save(self):
# raise AttributeError(f"Save not implemented for {self.__class__}")
self.__database_session__.add(self)
self.__database_session__.commit()
def delete(self): def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1521,7 +1621,7 @@ class WastewaterSample(BasicSample):
""" """
Derivative wastewater sample Derivative wastewater sample
""" """
# id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True) id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
ww_processing_num = Column(String(64)) #: wastewater processing number ww_processing_num = Column(String(64)) #: wastewater processing number
ww_full_sample_id = Column(String(64)) #: full id given by entrics ww_full_sample_id = Column(String(64)) #: full id given by entrics
rsl_number = Column(String(64)) #: rsl plate identification number rsl_number = Column(String(64)) #: rsl plate identification number
@@ -1529,46 +1629,21 @@ class WastewaterSample(BasicSample):
received_date = Column(TIMESTAMP) #: Date sample received received_date = Column(TIMESTAMP) #: Date sample received
notes = Column(String(2000)) #: notes from submission form notes = Column(String(2000)) #: notes from submission form
sample_location = Column(String(8)) #: location on 24 well plate sample_location = Column(String(8)) #: location on 24 well plate
__mapper_args__ = {"polymorphic_identity": "Wastewater Sample", "polymorphic_load": "inline"} __mapper_args__ = dict(polymorphic_identity="Wastewater Sample",
polymorphic_load="inline",
inherit_condition=(id == BasicSample.id))
def to_hitpick(self, submission_rsl:str) -> dict|None: def to_sub_dict(self, submission_rsl:str) -> dict:
""" """
Outputs a dictionary usable for html plate maps. Extends parent method. gui friendly dictionary, extends parent method.
Args:
submission_rsl (str): rsl_plate_num of the submission
Returns: Returns:
dict|None: dict: dictionary of sample id, row and column in elution plate dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
""" """
sample = super().to_hitpick(submission_rsl=submission_rsl) sample = super().to_sub_dict(submission_rsl=submission_rsl)
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0] sample['ww_processing_num'] = self.ww_processing_num
# if either n1 or n2 is positive, include this sample
try:
sample['positive'] = any(["positive" in item for item in [assoc.n1_status, assoc.n2_status]])
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
try:
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(assoc.ct_n1)} ({assoc.n1_status})<br>- ct N2: {'{:.2f}'.format(assoc.ct_n2)} ({assoc.n2_status})"
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
return sample return sample
def get_recent_ww_submission(self) -> Wastewater:
"""
Gets most recent associated wastewater submission
Returns:
Wastewater: Most recent wastewater submission
"""
results = [sub for sub in self.submissions if isinstance(sub, Wastewater)]
if len(results) > 1:
results = results.sort(key=lambda sub: sub.submitted_date)
try:
return results[0]
except IndexError:
return None
@classmethod @classmethod
def parse_sample(cls, input_dict: dict) -> dict: def parse_sample(cls, input_dict: dict) -> dict:
output_dict = super().parse_sample(input_dict) output_dict = super().parse_sample(input_dict)
@@ -1592,34 +1667,27 @@ class WastewaterSample(BasicSample):
del output_dict['collection_date'] del output_dict['collection_date']
return output_dict return output_dict
def to_sub_dict(self, submission_rsl: str | BasicSubmission) -> dict: def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
sample = super().to_sub_dict(submission_rsl) # assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
if self.ww_processing_num != None: subs = self.submissions[:self.submissions.index(current_artic_submission)]
sample['ww_processing_num'] = self.ww_processing_num subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
else: logger.debug(f"Submissions up to current artic submission: {subs}")
sample['ww_processing_num'] = self.submitter_id
try: try:
assoc = [item for item in self.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1] return subs[-1]
except: except IndexError:
assoc = None return None
if assoc != None:
try:
sample['ct'] = f"{assoc.ct_n1:.2f}, {assoc.ct_n2:.2f}"
except TypeError:
sample['ct'] = "None, None"
sample['source_plate'] = assoc.submission.rsl_plate_num
sample['source_well'] = f"{row_map[assoc.row]}{assoc.column}"
return sample
class BacterialCultureSample(BasicSample): class BacterialCultureSample(BasicSample):
""" """
base of bacterial culture sample base of bacterial culture sample
""" """
# id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True) id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
organism = Column(String(64)) #: bacterial specimen organism = Column(String(64)) #: bacterial specimen
concentration = Column(String(16)) #: sample concentration concentration = Column(String(16)) #: sample concentration
control = relationship("Control", back_populates="sample", uselist=False) control = relationship("Control", back_populates="sample", uselist=False)
__mapper_args__ = {"polymorphic_identity": "Bacterial Culture Sample", "polymorphic_load": "inline"} __mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample",
polymorphic_load="inline",
inherit_condition=(id == BasicSample.id))
def to_sub_dict(self, submission_rsl:str) -> dict: def to_sub_dict(self, submission_rsl:str) -> dict:
""" """
@@ -1632,15 +1700,18 @@ class BacterialCultureSample(BasicSample):
sample['name'] = self.submitter_id sample['name'] = self.submitter_id
sample['organism'] = self.organism sample['organism'] = self.organism
sample['concentration'] = self.concentration sample['concentration'] = self.concentration
return sample
def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
sample = super().to_hitpick(submission_rsl)
if self.control != None: if self.control != None:
sample['colour'] = [0,128,0] sample['colour'] = [0,128,0]
sample['tooltip'] += f"<br>- Control: {self.control.controltype.name} - {self.control.controltype.targets}" sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
return sample return sample
# def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
# sample = super().to_hitpick(submission_rsl)
# if self.control != None:
# sample['colour'] = [0,128,0]
# sample['tooltip'] += f"<br>- Control: {self.control.controltype.name} - {self.control.controltype.targets}"
# return sample
# Submission to Sample Associations # Submission to Sample Associations
class SubmissionSampleAssociation(BaseClass): class SubmissionSampleAssociation(BaseClass):
@@ -1649,7 +1720,7 @@ class SubmissionSampleAssociation(BaseClass):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
__tablename__ = "_submission_sample" # __tablename__ = "_submission_sample"
sample_id = Column(INTEGER, ForeignKey("_samples.id"), nullable=False) #: id of associated sample sample_id = Column(INTEGER, ForeignKey("_samples.id"), nullable=False) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
@@ -1688,7 +1759,12 @@ class SubmissionSampleAssociation(BaseClass):
Returns: Returns:
dict: Updated dictionary with row, column and well updated dict: Updated dictionary with row, column and well updated
""" """
# Get sample info
sample = self.sample.to_sub_dict(submission_rsl=self.submission) sample = self.sample.to_sub_dict(submission_rsl=self.submission)
# sample = {}
sample['name'] = self.sample.submitter_id
# sample['submitter_id'] = self.sample.submitter_id
# sample['sample_type'] = self.sample.sample_type
sample['row'] = self.row sample['row'] = self.row
sample['column'] = self.column sample['column'] = self.column
try: try:
@@ -1696,6 +1772,33 @@ class SubmissionSampleAssociation(BaseClass):
except KeyError as e: except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.") logger.error(f"Unable to find row {self.row} in row_map.")
sample['well'] = None sample['well'] = None
sample['plate_name'] = self.submission.rsl_plate_num
sample['positive'] = False
return sample
def to_hitpick(self) -> dict|None:
"""
Outputs a dictionary usable for html plate maps.
Returns:
dict: dictionary of sample id, row and column in elution plate
"""
# Since there is no PCR, negliable result is necessary.
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
sample = self.to_sub_dict()
env = jinja_template_loading()
template = env.get_template("tooltip.html")
tooltip_text = template.render(fields=sample)
try:
tooltip_text += sample['tooltip']
except KeyError:
pass
# tooltip_text = f"""
# Sample name: {self.submitter_id}<br>
# Well: {row_map[fields['row']]}{fields['column']}
# """
sample.update(dict(name=self.sample.submitter_id[:10], tooltip=tooltip_text))
return sample return sample
@classmethod @classmethod
@@ -1831,14 +1934,6 @@ class SubmissionSampleAssociation(BaseClass):
instance = used_cls(submission=submission, sample=sample, **kwargs) instance = used_cls(submission=submission, sample=sample, **kwargs)
return instance return instance
def save(self):
"""
Adds this instance to the database and commits.
"""
self.__database_session__.add(self)
self.__database_session__.commit()
return None
def delete(self): def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1846,10 +1941,32 @@ class WastewaterAssociation(SubmissionSampleAssociation):
""" """
Derivative custom Wastewater/Submission Association... fancy. Derivative custom Wastewater/Submission Association... fancy.
""" """
sample_id = Column(INTEGER, ForeignKey('_submissionsampleassociation.sample_id'), primary_key=True)
submission_id = Column(INTEGER, ForeignKey('_submissionsampleassociation.submission_id'), primary_key=True)
ct_n1 = Column(FLOAT(2)) #: AKA ct for N1 ct_n1 = Column(FLOAT(2)) #: AKA ct for N1
ct_n2 = Column(FLOAT(2)) #: AKA ct for N2 ct_n2 = Column(FLOAT(2)) #: AKA ct for N2
n1_status = Column(String(32)) #: positive or negative for N1 n1_status = Column(String(32)) #: positive or negative for N1
n2_status = Column(String(32)) #: positive or negative for N2 n2_status = Column(String(32)) #: positive or negative for N2
pcr_results = Column(JSON) #: imported PCR status from QuantStudio pcr_results = Column(JSON) #: imported PCR status from QuantStudio
__mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"} # __mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
__mapper_args__ = dict(polymorphic_identity="Wastewater Association",
polymorphic_load="inline",
inherit_condition=(sample_id == SubmissionSampleAssociation.sample_id))
def to_sub_dict(self) -> dict:
sample = super().to_sub_dict()
sample['ct'] = f"({self.ct_n1}, {self.ct_n2})"
try:
sample['positive'] = any(["positive" in item for item in [self.n1_status, self.n2_status]])
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't check positives for {self.sample.rsl_number}. Looks like there isn't PCR data.")
return sample
def to_hitpick(self) -> dict | None:
sample = super().to_hitpick()
try:
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})<br>- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})"
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.")
return sample

View File

@@ -503,7 +503,12 @@ class EquipmentParser(object):
def get_asset_number(self, input:str) -> str: def get_asset_number(self, input:str) -> str:
regex = Equipment.get_regex() regex = Equipment.get_regex()
logger.debug(f"Using equipment regex: {regex} on {input}")
try:
return regex.search(input).group().strip("-") return regex.search(input).group().strip("-")
except AttributeError:
return input
def parse_equipment(self): def parse_equipment(self):
logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}") logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
@@ -512,7 +517,10 @@ class EquipmentParser(object):
# logger.debug(f"Sheets: {sheets}") # logger.debug(f"Sheets: {sheets}")
for sheet in self.xl.sheet_names: for sheet in self.xl.sheet_names:
df = self.xl.parse(sheet, header=None, dtype=object) df = self.xl.parse(sheet, header=None, dtype=object)
try:
relevant = [item for item in self.map if item['sheet']==sheet] relevant = [item for item in self.map if item['sheet']==sheet]
except (TypeError, KeyError):
continue
# logger.debug(f"Relevant equipment: {pformat(relevant)}") # logger.debug(f"Relevant equipment: {pformat(relevant)}")
previous_asset = "" previous_asset = ""
for equipment in relevant: for equipment in relevant:
@@ -524,7 +532,10 @@ class EquipmentParser(object):
asset = self.get_asset_number(input=asset) asset = self.get_asset_number(input=asset)
eq = Equipment.query(asset_number=asset) eq = Equipment.query(asset_number=asset)
process = df.iat[equipment['process']['row']-1, equipment['process']['column']-1] process = df.iat[equipment['process']['row']-1, equipment['process']['column']-1]
output.append(PydEquipment(name=eq.name, process=process, role=equipment['role'], asset_number=asset, nickname=eq.nickname)) try:
output.append(PydEquipment(name=eq.name, processes=[process], role=equipment['role'], asset_number=asset, nickname=eq.nickname))
except AttributeError:
logger.error(f"Unable to add {eq} to PydEquipment list.")
# logger.debug(f"Here is the output so far: {pformat(output)}") # logger.debug(f"Here is the output so far: {pformat(output)}")
return output return output

View File

@@ -113,12 +113,15 @@ class RSLNamer(object):
@classmethod @classmethod
def construct_new_plate_name(cls, data:dict) -> str: def construct_new_plate_name(cls, data:dict) -> str:
if "submitted_date" in data.keys(): if "submitted_date" in data.keys():
if isinstance(data['submitted_date'], dict):
if data['submitted_date']['value'] != None: if data['submitted_date']['value'] != None:
today = data['submitted_date']['value'] today = data['submitted_date']['value']
else: else:
today = datetime.now() today = datetime.now()
else: else:
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr) today = data['submitted_date']
else:
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", data['rsl_plate_num'])
try: try:
today = parse(today.group()) today = parse(today.group())
except AttributeError: except AttributeError:

View File

@@ -106,7 +106,7 @@ class PydReagent(BaseModel):
if self.model_extra != None: if self.model_extra != None:
self.__dict__.update(self.model_extra) self.__dict__.update(self.model_extra)
logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}") logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
reagent = Reagent.query(lot_number=self.lot) reagent = Reagent.query(lot_number=self.lot, name=self.name)
logger.debug(f"Result: {reagent}") logger.debug(f"Result: {reagent}")
if reagent == None: if reagent == None:
reagent = Reagent() reagent = Reagent()
@@ -209,6 +209,56 @@ class PydSample(BaseModel, extra='allow'):
instance.metadata.session.rollback() instance.metadata.session.rollback()
return instance, out_associations, report return instance, out_associations, report
class PydEquipment(BaseModel, extra='ignore'):
asset_number: str
name: str
nickname: str|None
processes: List[str]|None
role: str|None
@field_validator('processes', mode='before')
@classmethod
def make_empty_list(cls, value):
# logger.debug(f"Pydantic value: {value}")
if value == None:
value = ['']
if len(value)==0:
value=['']
return value
# def toForm(self, parent):
# from frontend.widgets.equipment_usage import EquipmentCheckBox
# return EquipmentCheckBox(parent=parent, equipment=self)
def toSQL(self, submission:BasicSubmission|str=None):
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_number=submission)
equipment = Equipment.query(asset_number=self.asset_number)
if equipment == None:
return
if submission != None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
process = Process.query(name=self.processes[0])
if process == None:
from frontend.widgets.pop_ups import QuestionAsker
dlg = QuestionAsker(title="Add Process?", message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
if dlg.exec():
kit = submission.extraction_kit
submission_type = submission.submission_type
process = Process(name=self.processes[0])
process.kit_types.append(kit)
process.submission_types.append(submission_type)
process.equipment.append(equipment)
process.save()
assoc.process = process
assoc.role = self.role
# equipment.equipment_submission_associations.append(assoc)
equipment.equipment_submission_associations.append(assoc)
else:
assoc = None
return equipment, assoc
class PydSubmission(BaseModel, extra='allow'): class PydSubmission(BaseModel, extra='allow'):
filepath: Path filepath: Path
submission_type: dict|None submission_type: dict|None
@@ -453,8 +503,10 @@ class PydSubmission(BaseModel, extra='allow'):
for equip in self.equipment: for equip in self.equipment:
equip, association = equip.toSQL(submission=instance) equip, association = equip.toSQL(submission=instance)
if association != None: if association != None:
association.save()
logger.debug(f"Equipment association SQL object to be added to submission: {association.__dict__}") logger.debug(f"Equipment association SQL object to be added to submission: {association.__dict__}")
instance.submission_equipment_associations.append(association) instance.submission_equipment_associations.append(association)
case _: case _:
try: try:
instance.set_attribute(key=key, value=value) instance.set_attribute(key=key, value=value)
@@ -570,9 +622,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"New reagents: {new_reagents}") logger.debug(f"New reagents: {new_reagents}")
logger.debug(f"New info: {new_info}") logger.debug(f"New info: {new_info}")
# get list of sheet names # get list of sheet names
sheets = workbook.sheetnames for sheet in workbook.sheetnames:
# logger.debug(workbook.sheetnames)
for sheet in sheets:
# open sheet # open sheet
worksheet=workbook[sheet] worksheet=workbook[sheet]
# Get relevant reagents for that sheet # Get relevant reagents for that sheet
@@ -613,11 +663,14 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Workbook sheets: {workbook.sheetnames}") logger.debug(f"Workbook sheets: {workbook.sheetnames}")
worksheet = workbook[sample_info["lookup_table"]['sheet']] worksheet = workbook[sample_info["lookup_table"]['sheet']]
samples = sorted(self.samples, key=attrgetter('column', 'row')) samples = sorted(self.samples, key=attrgetter('column', 'row'))
custom_sampler = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type).adjust_autofill_samples
samples = custom_sampler(samples=samples)
logger.debug(f"Samples: {pformat(samples)}") logger.debug(f"Samples: {pformat(samples)}")
# Fail safe against multiple instances of the same sample # Fail safe against multiple instances of the same sample
for iii, sample in enumerate(samples, start=1): for iii, sample in enumerate(samples, start=1):
row = sample_info['lookup_table']['start_row'] + iii row = sample_info['lookup_table']['start_row'] + iii
fields = [field for field in list(sample.model_fields.keys()) + list(sample.model_extra.keys()) if field in sample_info['sample_columns'].keys()] fields = [field for field in list(sample.model_fields.keys()) + list(sample.model_extra.keys()) if field in sample_info['sample_columns'].keys()]
for field in fields: for field in fields:
column = sample_info['sample_columns'][field] column = sample_info['sample_columns'][field]
value = getattr(sample, field) value = getattr(sample, field)
@@ -631,6 +684,42 @@ class PydSubmission(BaseModel, extra='allow'):
worksheet.cell(row=row, column=column, value=value) worksheet.cell(row=row, column=column, value=value)
return workbook return workbook
def autofill_equipment(self, workbook:Workbook) -> Workbook:
equipment_map = SubmissionType.query(name=self.submission_type['value']).construct_equipment_map()
logger.debug(f"Equipment map: {equipment_map}")
# See if all equipment has a location map
# If not, create a new sheet to store them in.
if not all([len(item.keys()) > 1 for item in equipment_map]):
logger.warning("Creating 'Equipment' sheet to hold unmapped equipment")
workbook.create_sheet("Equipment")
equipment = []
for ii, equip in enumerate(self.equipment, start=1):
loc = [item for item in equipment_map if item['role'] == equip.role][0]
try:
loc['name']['value'] = equip.name
loc['process']['value'] = equip.processes[0]
except KeyError:
loc['name'] = dict(row=ii, column=2)
loc['process'] = dict(row=ii, column=3)
loc['name']['value'] = equip.name
loc['process']['value'] = equip.processes[0]
loc['sheet'] = "Equipment"
equipment.append(loc)
logger.debug(f"Using equipment: {equipment}")
for sheet in workbook.sheetnames:
logger.debug(f"Looking at: {sheet}")
worksheet = workbook[sheet]
relevant = [item for item in equipment if item['sheet'] == sheet]
for rel in relevant:
match sheet:
case "Equipment":
worksheet.cell(row=rel['name']['row'], column=1, value=rel['role'])
case _:
pass
worksheet.cell(row=rel['name']['row'], column=rel['name']['column'], value=rel['name']['value'])
worksheet.cell(row=rel['process']['row'], column=rel['process']['column'], value=rel['process']['value'])
return workbook
def construct_filename(self) -> str: def construct_filename(self) -> str:
""" """
Creates filename for this instance Creates filename for this instance
@@ -774,42 +863,6 @@ class PydKit(BaseModel):
[item.toSQL(instance) for item in self.reagent_types] [item.toSQL(instance) for item in self.reagent_types]
return instance, report return instance, report
class PydEquipment(BaseModel, extra='ignore'):
asset_number: str
name: str
nickname: str|None
process: str|None
role: str|None
# @field_validator('process')
# @classmethod
# def remove_dupes(cls, value):
# if isinstance(value, list):
# return list(set(value))
# else:
# return value
# def toForm(self, parent):
# from frontend.widgets.equipment_usage import EquipmentCheckBox
# return EquipmentCheckBox(parent=parent, equipment=self)
def toSQL(self, submission:BasicSubmission|str=None):
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_number=submission)
equipment = Equipment.query(asset_number=self.asset_number)
if equipment == None:
return
if submission != None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
assoc.process = self.process
assoc.role = self.role
# equipment.equipment_submission_associations.append(assoc)
equipment.equipment_submission_associations.append(assoc)
else:
assoc = None
return equipment, assoc
class PydEquipmentRole(BaseModel): class PydEquipmentRole(BaseModel):
name: str name: str

View File

@@ -10,17 +10,14 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog): class EquipmentUsage(QDialog):
def __init__(self, parent, submission_type:SubmissionType|str, submission:BasicSubmission) -> QDialog: def __init__(self, parent, submission:BasicSubmission) -> QDialog:
super().__init__(parent) super().__init__(parent)
self.submission = submission
self.setWindowTitle("Equipment Checklist") self.setWindowTitle("Equipment Checklist")
self.used_equipment = submission.get_used_equipment() self.used_equipment = self.submission.get_used_equipment()
self.kit = self.submission.extraction_kit
logger.debug(f"Existing equipment: {self.used_equipment}") logger.debug(f"Existing equipment: {self.used_equipment}")
if isinstance(submission_type, str): self.opt_equipment = submission.submission_type.get_equipment()
self.submission_type = SubmissionType.query(name=submission_type)
else:
self.submission_type = submission_type
# self.static_equipment = submission_type.get_equipment()
self.opt_equipment = self.submission_type.get_equipment()
logger.debug(f"EquipmentRoles: {self.opt_equipment}") logger.debug(f"EquipmentRoles: {self.opt_equipment}")
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setLayout(self.layout) self.setLayout(self.layout)
@@ -31,20 +28,44 @@ class EquipmentUsage(QDialog):
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)
label = self.LabelRow(parent=self)
self.layout.addWidget(label)
for eq in self.opt_equipment: for eq in self.opt_equipment:
self.layout.addWidget(eq.toForm(parent=self, submission_type=self.submission_type, used=self.used_equipment)) widg = eq.toForm(parent=self, submission_type=self.submission.submission_type, used=self.used_equipment)
self.layout.addWidget(widg)
widg.update_processes()
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
def parse_form(self): def parse_form(self):
output = [] output = []
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):
match widget: match widget:
case (EquipmentCheckBox()|RoleComboBox()) : case RoleComboBox() :
if widget.check.isChecked():
output.append(widget.parse_form()) output.append(widget.parse_form())
case _: case _:
pass pass
return [item for item in output if item != None] return [item for item in output if item != None]
class LabelRow(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
self.check = QCheckBox()
self.layout.addWidget(self.check)
self.check.stateChanged.connect(self.check_all)
for item in ["Role", "Equipment", "Process"]:
l = QLabel(item)
l.setMaximumWidth(200)
l.setMinimumWidth(200)
self.layout.addWidget(l)
self.setLayout(self.layout)
def check_all(self):
for object in self.parent().findChildren(QCheckBox):
object.setChecked(self.check.isChecked())
class EquipmentCheckBox(QWidget): class EquipmentCheckBox(QWidget):
def __init__(self, parent, equipment:PydEquipment) -> None: def __init__(self, parent, equipment:PydEquipment) -> None:
@@ -56,7 +77,6 @@ class EquipmentCheckBox(QWidget):
self.check = QCheckBox() self.check = QCheckBox()
if equipment.static: if equipment.static:
self.check.setChecked(True) self.check.setChecked(True)
# self.check.setEnabled(False)
if equipment.nickname != None: if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})" text = f"{equipment.name} ({equipment.nickname})"
else: else:
@@ -87,28 +107,42 @@ class RoleComboBox(QWidget):
else: else:
self.check.setChecked(True) self.check.setChecked(True)
self.box = QComboBox() self.box = QComboBox()
self.box.setMaximumWidth(125) self.box.setMaximumWidth(200)
self.box.setMinimumWidth(125) self.box.setMinimumWidth(200)
self.box.addItems([item.name for item in role.equipment]) self.box.addItems([item.name for item in role.equipment])
self.box.currentTextChanged.connect(self.update_processes)
# self.check = QCheckBox() # self.check = QCheckBox()
# self.layout.addWidget(label) # self.layout.addWidget(label)
self.process = QComboBox() self.process = QComboBox()
self.process.setMaximumWidth(125) self.process.setMaximumWidth(200)
self.process.setMinimumWidth(125) self.process.setMinimumWidth(200)
self.process.setEditable(True) self.process.setEditable(True)
# self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name)) # self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
self.process.addItems(role.processes) # self.process.addItems(role.processes)
self.layout.addWidget(self.check) self.layout.addWidget(self.check)
self.layout.addWidget(QLabel(f"{role.name}:")) label = QLabel(f"{role.name}:")
label.setMinimumWidth(200)
label.setMaximumWidth(200)
label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.layout.addWidget(label)
self.layout.addWidget(self.box) self.layout.addWidget(self.box)
self.layout.addWidget(self.process) self.layout.addWidget(self.process)
# self.layout.addWidget(self.check) # self.layout.addWidget(self.check)
self.setLayout(self.layout) self.setLayout(self.layout)
# self.update_processes()
def update_processes(self):
equip = self.box.currentText()
logger.debug(f"Updating equipment: {equip}")
equip2 = [item for item in self.role.equipment if item.name==equip][0]
logger.debug(f"Using: {equip2}")
self.process.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.processes])
def parse_form(self) -> str|None: def parse_form(self) -> str|None:
eq = Equipment.query(name=self.box.currentText()) eq = Equipment.query(name=self.box.currentText())
if self.check: # if self.check.isChecked():
return PydEquipment(name=eq.name, process=self.process.currentText(), role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname) return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
else: # else:
return None # return None

View File

@@ -0,0 +1,93 @@
# import required modules
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import *
import sys
from PyQt6.QtWidgets import QWidget
import numpy as np
import pyqtgraph as pg
from PyQt6.QtGui import *
from PyQt6.QtCore import *
from PIL import Image
import numpy as np
# Main window class
class GelBox(QDialog):
def __init__(self, parent):
super().__init__(parent)
# setting title
self.setWindowTitle("PyQtGraph")
# setting geometry
self.setGeometry(100, 100, 600, 500)
# icon
icon = QIcon("skin.png")
# setting icon to the window
self.setWindowIcon(icon)
# calling method
self.UiComponents()
# showing all the widgets
# self.show()
# method for components
def UiComponents(self):
# widget = QWidget()
# setting configuration options
pg.setConfigOptions(antialias=True)
# creating image view object
self.imv = pg.ImageView()
img = np.array(Image.open("C:\\Users\\lwark\\Desktop\\PLATE1_17012024_103607AM_1_4x26.jpg").rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0]))
layout = QGridLayout()
# setting this layout to the widget
# widget.setLayout(layout)
# plot window goes on right side, spanning 3 rows
layout.addWidget(self.imv, 0, 0,20,20)
# setting this widget as central widget of the main window
self.form = ControlsForm(parent=self)
layout.addWidget(self.form,21,1,1,4)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 21, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
# self.buttonBox.clicked.connect(self.submit)
self.setLayout(layout)
def parse_form(self):
return self.form.parse_form()
class ControlsForm(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QGridLayout()
columns = []
rows = []
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
label = QLabel(item)
self.layout.addWidget(label, 0, iii,1,1)
if iii > 1:
columns.append(item)
for iii, item in enumerate(["RSL-NTC", "ENC-NTC", "NTC"], start=1):
label = QLabel(item)
self.layout.addWidget(label, iii, 0, 1, 1)
rows.append(item)
for iii, item in enumerate(["Processing Negative (PBS)", "Extraction Negative (Extraction buffers ONLY)", "Artic no-template control (mastermix ONLY)"], start=1):
label = QLabel(item)
self.layout.addWidget(label, iii, 1, 1, 1)
for iii in range(3):
for jjj in range(3):
widge = QLineEdit()
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
self.setLayout(self.layout)
def parse_form(self):
dicto = {}
for le in self.findChildren(QLineEdit):
label = [item.strip() for item in le.objectName().split(" : ")]
if label[0] not in dicto.keys():
dicto[label[0]] = {}
dicto[label[0]][label[1]] = le.text()
return dicto

View File

@@ -0,0 +1,198 @@
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
QLabel, QDialogButtonBox, QToolBar, QTextEdit)
from PyQt6.QtGui import QAction, QPixmap
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt
from PyQt6 import QtPrintSupport
from backend.db.models import BasicSubmission
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from tools import check_if_app, jinja_template_loading
from .functions import select_save_file
from io import BytesIO
from xhtml2pdf import pisa
import logging, base64
from getpass import getuser
from datetime import datetime
from pprint import pformat
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, parent, sub:BasicSubmission) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError:
self.app = None
self.setWindowTitle("Submission Details")
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# sub = BasicSubmission.query(id=id)
self.base_dict = sub.to_dict(full_data=True)
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(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
logger.debug(f"Hitpicking plate...")
self.plate_dicto = sub.hitpick_plate()
logger.debug(f"Making platemap...")
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
self.template = env.get_template("submission_details.html")
self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView()
webview.setMinimumSize(900, 500)
webview.setMaximumSize(900, 500)
webview.setHtml(self.html)
self.layout = QVBoxLayout()
interior.resize(900, 500)
interior.setWidget(webview)
self.setFixedSize(900, 500)
# button to export a pdf version
btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(900)
btn.clicked.connect(self.export)
def export(self):
"""
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
del self.base_dict['platemap']
export_map = make_plate_map(self.plate_dicto)
image_io = BytesIO()
try:
export_map.save(image_io, 'JPEG')
except AttributeError:
logger.error(f"No plate map found")
self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
self.html2 = self.template.render(sub=self.base_dict)
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(self.html2, dest=f)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
msg = QMessageBox()
msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()
class BarcodeWindow(QDialog):
def __init__(self, rsl_num:str):
super().__init__()
# set the title
self.setWindowTitle("Image")
self.layout = QVBoxLayout()
# setting the geometry of window
self.setGeometry(0, 0, 400, 300)
# creating label
self.label = QLabel()
self.img = make_plate_barcode(rsl_num)
self.pixmap = QPixmap()
self.pixmap.loadFromData(self.img)
# adding image to label
self.label.setPixmap(self.pixmap)
# show all the widgets]
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.addWidget(self.label)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self._createActions()
self._createToolBar()
self._connectActions()
def _createToolBar(self):
"""
adds items to menu bar
"""
toolbar = QToolBar("My main toolbar")
toolbar.addAction(self.printAction)
def _createActions(self):
"""
creates actions
"""
self.printAction = QAction("&Print", self)
def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
self.printAction.triggered.connect(self.print_barcode)
def print_barcode(self):
"""
Sends barcode image to printer.
"""
printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec():
self.handle_paint_request(printer, self.pixmap.toImage())
def handle_paint_request(self, printer:QtPrintSupport.QPrinter, im):
logger.debug(f"Hello from print handler.")
painter = QPainter(printer)
image = QPixmap.fromImage(im)
painter.drawPixmap(120, -20, image)
painter.end()
class SubmissionComment(QDialog):
"""
a window for adding comment text to a submission
"""
def __init__(self, parent, submission:BasicSubmission) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent
print(f"App: {self.app}")
except AttributeError:
pass
self.submission = submission
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
# create text field
self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False)
self.txt_editor.setText("Add Comment")
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()
self.setFixedSize(400, 300)
self.layout.addWidget(self.txt_editor)
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def parse_form(self):
"""
Adds comment to submission object.
"""
commenter = getuser()
comment = self.txt_editor.toPlainText()
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
full_comment = [{"name":commenter, "time": dt, "text": comment}]
logger.debug(f"Full comment: {full_comment}")
return full_comment

View File

@@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation, Process from backend.db.models import BasicSubmission, Equipment
from backend.excel import make_report_html, make_report_xlsx from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from xhtml2pdf import pisa from xhtml2pdf import pisa
@@ -95,8 +95,8 @@ class SubmissionsSheet(QTableView):
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) self.setSortingEnabled(True)
# self.doubleClicked.connect(self.show_details)
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
def setData(self) -> None: def setData(self) -> None:
""" """
@@ -114,16 +114,16 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data)) proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel) self.setModel(proxyModel)
def show_details(self) -> None: # def show_details(self, submission:BasicSubmission) -> None:
""" # """
creates detailed data to show in seperate window # creates detailed data to show in seperate window
""" # """
logger.debug(f"Sheet.app: {self.app}") # logger.debug(f"Sheet.app: {self.app}")
index = (self.selectionModel().currentIndex()) # # index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data() # # value = index.sibling(index.row(),0).data()
dlg = SubmissionDetails(parent=self, id=value) # dlg = SubmissionDetails(parent=self, sub=submission)
if dlg.exec(): # if dlg.exec():
pass # pass
def create_barcode(self) -> None: def create_barcode(self) -> None:
""" """
@@ -154,38 +154,47 @@ class SubmissionsSheet(QTableView):
Args: Args:
event (_type_): the item of interest event (_type_): the item of interest
""" """
id = self.selectionModel().currentIndex()
id = id.sibling(id.row(),0).data()
submission = BasicSubmission.query(id=id)
self.menu = QMenu(self) self.menu = QMenu(self)
renameAction = QAction('Delete', self) # renameAction = QAction('Delete', self)
detailsAction = QAction('Details', self) # detailsAction = QAction('Details', self)
# barcodeAction = QAction("Print Barcode", self) # commentAction = QAction("Add Comment", self)
commentAction = QAction("Add Comment", self) # equipAction = QAction("Add Equipment", self)
backupAction = QAction("Backup", self) # backupAction = QAction("Export", self)
equipAction = QAction("Add Equipment", self) # renameAction.triggered.connect(lambda: self.delete_item(submission))
# hitpickAction = QAction("Hitpicks", self) # detailsAction.triggered.connect(lambda: self.show_details(submission))
renameAction.triggered.connect(lambda: self.delete_item(event)) # commentAction.triggered.connect(lambda: self.add_comment(submission))
detailsAction.triggered.connect(lambda: self.show_details()) # backupAction.triggered.connect(lambda: self.regenerate_submission_form(submission))
# barcodeAction.triggered.connect(lambda: self.create_barcode()) # equipAction.triggered.connect(lambda: self.add_equipment(submission))
commentAction.triggered.connect(lambda: self.add_comment()) # self.menu.addAction(detailsAction)
backupAction.triggered.connect(lambda: self.regenerate_submission_form()) # self.menu.addAction(renameAction)
equipAction.triggered.connect(lambda: self.add_equipment()) # self.menu.addAction(commentAction)
# hitpickAction.triggered.connect(lambda: self.hit_pick()) # self.menu.addAction(backupAction)
self.menu.addAction(detailsAction) # self.menu.addAction(equipAction)
self.menu.addAction(renameAction) self.con_actions = submission.custom_context_events()
# self.menu.addAction(barcodeAction) for k in self.con_actions.keys():
self.menu.addAction(commentAction) logger.debug(f"Adding {k}")
self.menu.addAction(backupAction) action = QAction(k, self)
self.menu.addAction(equipAction) action.triggered.connect(lambda _, action_name=k: self.triggered_action(action_name=action_name))
# self.menu.addAction(hitpickAction) self.menu.addAction(action)
# add other required actions # add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
def triggered_action(self, action_name:str):
logger.debug(f"Action: {action_name}")
logger.debug(f"Responding with {self.con_actions[action_name]}")
func = self.con_actions[action_name]
func(obj=self)
def add_equipment(self): def add_equipment(self):
index = (self.selectionModel().currentIndex()) index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data() value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value) self.add_equipment_function(rsl_plate_id=value)
def add_equipment_function(self, rsl_plate_id): def add_equipment_function(self, submission:BasicSubmission):
submission = BasicSubmission.query(id=rsl_plate_id) # submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission) dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission)
if dlg.exec(): if dlg.exec():
@@ -193,29 +202,33 @@ class SubmissionsSheet(QTableView):
logger.debug(f"We've got equipment: {equipment}") logger.debug(f"We've got equipment: {equipment}")
for equip in equipment: for equip in equipment:
e = Equipment.query(name=equip.name) e = Equipment.query(name=equip.name)
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e) # assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
process = Process.query(name=equip.process) # process = Process.query(name=equip.processes)
assoc.process = process # assoc.process = process
assoc.role = equip.role # assoc.role = equip.role
_, assoc = equip.toSQL(submission=submission)
# submission.submission_equipment_associations.append(assoc) # submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}") logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save() # submission.save()
assoc.save() assoc.save()
else:
pass
def delete_item(self, event): def delete_item(self, submission:BasicSubmission):
""" """
Confirms user deletion and sends id to backend for deletion. Confirms user deletion and sends id to backend for deletion.
Args: Args:
event (_type_): the item of interest event (_type_): the item of interest
""" """
index = (self.selectionModel().currentIndex()) # index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data() # value = index.sibling(index.row(),0).data()
logger.debug(index) # logger.debug(index)
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n") # msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {submission.rsl_plate_num}?\n")
if msg.exec(): if msg.exec():
# delete_submission(id=value) # delete_submission(id=value)
BasicSubmission.query(id=value).delete() submission.delete()
else: else:
return return
self.setData() self.setData()
@@ -424,221 +437,11 @@ class SubmissionsSheet(QTableView):
writer.close() writer.close()
self.report.add_result(report) self.report.add_result(report)
def regenerate_submission_form(self): def regenerate_submission_form(self, submission:BasicSubmission):
index = (self.selectionModel().currentIndex()) # index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data() # value = index.sibling(index.row(),0).data()
logger.debug(index) # logger.debug(index)
# msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n") # sub = BasicSubmission.query(id=value)
# if msg.exec(): fname = select_save_file(self, default_name=submission.to_pydantic().construct_filename(), extension="xlsx")
# delete_submission(id=value) submission.backup(fname=fname, full_backup=False)
sub = BasicSubmission.query(id=value)
fname = select_save_file(self, default_name=sub.to_pydantic().construct_filename(), extension="xlsx")
sub.backup(fname=fname, full_backup=False)
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, parent, id:int) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError:
self.app = None
self.setWindowTitle("Submission Details")
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# get submision from db
# sub = lookup_submissions(ctx=ctx, id=id)
sub = BasicSubmission.query(id=id)
logger.debug(f"Submission details data:\n{pformat(sub.to_dict())}")
self.base_dict = sub.to_dict(full_data=True)
# 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(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
logger.debug(f"Hitpicking plate...")
self.plate_dicto = sub.hitpick_plate()
logger.debug(f"Making platemap...")
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
# logger.debug(f"Platemap: {self.base_dict['platemap']}")
# logger.debug(f"platemap: {platemap}")
# image_io = BytesIO()
# try:
# platemap.save(image_io, 'JPEG')
# except AttributeError:
# logger.error(f"No plate map found for {sub.rsl_plate_num}")
# self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
self.template = env.get_template("submission_details.html")
self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView()
webview.setMinimumSize(900, 500)
webview.setMaximumSize(900, 500)
webview.setHtml(self.html)
self.layout = QVBoxLayout()
interior.resize(900, 500)
interior.setWidget(webview)
self.setFixedSize(900, 500)
# button to export a pdf version
btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(900)
btn.clicked.connect(self.export)
def export(self):
"""
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
# try:
# home_dir = Path(self.ctx.directory_path).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
# except FileNotFoundError:
# home_dir = Path.home().resolve().__str__()
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
# if fname.__str__() == ".":
# logger.debug("Saving pdf was cancelled.")
# return
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
del self.base_dict['platemap']
export_map = make_plate_map(self.plate_dicto)
image_io = BytesIO()
try:
export_map.save(image_io, 'JPEG')
except AttributeError:
logger.error(f"No plate map found")
self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
self.html2 = self.template.render(sub=self.base_dict)
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(self.html2, dest=f)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
msg = QMessageBox()
msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()
class BarcodeWindow(QDialog):
def __init__(self, rsl_num:str):
super().__init__()
# set the title
self.setWindowTitle("Image")
self.layout = QVBoxLayout()
# setting the geometry of window
self.setGeometry(0, 0, 400, 300)
# creating label
self.label = QLabel()
self.img = make_plate_barcode(rsl_num)
self.pixmap = QPixmap()
self.pixmap.loadFromData(self.img)
# adding image to label
self.label.setPixmap(self.pixmap)
# show all the widgets]
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.addWidget(self.label)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self._createActions()
self._createToolBar()
self._connectActions()
def _createToolBar(self):
"""
adds items to menu bar
"""
toolbar = QToolBar("My main toolbar")
toolbar.addAction(self.printAction)
def _createActions(self):
"""
creates actions
"""
self.printAction = QAction("&Print", self)
def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
self.printAction.triggered.connect(self.print_barcode)
def print_barcode(self):
"""
Sends barcode image to printer.
"""
printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec():
self.handle_paint_request(printer, self.pixmap.toImage())
def handle_paint_request(self, printer:QtPrintSupport.QPrinter, im):
logger.debug(f"Hello from print handler.")
painter = QPainter(printer)
image = QPixmap.fromImage(im)
painter.drawPixmap(120, -20, image)
painter.end()
class SubmissionComment(QDialog):
"""
a window for adding comment text to a submission
"""
def __init__(self, parent, rsl:str) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent
print(f"App: {self.app}")
except AttributeError:
pass
self.rsl = rsl
self.setWindowTitle(f"{self.rsl} Submission Comment")
# create text field
self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False)
self.txt_editor.setText("Add Comment")
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()
self.setFixedSize(400, 300)
self.layout.addWidget(self.txt_editor)
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def add_comment(self):
"""
Adds comment to submission object.
"""
commenter = getuser()
comment = self.txt_editor.toPlainText()
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
full_comment = [{"name":commenter, "time": dt, "text": comment}]
logger.debug(f"Full comment: {full_comment}")
sub = BasicSubmission.query(rsl_number=self.rsl)
try:
# For some reason .append results in new comment being ignores, so have to concatenate lists.
sub.comment = sub.comment + full_comment
except (AttributeError, TypeError) as e:
logger.error(f"Hit error {e} creating comment")
sub.comment = full_comment
# logger.debug(sub.comment)
sub.save(original=False)
# logger.debug(f"Save result: {result}")

View File

@@ -323,21 +323,21 @@ class SubmissionFormContainer(QWidget):
# reset form # reset form
self.form.setParent(None) self.form.setParent(None)
# logger.debug(f"All attributes of obj: {pformat(self.__dict__)}") # logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
wkb = self.pyd.autofill_excel() # wkb = self.pyd.autofill_excel()
if wkb != None: # if wkb != None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx") # fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx")
try: # try:
wkb.save(filename=fname.__str__()) # wkb.save(filename=fname.__str__())
except PermissionError: # except PermissionError:
logger.error("Hit a permission error when saving workbook. Cancelled?") # logger.error("Hit a permission error when saving workbook. Cancelled?")
if hasattr(self.pyd, 'csv'): # if hasattr(self.pyd, 'csv'):
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?") # dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
if dlg.exec(): # if dlg.exec():
fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv") # fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv")
try: # try:
self.pyd.csv.to_csv(fname.__str__(), index=False) # self.pyd.csv.to_csv(fname.__str__(), index=False)
except PermissionError: # except PermissionError:
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.") # logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
self.report.add_result(report) self.report.add_result(report)
def export_csv_function(self, fname:Path|None=None): def export_csv_function(self, fname:Path|None=None):

View File

@@ -48,7 +48,7 @@
{% if sub['equipment'] %} {% if sub['equipment'] %}
<h3><u>Equipment:</u></h3> <h3><u>Equipment:</u></h3>
<p>{% for item in sub['equipment'] %} <p>{% for item in sub['equipment'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }}({{ item['asset_number'] }}): {{ item['process']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}<br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}<br>
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}
{% if sub['samples'] %} {% if sub['samples'] %}
@@ -97,9 +97,9 @@
{% endfor %}</p> {% endfor %}</p>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if sub['comments'] %} {% if sub['comment'] %}
<h3><u>Comments:</u></h3> <h3><u>Comments:</u></h3>
<p>{% for entry in sub['comments'] %} <p>{% for entry in sub['comment'] %}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}

View File

@@ -391,6 +391,21 @@ def check_authorization(func):
return dict(code=1, message="This user does not have permission for this function.", status="warning") return dict(code=1, message="This user does not have permission for this function.", status="warning")
return wrapper return wrapper
# def check_authorization(user:str):
# def decorator(function):
# def wrapper(*args, **kwargs):
# # funny_stuff()
# # print(argument)
# power_users =
# if user in ctx.power_users:
# result = function(*args, **kwargs)
# else:
# logger.error(f"User {getpass.getuser()} is not authorized for this function.")
# result = dict(code=1, message="This user does not have permission for this function.", status="warning")
# return result
# return wrapper
# return decorator
def check_if_app() -> bool: def check_if_app() -> bool:
""" """
Checks if the program is running from pyinstaller compiled Checks if the program is running from pyinstaller compiled
@@ -461,6 +476,12 @@ class Result(BaseModel):
msg: str msg: str
status: Literal["NoIcon", "Question", "Information", "Warning", "Critical"] = Field(default="NoIcon") status: Literal["NoIcon", "Question", "Information", "Warning", "Critical"] = Field(default="NoIcon")
@field_validator('status', mode='before')
@classmethod
def to_title(cls, value:str):
return value.title()
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Result({self.owner})" return f"Result({self.owner})"