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
__project__ = "submissions"
__version__ = "202401.1b"
__version__ = "202401.2b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2024, Government of Canada"

View File

@@ -18,9 +18,13 @@ class BaseClass(Base):
Base (DeclarativeMeta): Declarative base for metadata.
"""
__abstract__ = True
__table_args__ = {'extend_existing': True}
@declared_attr
def __tablename__(cls):
return f"_{cls.__name__.lower()}"
@declared_attr
def __database_session__(cls):
if not 'pytest' in sys.modules:
@@ -44,6 +48,15 @@ class BaseClass(Base):
else:
from test_settings import ctx
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 *
# import order must go: orgs, kit, subs due to circular import issues

View File

@@ -18,7 +18,7 @@ class ControlType(BaseClass):
"""
Base class of a control archetype.
"""
__tablename__ = '_control_types'
# __tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -75,7 +75,7 @@ class Control(BaseClass):
Base class of a control sample.
"""
__tablename__ = '_control_samples'
# __tablename__ = '_control_samples'
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
@@ -264,7 +264,4 @@ class Control(BaseClass):
case _:
pass
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 pathlib import Path
from . import Base, BaseClass, Organization
from tools import Settings
logger = logging.getLogger(f'submissions.{__name__}')
@@ -32,11 +31,19 @@ equipmentroles_equipment = Table(
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",
Base.metadata,
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
)
@@ -48,16 +55,24 @@ submissiontypes_processes = Table(
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):
"""
Base of kits used in submission processing
"""
__tablename__ = "_kits"
# __table_args__ = {'extend_existing': True}
# __tablename__ = "_kits"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit
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(
"KitTypeReagentTypeAssociation",
@@ -87,7 +102,7 @@ class KitType(BaseClass):
Args:
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:
list: List of reagent types
@@ -109,12 +124,13 @@ class KitType(BaseClass):
Creates map of locations in excel workbook for a SubmissionType
Args:
use (str): Submissiontype.name
use (str | SubmissionType): Submissiontype.name
Returns:
dict: Dictionary containing information locations.
"""
map = {}
# Account for submission_type variable type.
match submission_type:
case str():
assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name==submission_type]
@@ -125,7 +141,6 @@ class KitType(BaseClass):
case _:
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
# Get all KitTypeReagentTypeAssociation for SubmissionType
# assocs = [item for item in self.kit_reagenttype_associations if item.submission_type==submission_type]
for assoc in assocs:
try:
map[assoc.reagent_type.name] = assoc.uses
@@ -133,7 +148,6 @@ class KitType(BaseClass):
continue
# Get SubmissionType info map
try:
# st_assoc = [item for item in self.used_for if use == item.name][0]
map['info'] = st_assoc.info_map
except IndexError as e:
map['info'] = {}
@@ -152,7 +166,7 @@ class KitType(BaseClass):
Args:
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.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -190,17 +204,13 @@ class KitType(BaseClass):
@check_authorization
def save(self, ctx:Settings):
"""
Add this instance to database and commit
"""
self.__database_session__.add(self)
self.__database_session__.commit()
super().save()
class ReagentType(BaseClass):
"""
Base of reagent type abstract
"""
__tablename__ = "_reagent_types"
# __tablename__ = "_reagent_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type
@@ -281,130 +291,16 @@ class ReagentType(BaseClass):
def to_pydantic(self):
from backend.validators.pydant import PydReagent
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
# 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
# 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
@check_authorization
def save(self, ctx:Settings):
super().save()
class Reagent(BaseClass):
"""
Concrete reagent instance
"""
__tablename__ = "_reagents"
# __tablename__ = "_reagents"
id = Column(INTEGER, primary_key=True) #: primary key
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
lot = Column(String(64)) #: lot number of reagent
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(
"SubmissionReagentAssociation",
@@ -497,6 +392,7 @@ class Reagent(BaseClass):
def query(cls,
reagent_type:str|ReagentType|None=None,
lot_number:str|None=None,
name:str|None=None,
limit:int=0
) -> Reagent|List[Reagent]:
"""
@@ -505,6 +401,7 @@ class Reagent(BaseClass):
Args:
reagent_type (str | models.ReagentType | None, optional): Reagent type. 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.
Returns:
@@ -521,6 +418,12 @@ class Reagent(BaseClass):
query = query.filter(cls.type.contains(reagent_type))
case _:
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:
case str():
logger.debug(f"Looking up reagent by lot number: {lot_number}")
@@ -530,19 +433,12 @@ class Reagent(BaseClass):
case _:
pass
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):
"""
Relationship table for client labs for certain kits.
"""
__tablename__ = "_discounts"
# __tablename__ = "_discounts"
id = Column(INTEGER, primary_key=True) #: primary key
kit = relationship("KitType") #: joined parent reagent type
@@ -604,17 +500,20 @@ class Discount(BaseClass):
pass
return query.all()
@check_authorization
def save(self, ctx:Settings):
super().save()
class SubmissionType(BaseClass):
"""
Abstract of types of submissions.
"""
__tablename__ = "_submission_types"
# __tablename__ = "_submission_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(128), unique=True) #: name of submission type
info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type.
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.
processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes)
@@ -664,20 +563,25 @@ class SubmissionType(BaseClass):
output = []
for item in self.submissiontype_equipmentrole_associations:
map = item.uses
map['role'] = item.equipment_role.name
if map == None:
map = {}
try:
map['role'] = item.equipment_role.name
except TypeError:
pass
output.append(map)
return output
# return [item.uses for item in self.submissiontype_equipmentrole_associations]
def get_equipment(self) -> List['PydEquipmentRole']:
return [item.to_pydantic(submission_type=self) for item in self.equipment]
def get_equipment(self, extraction_kit:str|KitType|None=None) -> List['PydEquipmentRole']:
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:
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():
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 _:
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 ]))
@@ -728,7 +632,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
"""
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
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
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
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
@@ -902,22 +806,9 @@ class KitTypeReagentTypeAssociation(BaseClass):
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 SubmissionReagentAssociation(BaseClass):
__tablename__ = "_reagents_submissions"
# __tablename__ = "_reagents_submissions"
reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample
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}")
reagent = Reagent.query(lot_number=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 _:
pass
# logger.debug(f"Result of query after reagent: {query.all()}")
@@ -976,7 +865,6 @@ class SubmissionReagentAssociation(BaseClass):
case _:
pass
# logger.debug(f"Result of query after submission: {query.all()}")
# limit = query.count()
return query_return(query=query, limit=limit)
def to_sub_dict(self, extraction_kit):
@@ -989,13 +877,14 @@ class Equipment(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
__tablename__ = "_equipment"
# __tablename__ = "_equipment"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
nickname = Column(String(64))
asset_number = Column(String(16))
roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment)
processes = relationship("Process", back_populates="equipment", secondary=equipment_processes)
equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation",
@@ -1008,10 +897,30 @@ class Equipment(BaseClass):
def __repr__(self):
return f"<Equipment({self.name})>"
def get_processes(self, submission_type:SubmissionType):
processes = [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
def to_dict(self, processes:bool=False):
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:
processes = ['']
# logger.debug(f"Processes: {processes}")
return processes
@classmethod
@@ -1043,10 +952,10 @@ class Equipment(BaseClass):
pass
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
# return PydEquipment(process=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
return PydEquipment(process=None, 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__)
def save(self):
self.__database_session__.add(self)
@@ -1064,12 +973,12 @@ class Equipment(BaseClass):
class EquipmentRole(BaseClass):
__tablename__ = "_equipment_roles"
# __tablename__ = "_equipment_roles"
id = Column(INTEGER, primary_key=True)
name = Column(String(32))
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(
"SubmissionTypeEquipmentRoleAssociation",
@@ -1081,12 +990,24 @@ class EquipmentRole(BaseClass):
def __repr__(self):
return f"<EquipmentRole({self.name})>"
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):
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipmentRole
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
pyd_dict = self.__dict__
pyd_dict['processes'] = self.get_processes(submission_type=submission_type)
equipment = [item.to_pydantic(submission_type=submission_type, extraction_kit=extraction_kit) for item in self.instances]
# processes = [item.name for item in self.processes]
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)
@classmethod
@@ -1107,31 +1028,36 @@ class EquipmentRole(BaseClass):
pass
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):
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:
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:
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:
return ['']
else:
return output
def save(self):
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except:
self.__database_session__.rollback()
class SubmissionEquipmentAssociation(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
__tablename__ = "_equipment_submissions"
# __tablename__ = "_equipment_submissions"
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
@@ -1144,14 +1070,14 @@ class SubmissionEquipmentAssociation(BaseClass):
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):
self.submission = submission
self.equipment = equipment
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
def save(self):
@@ -1162,7 +1088,7 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
# __abstract__ = True
__tablename__ = "_submissiontype_equipmentrole"
# __tablename__ = "_submissiontype_equipmentrole"
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
@@ -1192,25 +1118,38 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
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]
# flatten list
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
@check_authorization
def save(self, ctx:Settings):
self.__database_session__.add(self)
self.__database_session__.commit()
# self.__database_session__.add(self)
# self.__database_session__.commit()
super().save()
class Process(BaseClass):
__tablename__ = "_process"
"""
A Process is a method used by a piece of equipment.
"""
# __tablename__ = "_process"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
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)
submissions = relationship("SubmissionEquipmentAssociation", backref='process')
kit_types = relationship("KitType", back_populates='processes', secondary=kittypes_processes)
def __repr__(self):
return f"<Process({self.name})"
@@ -1227,124 +1166,3 @@ class Process(BaseClass):
pass
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
"""
__tablename__ = "_organizations"
# __tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name
@@ -73,14 +73,13 @@ class Organization(BaseClass):
Args:
ctx (Settings): Settings object passed down from GUI. Necessary to check authorization
"""
ctx.database_session.add(self)
ctx.database_session.commit()
super().save()
class Contact(BaseClass):
"""
Base of Contact
"""
__tablename__ = "_contacts"
# __tablename__ = "_contacts"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name

View File

@@ -13,23 +13,24 @@ from json.decoder import JSONDecodeError
from sqlalchemy.ext.associationproxy import association_proxy
import pandas as pd
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 datetime import datetime, date, time
from typing import List
from typing import List, Any
from dateutil.parser import parse
from dateutil.parser._parser import ParserError
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
class BasicSubmission(BaseClass):
"""
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
"""
__tablename__ = "_submissions"
# __tablename__ = "_submissions"
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)
@@ -96,7 +97,7 @@ class BasicSubmission(BaseClass):
"""
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
@@ -137,7 +138,7 @@ class BasicSubmission(BaseClass):
logger.error(f"We got an error retrieving reagents: {e}")
reagents = None
# 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:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if len(equipment) == 0:
@@ -255,18 +256,7 @@ class BasicSubmission(BaseClass):
Returns:
list: list of htipick dictionaries for each sample
"""
output_list = []
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
output_list = [assoc.to_hitpick() for assoc in self.submission_sample_associations]
return output_list
@classmethod
@@ -548,7 +538,7 @@ class BasicSubmission(BaseClass):
result = assoc.save()
return result
def to_pydantic(self):
def to_pydantic(self, backup:bool=False):
"""
Converts this instance into a PydSubmission
@@ -556,7 +546,7 @@ class BasicSubmission(BaseClass):
PydSubmission: converted object.
"""
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)}")
# dicto['filepath'] = Path(tempfile.TemporaryFile().name)
new_dict = {}
@@ -567,7 +557,11 @@ class BasicSubmission(BaseClass):
case "samples":
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
case "equipment":
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
# logger.debug(f"\n\nEquipment: {dicto['equipment']}\n\n")
try:
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":
new_dict['rsl_plate_num'] = dict(value=value, missing=True)
case "Submitter Plate Number":
@@ -582,25 +576,6 @@ class BasicSubmission(BaseClass):
# sys.exit()
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):
"""
Adds this instance to database and commits.
@@ -610,24 +585,7 @@ class BasicSubmission(BaseClass):
"""
if original:
self.uploaded_by = getuser()
self.__database_session__.add(self)
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
super().save()
@classmethod
@setup_lookup
@@ -782,15 +740,121 @@ class BasicSubmission(BaseClass):
def get_used_equipment(self) -> List[str]:
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
class BacterialCulture(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
__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:
"""
@@ -804,6 +868,10 @@ class BacterialCulture(BasicSubmission):
output['controls'] = [item.to_sub_dict() for item in self.controls]
return output
@classmethod
def get_abbreviation(cls):
return "BC"
@classmethod
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
"""
@@ -853,7 +921,7 @@ class BacterialCulture(BasicSubmission):
Extends parent
"""
from backend.validators import RSLNamer
data['abbreviation'] = "BC"
data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None) -> str:
# """
@@ -932,10 +1000,12 @@ class Wastewater(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))
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:
"""
@@ -952,6 +1022,10 @@ class Wastewater(BasicSubmission):
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
return output
@classmethod
def get_abbreviation(cls):
return "WW"
@classmethod
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
"""
@@ -1012,7 +1086,7 @@ class Wastewater(BasicSubmission):
Extends parent
"""
from backend.validators import RSLNamer
data['abbreviation'] = "WW"
data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None):
# 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?)?)"
@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):
"""
derivative submission type for artic wastewater
"""
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
__mapper_args__ = {"polymorphic_identity": "Wastewater Artic", "polymorphic_load": "inline"}
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
artic_technician = Column(String(64))
dna_core_submission_number = Column(String(64))
def calculate_base_cost(self):
"""
@@ -1093,6 +1175,10 @@ class WastewaterArtic(BasicSubmission):
except Exception as e:
logger.error(f"Calculation error: {e}")
@classmethod
def get_abbreviation(cls):
return "AR"
@classmethod
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})"
# 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()
if "ENC" in input_dict['submitter_id']:
input_dict['submitter_id'] = cls.en_adapter(input_str=input_dict['submitter_id'])
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
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
"""
Extends parent
"""
from backend.validators import RSLNamer
data['abbreviation'] = "AR"
data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None):
# today = datetime.now()
@@ -1240,6 +1356,36 @@ class WastewaterArtic(BasicSubmission):
worksheet.cell(row=iii, column=jjj, value=value)
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
class BasicSample(BaseClass):
@@ -1247,7 +1393,7 @@ class BasicSample(BaseClass):
Base of basic sample which polymorphs into BCSample and WWSample
"""
__tablename__ = "_samples"
# __tablename__ = "_samples"
id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
@@ -1295,6 +1441,18 @@ class BasicSample(BaseClass):
def __repr__(self) -> str:
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):
"""
Custom attribute setter
@@ -1307,59 +1465,6 @@ class BasicSample(BaseClass):
setattr(self, name, value)
except AttributeError:
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
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}")
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):
raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1521,7 +1621,7 @@ class WastewaterSample(BasicSample):
"""
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_full_sample_id = Column(String(64)) #: full id given by entrics
rsl_number = Column(String(64)) #: rsl plate identification number
@@ -1529,46 +1629,21 @@ class WastewaterSample(BasicSample):
received_date = Column(TIMESTAMP) #: Date sample received
notes = Column(String(2000)) #: notes from submission form
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.
Args:
submission_rsl (str): rsl_plate_num of the submission
gui friendly dictionary, extends parent method.
Returns:
dict|None: dict: dictionary of sample id, row and column in elution plate
"""
sample = super().to_hitpick(submission_rsl=submission_rsl)
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
# 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.")
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""
sample = super().to_sub_dict(submission_rsl=submission_rsl)
sample['ww_processing_num'] = self.ww_processing_num
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
def parse_sample(cls, input_dict: dict) -> dict:
output_dict = super().parse_sample(input_dict)
@@ -1591,35 +1666,28 @@ class WastewaterSample(BasicSample):
case _:
del output_dict['collection_date']
return output_dict
def to_sub_dict(self, submission_rsl: str | BasicSubmission) -> dict:
sample = super().to_sub_dict(submission_rsl)
if self.ww_processing_num != None:
sample['ww_processing_num'] = self.ww_processing_num
else:
sample['ww_processing_num'] = self.submitter_id
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
subs = self.submissions[:self.submissions.index(current_artic_submission)]
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
logger.debug(f"Submissions up to current artic submission: {subs}")
try:
assoc = [item for item in self.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
except:
assoc = 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
return subs[-1]
except IndexError:
return None
class BacterialCultureSample(BasicSample):
"""
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
concentration = Column(String(16)) #: sample concentration
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:
"""
@@ -1632,15 +1700,18 @@ class BacterialCultureSample(BasicSample):
sample['name'] = self.submitter_id
sample['organism'] = self.organism
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:
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
# 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
class SubmissionSampleAssociation(BaseClass):
@@ -1649,7 +1720,7 @@ class SubmissionSampleAssociation(BaseClass):
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
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
@@ -1688,7 +1759,12 @@ class SubmissionSampleAssociation(BaseClass):
Returns:
dict: Updated dictionary with row, column and well updated
"""
# Get sample info
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['column'] = self.column
try:
@@ -1696,6 +1772,33 @@ class SubmissionSampleAssociation(BaseClass):
except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.")
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
@classmethod
@@ -1831,14 +1934,6 @@ class SubmissionSampleAssociation(BaseClass):
instance = used_cls(submission=submission, sample=sample, **kwargs)
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):
raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1846,10 +1941,32 @@ class WastewaterAssociation(SubmissionSampleAssociation):
"""
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_n2 = Column(FLOAT(2)) #: AKA ct for N2
n1_status = Column(String(32)) #: positive or negative for N1
n2_status = Column(String(32)) #: positive or negative for N2
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:
regex = Equipment.get_regex()
return regex.search(input).group().strip("-")
logger.debug(f"Using equipment regex: {regex} on {input}")
try:
return regex.search(input).group().strip("-")
except AttributeError:
return input
def parse_equipment(self):
logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
@@ -512,7 +517,10 @@ class EquipmentParser(object):
# logger.debug(f"Sheets: {sheets}")
for sheet in self.xl.sheet_names:
df = self.xl.parse(sheet, header=None, dtype=object)
relevant = [item for item in self.map if item['sheet']==sheet]
try:
relevant = [item for item in self.map if item['sheet']==sheet]
except (TypeError, KeyError):
continue
# logger.debug(f"Relevant equipment: {pformat(relevant)}")
previous_asset = ""
for equipment in relevant:
@@ -524,7 +532,10 @@ class EquipmentParser(object):
asset = self.get_asset_number(input=asset)
eq = Equipment.query(asset_number=asset)
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)}")
return output

View File

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

View File

@@ -106,7 +106,7 @@ class PydReagent(BaseModel):
if self.model_extra != None:
self.__dict__.update(self.model_extra)
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}")
if reagent == None:
reagent = Reagent()
@@ -209,6 +209,56 @@ class PydSample(BaseModel, extra='allow'):
instance.metadata.session.rollback()
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'):
filepath: Path
submission_type: dict|None
@@ -453,8 +503,10 @@ class PydSubmission(BaseModel, extra='allow'):
for equip in self.equipment:
equip, association = equip.toSQL(submission=instance)
if association != None:
association.save()
logger.debug(f"Equipment association SQL object to be added to submission: {association.__dict__}")
instance.submission_equipment_associations.append(association)
case _:
try:
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 info: {new_info}")
# get list of sheet names
sheets = workbook.sheetnames
# logger.debug(workbook.sheetnames)
for sheet in sheets:
for sheet in workbook.sheetnames:
# open sheet
worksheet=workbook[sheet]
# Get relevant reagents for that sheet
@@ -613,11 +663,14 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Workbook sheets: {workbook.sheetnames}")
worksheet = workbook[sample_info["lookup_table"]['sheet']]
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)}")
# Fail safe against multiple instances of the same sample
for iii, sample in enumerate(samples, start=1):
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()]
for field in fields:
column = sample_info['sample_columns'][field]
value = getattr(sample, field)
@@ -630,6 +683,42 @@ class PydSubmission(BaseModel, extra='allow'):
value = row_map[value]
worksheet.cell(row=row, column=column, value=value)
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:
"""
@@ -774,42 +863,6 @@ class PydKit(BaseModel):
[item.toSQL(instance) for item in self.reagent_types]
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):
name: str
@@ -819,4 +872,4 @@ class PydEquipmentRole(BaseModel):
def toForm(self, parent, submission_type, used):
from frontend.widgets.equipment_usage import RoleComboBox
return RoleComboBox(parent=parent, role=self, submission_type=submission_type, used=used)

View File

@@ -10,17 +10,14 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, submission_type:SubmissionType|str, submission:BasicSubmission) -> QDialog:
def __init__(self, parent, submission:BasicSubmission) -> QDialog:
super().__init__(parent)
self.submission = submission
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}")
if isinstance(submission_type, str):
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()
self.opt_equipment = submission.submission_type.get_equipment()
logger.debug(f"EquipmentRoles: {self.opt_equipment}")
self.layout = QVBoxLayout()
self.setLayout(self.layout)
@@ -31,20 +28,44 @@ class EquipmentUsage(QDialog):
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
label = self.LabelRow(parent=self)
self.layout.addWidget(label)
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)
def parse_form(self):
output = []
for widget in self.findChildren(QWidget):
match widget:
case (EquipmentCheckBox()|RoleComboBox()) :
output.append(widget.parse_form())
case RoleComboBox() :
if widget.check.isChecked():
output.append(widget.parse_form())
case _:
pass
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):
def __init__(self, parent, equipment:PydEquipment) -> None:
@@ -56,7 +77,6 @@ class EquipmentCheckBox(QWidget):
self.check = QCheckBox()
if equipment.static:
self.check.setChecked(True)
# self.check.setEnabled(False)
if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})"
else:
@@ -87,28 +107,42 @@ class RoleComboBox(QWidget):
else:
self.check.setChecked(True)
self.box = QComboBox()
self.box.setMaximumWidth(125)
self.box.setMinimumWidth(125)
self.box.setMaximumWidth(200)
self.box.setMinimumWidth(200)
self.box.addItems([item.name for item in role.equipment])
self.box.currentTextChanged.connect(self.update_processes)
# self.check = QCheckBox()
# self.layout.addWidget(label)
self.process = QComboBox()
self.process.setMaximumWidth(125)
self.process.setMinimumWidth(125)
self.process.setMaximumWidth(200)
self.process.setMinimumWidth(200)
self.process.setEditable(True)
# 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(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.process)
# self.layout.addWidget(self.check)
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:
eq = Equipment.query(name=self.box.currentText())
if self.check:
return PydEquipment(name=eq.name, process=self.process.currentText(), role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
else:
return None
# if self.check.isChecked():
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
# else:
# 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.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
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 tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from xhtml2pdf import pisa
@@ -95,8 +95,8 @@ class SubmissionsSheet(QTableView):
self.resizeColumnsToContents()
self.resizeRowsToContents()
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:
"""
@@ -114,16 +114,16 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
def show_details(self) -> None:
"""
creates detailed data to show in seperate window
"""
logger.debug(f"Sheet.app: {self.app}")
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
dlg = SubmissionDetails(parent=self, id=value)
if dlg.exec():
pass
# def show_details(self, submission:BasicSubmission) -> None:
# """
# creates detailed data to show in seperate window
# """
# logger.debug(f"Sheet.app: {self.app}")
# # index = (self.selectionModel().currentIndex())
# # value = index.sibling(index.row(),0).data()
# dlg = SubmissionDetails(parent=self, sub=submission)
# if dlg.exec():
# pass
def create_barcode(self) -> None:
"""
@@ -154,38 +154,47 @@ class SubmissionsSheet(QTableView):
Args:
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)
renameAction = QAction('Delete', self)
detailsAction = QAction('Details', self)
# barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", self)
backupAction = QAction("Backup", self)
equipAction = QAction("Add Equipment", self)
# hitpickAction = QAction("Hitpicks", self)
renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details())
# barcodeAction.triggered.connect(lambda: self.create_barcode())
commentAction.triggered.connect(lambda: self.add_comment())
backupAction.triggered.connect(lambda: self.regenerate_submission_form())
equipAction.triggered.connect(lambda: self.add_equipment())
# hitpickAction.triggered.connect(lambda: self.hit_pick())
self.menu.addAction(detailsAction)
self.menu.addAction(renameAction)
# self.menu.addAction(barcodeAction)
self.menu.addAction(commentAction)
self.menu.addAction(backupAction)
self.menu.addAction(equipAction)
# self.menu.addAction(hitpickAction)
# renameAction = QAction('Delete', self)
# detailsAction = QAction('Details', self)
# commentAction = QAction("Add Comment", self)
# equipAction = QAction("Add Equipment", self)
# backupAction = QAction("Export", self)
# renameAction.triggered.connect(lambda: self.delete_item(submission))
# detailsAction.triggered.connect(lambda: self.show_details(submission))
# commentAction.triggered.connect(lambda: self.add_comment(submission))
# backupAction.triggered.connect(lambda: self.regenerate_submission_form(submission))
# equipAction.triggered.connect(lambda: self.add_equipment(submission))
# self.menu.addAction(detailsAction)
# self.menu.addAction(renameAction)
# self.menu.addAction(commentAction)
# self.menu.addAction(backupAction)
# self.menu.addAction(equipAction)
self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys():
logger.debug(f"Adding {k}")
action = QAction(k, self)
action.triggered.connect(lambda _, action_name=k: self.triggered_action(action_name=action_name))
self.menu.addAction(action)
# add other required actions
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):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value)
def add_equipment_function(self, rsl_plate_id):
submission = BasicSubmission.query(id=rsl_plate_id)
def add_equipment_function(self, submission:BasicSubmission):
# submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission)
if dlg.exec():
@@ -193,29 +202,33 @@ class SubmissionsSheet(QTableView):
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.process)
assoc.process = process
assoc.role = equip.role
# assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# process = Process.query(name=equip.processes)
# assoc.process = process
# assoc.role = equip.role
_, assoc = equip.toSQL(submission=submission)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.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.
Args:
event (_type_): the item of interest
"""
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
logger.debug(index)
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
# index = (self.selectionModel().currentIndex())
# value = index.sibling(index.row(),0).data()
# 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 {submission.rsl_plate_num}?\n")
if msg.exec():
# delete_submission(id=value)
BasicSubmission.query(id=value).delete()
submission.delete()
else:
return
self.setData()
@@ -424,221 +437,11 @@ class SubmissionsSheet(QTableView):
writer.close()
self.report.add_result(report)
def regenerate_submission_form(self):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
logger.debug(index)
# msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
# if msg.exec():
# delete_submission(id=value)
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)
def regenerate_submission_form(self, submission:BasicSubmission):
# index = (self.selectionModel().currentIndex())
# value = index.sibling(index.row(),0).data()
# logger.debug(index)
# sub = BasicSubmission.query(id=value)
fname = select_save_file(self, default_name=submission.to_pydantic().construct_filename(), extension="xlsx")
submission.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
self.form.setParent(None)
# logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
wkb = self.pyd.autofill_excel()
if wkb != None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx")
try:
wkb.save(filename=fname.__str__())
except PermissionError:
logger.error("Hit a permission error when saving workbook. Cancelled?")
if hasattr(self.pyd, 'csv'):
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
if dlg.exec():
fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv")
try:
self.pyd.csv.to_csv(fname.__str__(), index=False)
except PermissionError:
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
# wkb = self.pyd.autofill_excel()
# if wkb != None:
# fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx")
# try:
# wkb.save(filename=fname.__str__())
# except PermissionError:
# logger.error("Hit a permission error when saving workbook. Cancelled?")
# if hasattr(self.pyd, 'csv'):
# dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
# if dlg.exec():
# fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv")
# try:
# self.pyd.csv.to_csv(fname.__str__(), index=False)
# except PermissionError:
# logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
self.report.add_result(report)
def export_csv_function(self, fname:Path|None=None):

View File

@@ -48,7 +48,7 @@
{% if sub['equipment'] %}
<h3><u>Equipment:</u></h3>
<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>
{% endif %}
{% if sub['samples'] %}
@@ -97,9 +97,9 @@
{% endfor %}</p>
{% endfor %}
{% endif %}
{% if sub['comments'] %}
{% if sub['comment'] %}
<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>
{% endfor %}</p>
{% 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 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:
"""
Checks if the program is running from pyinstaller compiled
@@ -461,6 +476,12 @@ class Result(BaseModel):
msg: str
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:
return f"Result({self.owner})"