Midway through disaster of changing table names.
This commit is contained in:
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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 ###
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ class BaseClass(Base):
|
||||
|
||||
__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:
|
||||
@@ -45,6 +49,15 @@ class BaseClass(Base):
|
||||
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
|
||||
from .organizations import *
|
||||
|
||||
@@ -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
|
||||
@@ -265,6 +265,3 @@ class Control(BaseClass):
|
||||
pass
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def save(self):
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
|
||||
@@ -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
|
||||
@@ -282,129 +292,15 @@ class ReagentType(BaseClass):
|
||||
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}")
|
||||
@@ -531,18 +434,11 @@ class Reagent(BaseClass):
|
||||
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
|
||||
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",
|
||||
@@ -1082,11 +991,23 @@ class EquipmentRole(BaseClass):
|
||||
def __repr__(self):
|
||||
return f"<EquipmentRole({self.name})>"
|
||||
|
||||
def to_pydantic(self, submission_type:SubmissionType):
|
||||
def to_dict(self):
|
||||
output = {}
|
||||
for key, value in self.__dict__.items():
|
||||
match key:
|
||||
case "processes":
|
||||
pass
|
||||
case _:
|
||||
value = value
|
||||
output[key] = value
|
||||
return output
|
||||
|
||||
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
|
||||
from backend.validators.pydant import PydEquipmentRole
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
# 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
|
||||
@@ -1308,59 +1466,6 @@ class BasicSample(BaseClass):
|
||||
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
|
||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||
"""
|
||||
sample = super().to_hitpick(submission_rsl=submission_rsl)
|
||||
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.")
|
||||
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)
|
||||
@@ -1592,34 +1667,27 @@ class WastewaterSample(BasicSample):
|
||||
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
|
||||
|
||||
@@ -503,7 +503,12 @@ class EquipmentParser(object):
|
||||
|
||||
def get_asset_number(self, input:str) -> str:
|
||||
regex = Equipment.get_regex()
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
@@ -113,12 +113,15 @@ class RSLNamer(object):
|
||||
@classmethod
|
||||
def construct_new_plate_name(cls, data:dict) -> str:
|
||||
if "submitted_date" in data.keys():
|
||||
if isinstance(data['submitted_date'], dict):
|
||||
if data['submitted_date']['value'] != None:
|
||||
today = data['submitted_date']['value']
|
||||
else:
|
||||
today = datetime.now()
|
||||
else:
|
||||
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr)
|
||||
today = data['submitted_date']
|
||||
else:
|
||||
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", data['rsl_plate_num'])
|
||||
try:
|
||||
today = parse(today.group())
|
||||
except AttributeError:
|
||||
|
||||
@@ -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)
|
||||
@@ -631,6 +684,42 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
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:
|
||||
"""
|
||||
Creates filename for this instance
|
||||
@@ -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
|
||||
|
||||
@@ -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()) :
|
||||
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
|
||||
|
||||
93
src/submissions/frontend/widgets/gel_checker.py
Normal file
93
src/submissions/frontend/widgets/gel_checker.py
Normal 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
|
||||
198
src/submissions/frontend/widgets/submission_details.py
Normal file
198
src/submissions/frontend/widgets/submission_details.py
Normal 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
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}")
|
||||
|
||||
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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
{% if sub['equipment'] %}
|
||||
<h3><u>Equipment:</u></h3>
|
||||
<p>{% for item in sub['equipment'] %}
|
||||
<b>{{ item['role'] }}:</b> {{ item['name'] }}({{ item['asset_number'] }}): {{ item['process']|replace('\n\t', '<br> ') }}<br>
|
||||
<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br> ') }}<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'] %}
|
||||
<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -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})"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user