diff --git a/alembic/versions/10c47a04559d_adding_in_processes.py b/alembic/versions/10c47a04559d_adding_in_processes.py
deleted file mode 100644
index b499050..0000000
--- a/alembic/versions/10c47a04559d_adding_in_processes.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/238c3c3e5863_submissionreagentassociations_added.py b/alembic/versions/238c3c3e5863_submissionreagentassociations_added.py
deleted file mode 100644
index f21b9f0..0000000
--- a/alembic/versions/238c3c3e5863_submissionreagentassociations_added.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/2684f065037c_link_controls_with_bacterial_culture_.py b/alembic/versions/2684f065037c_link_controls_with_bacterial_culture_.py
deleted file mode 100644
index f6eb5a2..0000000
--- a/alembic/versions/2684f065037c_link_controls_with_bacterial_culture_.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/30aab47d6f12_adding_role_tag_to_.py b/alembic/versions/30aab47d6f12_adding_role_tag_to_.py
deleted file mode 100644
index 02f0ae3..0000000
--- a/alembic/versions/30aab47d6f12_adding_role_tag_to_.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/36a47d8837ca_adding_in_equipment.py b/alembic/versions/36a47d8837ca_adding_in_equipment.py
deleted file mode 100644
index 1a6335d..0000000
--- a/alembic/versions/36a47d8837ca_adding_in_equipment.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/3e94fecbbe91_adding_times_to_equipsubassoc.py b/alembic/versions/3e94fecbbe91_adding_times_to_equipsubassoc.py
deleted file mode 100644
index 3f80514..0000000
--- a/alembic/versions/3e94fecbbe91_adding_times_to_equipsubassoc.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/4606a7be32e8_adding_submissiontype_to_.py b/alembic/versions/4606a7be32e8_adding_submissiontype_to_.py
deleted file mode 100644
index 54a1d4d..0000000
--- a/alembic/versions/4606a7be32e8_adding_submissiontype_to_.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/67fa77849024_adjusting_process_submissionequipassoc.py b/alembic/versions/67fa77849024_adjusting_process_submissionequipassoc.py
deleted file mode 100644
index ab8bb47..0000000
--- a/alembic/versions/67fa77849024_adjusting_process_submissionequipassoc.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/761baf9d7842_adding_equipment_clustering.py b/alembic/versions/761baf9d7842_adding_equipment_clustering.py
deleted file mode 100644
index 3b1e6fa..0000000
--- a/alembic/versions/761baf9d7842_adding_equipment_clustering.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/7e7b6eeca468_add_templates_to_submission_types.py b/alembic/versions/7e7b6eeca468_add_templates_to_submission_types.py
deleted file mode 100644
index 88feb4f..0000000
--- a/alembic/versions/7e7b6eeca468_add_templates_to_submission_types.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/94289d4e63e6_updating_primary_key_for_.py b/alembic/versions/94289d4e63e6_updating_primary_key_for_.py
deleted file mode 100644
index 7343ec2..0000000
--- a/alembic/versions/94289d4e63e6_updating_primary_key_for_.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/bc7a74476609_adding_equipment_roles.py b/alembic/versions/bc7a74476609_adding_equipment_roles.py
deleted file mode 100644
index 2a38e66..0000000
--- a/alembic/versions/bc7a74476609_adding_equipment_roles.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/cd11db3794ed_adding_static_option_to_equipstassoc.py b/alembic/versions/cd11db3794ed_adding_static_option_to_equipstassoc.py
deleted file mode 100644
index 6cee413..0000000
--- a/alembic/versions/cd11db3794ed_adding_static_option_to_equipstassoc.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/cd5c225b5d2a_adding_process_to_equipsubassoc.py b/alembic/versions/cd5c225b5d2a_adding_process_to_equipsubassoc.py
deleted file mode 100644
index 262e7a7..0000000
--- a/alembic/versions/cd5c225b5d2a_adding_process_to_equipsubassoc.py
+++ /dev/null
@@ -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 ###
diff --git a/alembic/versions/e08a69a0f381_attaching_process_to_.py b/alembic/versions/e08a69a0f381_attaching_process_to_.py
deleted file mode 100644
index 933818e..0000000
--- a/alembic/versions/e08a69a0f381_attaching_process_to_.py
+++ /dev/null
@@ -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 ###
diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py
index c067690..048459e 100644
--- a/src/submissions/__init__.py
+++ b/src/submissions/__init__.py
@@ -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"
diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py
index 40a9935..6422211 100644
--- a/src/submissions/backend/db/models/__init__.py
+++ b/src/submissions/backend/db/models/__init__.py
@@ -18,9 +18,13 @@ class BaseClass(Base):
Base (DeclarativeMeta): Declarative base for metadata.
"""
__abstract__ = True
-
+
__table_args__ = {'extend_existing': True}
+ @declared_attr
+ def __tablename__(cls):
+ return f"_{cls.__name__.lower()}"
+
@declared_attr
def __database_session__(cls):
if not 'pytest' in sys.modules:
@@ -44,6 +48,15 @@ class BaseClass(Base):
else:
from test_settings import ctx
return ctx.backup_path
+
+ def save(self):
+ logger.debug(f"Saving {self}")
+ try:
+ self.__database_session__.add(self)
+ self.__database_session__.commit()
+ except Exception as e:
+ logger.critical(f"Problem saving object: {e}")
+ self.__database_session__.rollback()
from .controls import *
# import order must go: orgs, kit, subs due to circular import issues
diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py
index 8d396d5..0546380 100644
--- a/src/submissions/backend/db/models/controls.py
+++ b/src/submissions/backend/db/models/controls.py
@@ -18,7 +18,7 @@ class ControlType(BaseClass):
"""
Base class of a control archetype.
"""
- __tablename__ = '_control_types'
+ # __tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -75,7 +75,7 @@ class Control(BaseClass):
Base class of a control sample.
"""
- __tablename__ = '_control_samples'
+ # __tablename__ = '_control_samples'
id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type
@@ -264,7 +264,4 @@ class Control(BaseClass):
case _:
pass
return query_return(query=query, limit=limit)
-
- def save(self):
- self.__database_session__.add(self)
- self.__database_session__.commit()
+
diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py
index 3ff5899..7639931 100644
--- a/src/submissions/backend/db/models/kits.py
+++ b/src/submissions/backend/db/models/kits.py
@@ -12,7 +12,6 @@ from typing import List
from pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, Organization
-from tools import Settings
logger = logging.getLogger(f'submissions.{__name__}')
@@ -32,11 +31,19 @@ equipmentroles_equipment = Table(
extend_existing=True
)
+equipment_processes = Table(
+ "_equipment_processes",
+ Base.metadata,
+ Column("process_id", INTEGER, ForeignKey("_process.id")),
+ Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
+ extend_existing=True
+)
+
equipmentroles_processes = Table(
"_equipmentroles_processes",
Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
- Column("equipmentroles_id", INTEGER, ForeignKey("_equipment_roles.id")),
+ Column("equipmentrole_id", INTEGER, ForeignKey("_equipment_roles.id")),
extend_existing=True
)
@@ -48,16 +55,24 @@ submissiontypes_processes = Table(
extend_existing=True
)
+kittypes_processes = Table(
+ "_kittypes_processes",
+ Base.metadata,
+ Column("process_id", INTEGER, ForeignKey("_process.id")),
+ Column("kit_id", INTEGER, ForeignKey("_kits.id")),
+ extend_existing=True
+)
+
class KitType(BaseClass):
"""
Base of kits used in submission processing
"""
- __tablename__ = "_kits"
- # __table_args__ = {'extend_existing': True}
+ # __tablename__ = "_kits"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
+ processes = relationship("Process", back_populates="kit_types", secondary=kittypes_processes)
kit_reagenttype_associations = relationship(
"KitTypeReagentTypeAssociation",
@@ -87,7 +102,7 @@ class KitType(BaseClass):
Args:
required (bool, optional): If true only return required types. Defaults to False.
- submission_type (str | None, optional): Submission type to narrow results. Defaults to None.
+ submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None.
Returns:
list: List of reagent types
@@ -109,12 +124,13 @@ class KitType(BaseClass):
Creates map of locations in excel workbook for a SubmissionType
Args:
- use (str): Submissiontype.name
+ use (str | SubmissionType): Submissiontype.name
Returns:
dict: Dictionary containing information locations.
"""
map = {}
+ # Account for submission_type variable type.
match submission_type:
case str():
assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name==submission_type]
@@ -125,7 +141,6 @@ class KitType(BaseClass):
case _:
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
# Get all KitTypeReagentTypeAssociation for SubmissionType
- # assocs = [item for item in self.kit_reagenttype_associations if item.submission_type==submission_type]
for assoc in assocs:
try:
map[assoc.reagent_type.name] = assoc.uses
@@ -133,7 +148,6 @@ class KitType(BaseClass):
continue
# Get SubmissionType info map
try:
- # st_assoc = [item for item in self.used_for if use == item.name][0]
map['info'] = st_assoc.info_map
except IndexError as e:
map['info'] = {}
@@ -152,7 +166,7 @@ class KitType(BaseClass):
Args:
name (str, optional): Name of desired kit (returns single instance). Defaults to None.
- used_for (str | models.Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
+ used_for (str | Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
id (int | None, optional): Kit id in the database. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -190,17 +204,13 @@ class KitType(BaseClass):
@check_authorization
def save(self, ctx:Settings):
- """
- Add this instance to database and commit
- """
- self.__database_session__.add(self)
- self.__database_session__.commit()
+ super().save()
class ReagentType(BaseClass):
"""
Base of reagent type abstract
"""
- __tablename__ = "_reagent_types"
+ # __tablename__ = "_reagent_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type
@@ -281,130 +291,16 @@ class ReagentType(BaseClass):
def to_pydantic(self):
from backend.validators.pydant import PydReagent
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
-
-# class KitTypeReagentTypeAssociation(BaseClass):
-# """
-# table containing reagenttype/kittype associations
-# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
-# """
-# __tablename__ = "_reagenttypes_kittypes"
-
-# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
-# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
-# submission_type_id = (Column(INTEGER), ForeignKey("_submission_types.id"), primary_key=True)
-# uses = Column(JSON) #: map to location on excel sheets of different submission types
-# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
-# last_used = Column(String(32)) #: last used lot number of this type of reagent
-
-# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
-
-# # reference to the "ReagentType" object
-# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
-
-# submission_type = relationship(SubmissionType, back_populates="submissiontype_kit_rt_associations")
-
-# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
-# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
-# self.kit_type = kit_type
-# self.reagent_type = reagent_type
-# self.uses = uses
-# self.required = required
-
-# def __repr__(self) -> str:
-# return f" {% for item in sub['equipment'] %}
- {{ item['role'] }}: {{ item['name'] }}({{ item['asset_number'] }}): {{ item['process']|replace('\n\t', '
- # Well: {row_map[assoc.row]}{assoc.column}
- # """
- return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
@classmethod
def find_subclasses(cls, attrs:dict|None=None, sample_type:str|None=None) -> BasicSample:
@@ -1507,11 +1612,6 @@ class BasicSample(BaseClass):
logger.debug(f"Creating instance: {instance}")
return instance
- def save(self):
- # raise AttributeError(f"Save not implemented for {self.__class__}")
- self.__database_session__.add(self)
- self.__database_session__.commit()
-
def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1521,7 +1621,7 @@ class WastewaterSample(BasicSample):
"""
Derivative wastewater sample
"""
- # id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True)
+ id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
ww_processing_num = Column(String(64)) #: wastewater processing number
ww_full_sample_id = Column(String(64)) #: full id given by entrics
rsl_number = Column(String(64)) #: rsl plate identification number
@@ -1529,46 +1629,21 @@ class WastewaterSample(BasicSample):
received_date = Column(TIMESTAMP) #: Date sample received
notes = Column(String(2000)) #: notes from submission form
sample_location = Column(String(8)) #: location on 24 well plate
- __mapper_args__ = {"polymorphic_identity": "Wastewater Sample", "polymorphic_load": "inline"}
+ __mapper_args__ = dict(polymorphic_identity="Wastewater Sample",
+ polymorphic_load="inline",
+ inherit_condition=(id == BasicSample.id))
- def to_hitpick(self, submission_rsl:str) -> dict|None:
+ def to_sub_dict(self, submission_rsl:str) -> dict:
"""
- Outputs a dictionary usable for html plate maps. Extends parent method.
-
- Args:
- submission_rsl (str): rsl_plate_num of the submission
+ gui friendly dictionary, extends parent method.
Returns:
- dict|None: dict: dictionary of sample id, row and column in elution plate
- """
- sample = super().to_hitpick(submission_rsl=submission_rsl)
- assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
- # if either n1 or n2 is positive, include this sample
- try:
- sample['positive'] = any(["positive" in item for item in [assoc.n1_status, assoc.n2_status]])
- except (TypeError, AttributeError) as e:
- logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
- try:
- sample['tooltip'] += f"
- ct N1: {'{:.2f}'.format(assoc.ct_n1)} ({assoc.n1_status})
- ct N2: {'{:.2f}'.format(assoc.ct_n2)} ({assoc.n2_status})"
- except (TypeError, AttributeError) as e:
- logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
+ dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
+ """
+ sample = super().to_sub_dict(submission_rsl=submission_rsl)
+ sample['ww_processing_num'] = self.ww_processing_num
return sample
-
- def get_recent_ww_submission(self) -> Wastewater:
- """
- Gets most recent associated wastewater submission
-
- Returns:
- Wastewater: Most recent wastewater submission
- """
- results = [sub for sub in self.submissions if isinstance(sub, Wastewater)]
- if len(results) > 1:
- results = results.sort(key=lambda sub: sub.submitted_date)
- try:
- return results[0]
- except IndexError:
- return None
-
+
@classmethod
def parse_sample(cls, input_dict: dict) -> dict:
output_dict = super().parse_sample(input_dict)
@@ -1591,35 +1666,28 @@ class WastewaterSample(BasicSample):
case _:
del output_dict['collection_date']
return output_dict
-
- def to_sub_dict(self, submission_rsl: str | BasicSubmission) -> dict:
- sample = super().to_sub_dict(submission_rsl)
- if self.ww_processing_num != None:
- sample['ww_processing_num'] = self.ww_processing_num
- else:
- sample['ww_processing_num'] = self.submitter_id
+
+ def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
+ # assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
+ subs = self.submissions[:self.submissions.index(current_artic_submission)]
+ subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
+ logger.debug(f"Submissions up to current artic submission: {subs}")
try:
- assoc = [item for item in self.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
- except:
- assoc = None
- if assoc != None:
- try:
- sample['ct'] = f"{assoc.ct_n1:.2f}, {assoc.ct_n2:.2f}"
- except TypeError:
- sample['ct'] = "None, None"
- sample['source_plate'] = assoc.submission.rsl_plate_num
- sample['source_well'] = f"{row_map[assoc.row]}{assoc.column}"
- return sample
+ return subs[-1]
+ except IndexError:
+ return None
class BacterialCultureSample(BasicSample):
"""
base of bacterial culture sample
"""
- # id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True)
+ id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
organism = Column(String(64)) #: bacterial specimen
concentration = Column(String(16)) #: sample concentration
control = relationship("Control", back_populates="sample", uselist=False)
- __mapper_args__ = {"polymorphic_identity": "Bacterial Culture Sample", "polymorphic_load": "inline"}
+ __mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample",
+ polymorphic_load="inline",
+ inherit_condition=(id == BasicSample.id))
def to_sub_dict(self, submission_rsl:str) -> dict:
"""
@@ -1632,15 +1700,18 @@ class BacterialCultureSample(BasicSample):
sample['name'] = self.submitter_id
sample['organism'] = self.organism
sample['concentration'] = self.concentration
- return sample
-
- def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
- sample = super().to_hitpick(submission_rsl)
if self.control != None:
sample['colour'] = [0,128,0]
- sample['tooltip'] += f"
- 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"
- 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}
+ # 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"
- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})
- 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
diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py
index d6735ee..04863fe 100644
--- a/src/submissions/backend/excel/parser.py
+++ b/src/submissions/backend/excel/parser.py
@@ -503,7 +503,12 @@ class EquipmentParser(object):
def get_asset_number(self, input:str) -> str:
regex = Equipment.get_regex()
- return regex.search(input).group().strip("-")
+ logger.debug(f"Using equipment regex: {regex} on {input}")
+ try:
+ return regex.search(input).group().strip("-")
+ except AttributeError:
+ return input
+
def parse_equipment(self):
logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
@@ -512,7 +517,10 @@ class EquipmentParser(object):
# logger.debug(f"Sheets: {sheets}")
for sheet in self.xl.sheet_names:
df = self.xl.parse(sheet, header=None, dtype=object)
- relevant = [item for item in self.map if item['sheet']==sheet]
+ try:
+ relevant = [item for item in self.map if item['sheet']==sheet]
+ except (TypeError, KeyError):
+ continue
# logger.debug(f"Relevant equipment: {pformat(relevant)}")
previous_asset = ""
for equipment in relevant:
@@ -524,7 +532,10 @@ class EquipmentParser(object):
asset = self.get_asset_number(input=asset)
eq = Equipment.query(asset_number=asset)
process = df.iat[equipment['process']['row']-1, equipment['process']['column']-1]
- output.append(PydEquipment(name=eq.name, process=process, role=equipment['role'], asset_number=asset, nickname=eq.nickname))
+ try:
+ output.append(PydEquipment(name=eq.name, processes=[process], role=equipment['role'], asset_number=asset, nickname=eq.nickname))
+ except AttributeError:
+ logger.error(f"Unable to add {eq} to PydEquipment list.")
# logger.debug(f"Here is the output so far: {pformat(output)}")
return output
diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py
index b1337cf..e7f47f2 100644
--- a/src/submissions/backend/validators/__init__.py
+++ b/src/submissions/backend/validators/__init__.py
@@ -113,12 +113,15 @@ class RSLNamer(object):
@classmethod
def construct_new_plate_name(cls, data:dict) -> str:
if "submitted_date" in data.keys():
- if data['submitted_date']['value'] != None:
- today = data['submitted_date']['value']
+ if isinstance(data['submitted_date'], dict):
+ if data['submitted_date']['value'] != None:
+ today = data['submitted_date']['value']
+ else:
+ today = datetime.now()
else:
- today = datetime.now()
+ today = data['submitted_date']
else:
- today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr)
+ today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", data['rsl_plate_num'])
try:
today = parse(today.group())
except AttributeError:
diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py
index 8cc240b..8bc9992 100644
--- a/src/submissions/backend/validators/pydant.py
+++ b/src/submissions/backend/validators/pydant.py
@@ -106,7 +106,7 @@ class PydReagent(BaseModel):
if self.model_extra != None:
self.__dict__.update(self.model_extra)
logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
- reagent = Reagent.query(lot_number=self.lot)
+ reagent = Reagent.query(lot_number=self.lot, name=self.name)
logger.debug(f"Result: {reagent}")
if reagent == None:
reagent = Reagent()
@@ -209,6 +209,56 @@ class PydSample(BaseModel, extra='allow'):
instance.metadata.session.rollback()
return instance, out_associations, report
+class PydEquipment(BaseModel, extra='ignore'):
+
+ asset_number: str
+ name: str
+ nickname: str|None
+ processes: List[str]|None
+ role: str|None
+
+ @field_validator('processes', mode='before')
+ @classmethod
+ def make_empty_list(cls, value):
+ # logger.debug(f"Pydantic value: {value}")
+ if value == None:
+ value = ['']
+ if len(value)==0:
+ value=['']
+ return value
+
+ # def toForm(self, parent):
+ # from frontend.widgets.equipment_usage import EquipmentCheckBox
+ # return EquipmentCheckBox(parent=parent, equipment=self)
+
+ def toSQL(self, submission:BasicSubmission|str=None):
+ if isinstance(submission, str):
+ submission = BasicSubmission.query(rsl_number=submission)
+ equipment = Equipment.query(asset_number=self.asset_number)
+ if equipment == None:
+ return
+ if submission != None:
+ assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
+ process = Process.query(name=self.processes[0])
+ if process == None:
+ from frontend.widgets.pop_ups import QuestionAsker
+ dlg = QuestionAsker(title="Add Process?", message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
+ if dlg.exec():
+ kit = submission.extraction_kit
+ submission_type = submission.submission_type
+ process = Process(name=self.processes[0])
+ process.kit_types.append(kit)
+ process.submission_types.append(submission_type)
+ process.equipment.append(equipment)
+ process.save()
+ assoc.process = process
+ assoc.role = self.role
+ # equipment.equipment_submission_associations.append(assoc)
+ equipment.equipment_submission_associations.append(assoc)
+ else:
+ assoc = None
+ return equipment, assoc
+
class PydSubmission(BaseModel, extra='allow'):
filepath: Path
submission_type: dict|None
@@ -453,8 +503,10 @@ class PydSubmission(BaseModel, extra='allow'):
for equip in self.equipment:
equip, association = equip.toSQL(submission=instance)
if association != None:
+ association.save()
logger.debug(f"Equipment association SQL object to be added to submission: {association.__dict__}")
instance.submission_equipment_associations.append(association)
+
case _:
try:
instance.set_attribute(key=key, value=value)
@@ -570,9 +622,7 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"New reagents: {new_reagents}")
logger.debug(f"New info: {new_info}")
# get list of sheet names
- sheets = workbook.sheetnames
- # logger.debug(workbook.sheetnames)
- for sheet in sheets:
+ for sheet in workbook.sheetnames:
# open sheet
worksheet=workbook[sheet]
# Get relevant reagents for that sheet
@@ -613,11 +663,14 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Workbook sheets: {workbook.sheetnames}")
worksheet = workbook[sample_info["lookup_table"]['sheet']]
samples = sorted(self.samples, key=attrgetter('column', 'row'))
+ custom_sampler = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type).adjust_autofill_samples
+ samples = custom_sampler(samples=samples)
logger.debug(f"Samples: {pformat(samples)}")
# Fail safe against multiple instances of the same sample
for iii, sample in enumerate(samples, start=1):
row = sample_info['lookup_table']['start_row'] + iii
fields = [field for field in list(sample.model_fields.keys()) + list(sample.model_extra.keys()) if field in sample_info['sample_columns'].keys()]
+
for field in fields:
column = sample_info['sample_columns'][field]
value = getattr(sample, field)
@@ -630,6 +683,42 @@ class PydSubmission(BaseModel, extra='allow'):
value = row_map[value]
worksheet.cell(row=row, column=column, value=value)
return workbook
+
+ def autofill_equipment(self, workbook:Workbook) -> Workbook:
+ equipment_map = SubmissionType.query(name=self.submission_type['value']).construct_equipment_map()
+ logger.debug(f"Equipment map: {equipment_map}")
+ # See if all equipment has a location map
+ # If not, create a new sheet to store them in.
+ if not all([len(item.keys()) > 1 for item in equipment_map]):
+ logger.warning("Creating 'Equipment' sheet to hold unmapped equipment")
+ workbook.create_sheet("Equipment")
+ equipment = []
+ for ii, equip in enumerate(self.equipment, start=1):
+ loc = [item for item in equipment_map if item['role'] == equip.role][0]
+ try:
+ loc['name']['value'] = equip.name
+ loc['process']['value'] = equip.processes[0]
+ except KeyError:
+ loc['name'] = dict(row=ii, column=2)
+ loc['process'] = dict(row=ii, column=3)
+ loc['name']['value'] = equip.name
+ loc['process']['value'] = equip.processes[0]
+ loc['sheet'] = "Equipment"
+ equipment.append(loc)
+ logger.debug(f"Using equipment: {equipment}")
+ for sheet in workbook.sheetnames:
+ logger.debug(f"Looking at: {sheet}")
+ worksheet = workbook[sheet]
+ relevant = [item for item in equipment if item['sheet'] == sheet]
+ for rel in relevant:
+ match sheet:
+ case "Equipment":
+ worksheet.cell(row=rel['name']['row'], column=1, value=rel['role'])
+ case _:
+ pass
+ worksheet.cell(row=rel['name']['row'], column=rel['name']['column'], value=rel['name']['value'])
+ worksheet.cell(row=rel['process']['row'], column=rel['process']['column'], value=rel['process']['value'])
+ return workbook
def construct_filename(self) -> str:
"""
@@ -774,42 +863,6 @@ class PydKit(BaseModel):
[item.toSQL(instance) for item in self.reagent_types]
return instance, report
-class PydEquipment(BaseModel, extra='ignore'):
-
- asset_number: str
- name: str
- nickname: str|None
- process: str|None
- role: str|None
-
- # @field_validator('process')
- # @classmethod
- # def remove_dupes(cls, value):
- # if isinstance(value, list):
- # return list(set(value))
- # else:
- # return value
-
- # def toForm(self, parent):
- # from frontend.widgets.equipment_usage import EquipmentCheckBox
- # return EquipmentCheckBox(parent=parent, equipment=self)
-
- def toSQL(self, submission:BasicSubmission|str=None):
- if isinstance(submission, str):
- submission = BasicSubmission.query(rsl_number=submission)
- equipment = Equipment.query(asset_number=self.asset_number)
- if equipment == None:
- return
- if submission != None:
- assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
- assoc.process = self.process
- assoc.role = self.role
- # equipment.equipment_submission_associations.append(assoc)
- equipment.equipment_submission_associations.append(assoc)
- else:
- assoc = None
- return equipment, assoc
-
class PydEquipmentRole(BaseModel):
name: str
@@ -819,4 +872,4 @@ class PydEquipmentRole(BaseModel):
def toForm(self, parent, submission_type, used):
from frontend.widgets.equipment_usage import RoleComboBox
return RoleComboBox(parent=parent, role=self, submission_type=submission_type, used=used)
-
+
\ No newline at end of file
diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py
index e9fb613..a2c0536 100644
--- a/src/submissions/frontend/widgets/equipment_usage.py
+++ b/src/submissions/frontend/widgets/equipment_usage.py
@@ -10,17 +10,14 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
- def __init__(self, parent, submission_type:SubmissionType|str, submission:BasicSubmission) -> QDialog:
+ def __init__(self, parent, submission:BasicSubmission) -> QDialog:
super().__init__(parent)
+ self.submission = submission
self.setWindowTitle("Equipment Checklist")
- self.used_equipment = submission.get_used_equipment()
+ self.used_equipment = self.submission.get_used_equipment()
+ self.kit = self.submission.extraction_kit
logger.debug(f"Existing equipment: {self.used_equipment}")
- if isinstance(submission_type, str):
- self.submission_type = SubmissionType.query(name=submission_type)
- else:
- self.submission_type = submission_type
- # self.static_equipment = submission_type.get_equipment()
- self.opt_equipment = self.submission_type.get_equipment()
+ self.opt_equipment = submission.submission_type.get_equipment()
logger.debug(f"EquipmentRoles: {self.opt_equipment}")
self.layout = QVBoxLayout()
self.setLayout(self.layout)
@@ -31,20 +28,44 @@ class EquipmentUsage(QDialog):
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
+ label = self.LabelRow(parent=self)
+ self.layout.addWidget(label)
for eq in self.opt_equipment:
- self.layout.addWidget(eq.toForm(parent=self, submission_type=self.submission_type, used=self.used_equipment))
+ widg = eq.toForm(parent=self, submission_type=self.submission.submission_type, used=self.used_equipment)
+ self.layout.addWidget(widg)
+ widg.update_processes()
self.layout.addWidget(self.buttonBox)
def parse_form(self):
output = []
for widget in self.findChildren(QWidget):
match widget:
- case (EquipmentCheckBox()|RoleComboBox()) :
- output.append(widget.parse_form())
+ case RoleComboBox() :
+ if widget.check.isChecked():
+ output.append(widget.parse_form())
case _:
pass
return [item for item in output if item != None]
+
+ class LabelRow(QWidget):
+ def __init__(self, parent) -> None:
+ super().__init__(parent)
+ self.layout = QHBoxLayout()
+ self.check = QCheckBox()
+ self.layout.addWidget(self.check)
+ self.check.stateChanged.connect(self.check_all)
+ for item in ["Role", "Equipment", "Process"]:
+ l = QLabel(item)
+ l.setMaximumWidth(200)
+ l.setMinimumWidth(200)
+ self.layout.addWidget(l)
+ self.setLayout(self.layout)
+
+ def check_all(self):
+ for object in self.parent().findChildren(QCheckBox):
+ object.setChecked(self.check.isChecked())
+
class EquipmentCheckBox(QWidget):
def __init__(self, parent, equipment:PydEquipment) -> None:
@@ -56,7 +77,6 @@ class EquipmentCheckBox(QWidget):
self.check = QCheckBox()
if equipment.static:
self.check.setChecked(True)
- # self.check.setEnabled(False)
if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})"
else:
@@ -87,28 +107,42 @@ class RoleComboBox(QWidget):
else:
self.check.setChecked(True)
self.box = QComboBox()
- self.box.setMaximumWidth(125)
- self.box.setMinimumWidth(125)
+ self.box.setMaximumWidth(200)
+ self.box.setMinimumWidth(200)
self.box.addItems([item.name for item in role.equipment])
+ self.box.currentTextChanged.connect(self.update_processes)
# self.check = QCheckBox()
# self.layout.addWidget(label)
self.process = QComboBox()
- self.process.setMaximumWidth(125)
- self.process.setMinimumWidth(125)
+ self.process.setMaximumWidth(200)
+ self.process.setMinimumWidth(200)
self.process.setEditable(True)
# self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
- self.process.addItems(role.processes)
+ # self.process.addItems(role.processes)
self.layout.addWidget(self.check)
- self.layout.addWidget(QLabel(f"{role.name}:"))
+ label = QLabel(f"{role.name}:")
+ label.setMinimumWidth(200)
+ label.setMaximumWidth(200)
+ label.setAlignment(Qt.AlignmentFlag.AlignLeft)
+ self.layout.addWidget(label)
self.layout.addWidget(self.box)
self.layout.addWidget(self.process)
# self.layout.addWidget(self.check)
self.setLayout(self.layout)
+ # self.update_processes()
+
+ def update_processes(self):
+ equip = self.box.currentText()
+ logger.debug(f"Updating equipment: {equip}")
+ equip2 = [item for item in self.role.equipment if item.name==equip][0]
+ logger.debug(f"Using: {equip2}")
+ self.process.clear()
+ self.process.addItems([item for item in equip2.processes if item in self.role.processes])
def parse_form(self) -> str|None:
eq = Equipment.query(name=self.box.currentText())
- if self.check:
- return PydEquipment(name=eq.name, process=self.process.currentText(), role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
- else:
- return None
+ # if self.check.isChecked():
+ return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
+ # else:
+ # return None
\ No newline at end of file
diff --git a/src/submissions/frontend/widgets/gel_checker.py b/src/submissions/frontend/widgets/gel_checker.py
new file mode 100644
index 0000000..aed5086
--- /dev/null
+++ b/src/submissions/frontend/widgets/gel_checker.py
@@ -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
\ No newline at end of file
diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py
new file mode 100644
index 0000000..2caf947
--- /dev/null
+++ b/src/submissions/frontend/widgets/submission_details.py
@@ -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
+
\ No newline at end of file
diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py
index c699f17..72d1cba 100644
--- a/src/submissions/frontend/widgets/submission_table.py
+++ b/src/submissions/frontend/widgets/submission_table.py
@@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
-from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation, Process
+from backend.db.models import BasicSubmission, Equipment
from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from xhtml2pdf import pisa
@@ -95,8 +95,8 @@ class SubmissionsSheet(QTableView):
self.resizeColumnsToContents()
self.resizeRowsToContents()
self.setSortingEnabled(True)
-
- self.doubleClicked.connect(self.show_details)
+ # self.doubleClicked.connect(self.show_details)
+ self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
def setData(self) -> None:
"""
@@ -114,16 +114,16 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
- def show_details(self) -> None:
- """
- creates detailed data to show in seperate window
- """
- logger.debug(f"Sheet.app: {self.app}")
- index = (self.selectionModel().currentIndex())
- value = index.sibling(index.row(),0).data()
- dlg = SubmissionDetails(parent=self, id=value)
- if dlg.exec():
- pass
+ # def show_details(self, submission:BasicSubmission) -> None:
+ # """
+ # creates detailed data to show in seperate window
+ # """
+ # logger.debug(f"Sheet.app: {self.app}")
+ # # index = (self.selectionModel().currentIndex())
+ # # value = index.sibling(index.row(),0).data()
+ # dlg = SubmissionDetails(parent=self, sub=submission)
+ # if dlg.exec():
+ # pass
def create_barcode(self) -> None:
"""
@@ -154,38 +154,47 @@ class SubmissionsSheet(QTableView):
Args:
event (_type_): the item of interest
"""
+ id = self.selectionModel().currentIndex()
+ id = id.sibling(id.row(),0).data()
+ submission = BasicSubmission.query(id=id)
self.menu = QMenu(self)
- renameAction = QAction('Delete', self)
- detailsAction = QAction('Details', self)
- # barcodeAction = QAction("Print Barcode", self)
- commentAction = QAction("Add Comment", self)
- backupAction = QAction("Backup", self)
- equipAction = QAction("Add Equipment", self)
- # hitpickAction = QAction("Hitpicks", self)
- renameAction.triggered.connect(lambda: self.delete_item(event))
- detailsAction.triggered.connect(lambda: self.show_details())
- # barcodeAction.triggered.connect(lambda: self.create_barcode())
- commentAction.triggered.connect(lambda: self.add_comment())
- backupAction.triggered.connect(lambda: self.regenerate_submission_form())
- equipAction.triggered.connect(lambda: self.add_equipment())
- # hitpickAction.triggered.connect(lambda: self.hit_pick())
- self.menu.addAction(detailsAction)
- self.menu.addAction(renameAction)
- # self.menu.addAction(barcodeAction)
- self.menu.addAction(commentAction)
- self.menu.addAction(backupAction)
- self.menu.addAction(equipAction)
- # self.menu.addAction(hitpickAction)
+ # renameAction = QAction('Delete', self)
+ # detailsAction = QAction('Details', self)
+ # commentAction = QAction("Add Comment", self)
+ # equipAction = QAction("Add Equipment", self)
+ # backupAction = QAction("Export", self)
+ # renameAction.triggered.connect(lambda: self.delete_item(submission))
+ # detailsAction.triggered.connect(lambda: self.show_details(submission))
+ # commentAction.triggered.connect(lambda: self.add_comment(submission))
+ # backupAction.triggered.connect(lambda: self.regenerate_submission_form(submission))
+ # equipAction.triggered.connect(lambda: self.add_equipment(submission))
+ # self.menu.addAction(detailsAction)
+ # self.menu.addAction(renameAction)
+ # self.menu.addAction(commentAction)
+ # self.menu.addAction(backupAction)
+ # self.menu.addAction(equipAction)
+ self.con_actions = submission.custom_context_events()
+ for k in self.con_actions.keys():
+ logger.debug(f"Adding {k}")
+ action = QAction(k, self)
+ action.triggered.connect(lambda _, action_name=k: self.triggered_action(action_name=action_name))
+ self.menu.addAction(action)
# add other required actions
self.menu.popup(QCursor.pos())
+ def triggered_action(self, action_name:str):
+ logger.debug(f"Action: {action_name}")
+ logger.debug(f"Responding with {self.con_actions[action_name]}")
+ func = self.con_actions[action_name]
+ func(obj=self)
+
def add_equipment(self):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value)
- def add_equipment_function(self, rsl_plate_id):
- submission = BasicSubmission.query(id=rsl_plate_id)
+ def add_equipment_function(self, submission:BasicSubmission):
+ # submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission)
if dlg.exec():
@@ -193,29 +202,33 @@ class SubmissionsSheet(QTableView):
logger.debug(f"We've got equipment: {equipment}")
for equip in equipment:
e = Equipment.query(name=equip.name)
- assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
- process = Process.query(name=equip.process)
- assoc.process = process
- assoc.role = equip.role
+ # assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
+ # process = Process.query(name=equip.processes)
+ # assoc.process = process
+ # assoc.role = equip.role
+ _, assoc = equip.toSQL(submission=submission)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
+ else:
+ pass
- def delete_item(self, event):
+ def delete_item(self, submission:BasicSubmission):
"""
Confirms user deletion and sends id to backend for deletion.
Args:
event (_type_): the item of interest
"""
- index = (self.selectionModel().currentIndex())
- value = index.sibling(index.row(),0).data()
- logger.debug(index)
- msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
+ # index = (self.selectionModel().currentIndex())
+ # value = index.sibling(index.row(),0).data()
+ # logger.debug(index)
+ # msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
+ msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {submission.rsl_plate_num}?\n")
if msg.exec():
# delete_submission(id=value)
- BasicSubmission.query(id=value).delete()
+ submission.delete()
else:
return
self.setData()
@@ -424,221 +437,11 @@ class SubmissionsSheet(QTableView):
writer.close()
self.report.add_result(report)
- def regenerate_submission_form(self):
- index = (self.selectionModel().currentIndex())
- value = index.sibling(index.row(),0).data()
- logger.debug(index)
- # msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n")
- # if msg.exec():
- # delete_submission(id=value)
- sub = BasicSubmission.query(id=value)
- fname = select_save_file(self, default_name=sub.to_pydantic().construct_filename(), extension="xlsx")
- sub.backup(fname=fname, full_backup=False)
+ def regenerate_submission_form(self, submission:BasicSubmission):
+ # index = (self.selectionModel().currentIndex())
+ # value = index.sibling(index.row(),0).data()
+ # logger.debug(index)
+ # sub = BasicSubmission.query(id=value)
+ fname = select_save_file(self, default_name=submission.to_pydantic().construct_filename(), extension="xlsx")
+ submission.backup(fname=fname, full_backup=False)
-class SubmissionDetails(QDialog):
- """
- a window showing text details of submission
- """
- def __init__(self, parent, id:int) -> None:
-
- super().__init__(parent)
- # self.ctx = ctx
- try:
- self.app = parent.parent().parent().parent().parent().parent().parent()
- except AttributeError:
- self.app = None
- self.setWindowTitle("Submission Details")
- # create scrollable interior
- interior = QScrollArea()
- interior.setParent(self)
- # get submision from db
- # sub = lookup_submissions(ctx=ctx, id=id)
- sub = BasicSubmission.query(id=id)
- logger.debug(f"Submission details data:\n{pformat(sub.to_dict())}")
- self.base_dict = sub.to_dict(full_data=True)
- # don't want id
- del self.base_dict['id']
- logger.debug(f"Creating barcode.")
- if not check_if_app():
- self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
- logger.debug(f"Hitpicking plate...")
- self.plate_dicto = sub.hitpick_plate()
- logger.debug(f"Making platemap...")
- self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
- # logger.debug(f"Platemap: {self.base_dict['platemap']}")
- # logger.debug(f"platemap: {platemap}")
- # image_io = BytesIO()
- # try:
- # platemap.save(image_io, 'JPEG')
- # except AttributeError:
- # logger.error(f"No plate map found for {sub.rsl_plate_num}")
- # self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
- self.template = env.get_template("submission_details.html")
- self.html = self.template.render(sub=self.base_dict)
- webview = QWebEngineView()
- webview.setMinimumSize(900, 500)
- webview.setMaximumSize(900, 500)
- webview.setHtml(self.html)
- self.layout = QVBoxLayout()
- interior.resize(900, 500)
- interior.setWidget(webview)
- self.setFixedSize(900, 500)
- # button to export a pdf version
- btn = QPushButton("Export PDF")
- btn.setParent(self)
- btn.setFixedWidth(900)
- btn.clicked.connect(self.export)
-
- def export(self):
- """
- Renders submission to html, then creates and saves .pdf file to user selected file.
- """
- # try:
- # home_dir = Path(self.ctx.directory_path).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
- # except FileNotFoundError:
- # home_dir = Path.home().resolve().__str__()
- # fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
- # if fname.__str__() == ".":
- # logger.debug("Saving pdf was cancelled.")
- # return
- fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
- del self.base_dict['platemap']
- export_map = make_plate_map(self.plate_dicto)
- image_io = BytesIO()
- try:
- export_map.save(image_io, 'JPEG')
- except AttributeError:
- logger.error(f"No plate map found")
- self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
- self.html2 = self.template.render(sub=self.base_dict)
- try:
- with open(fname, "w+b") as f:
- pisa.CreatePDF(self.html2, dest=f)
- except PermissionError as e:
- logger.error(f"Error saving pdf: {e}")
- msg = QMessageBox()
- msg.setText("Permission Error")
- msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
- msg.setWindowTitle("Permission Error")
- msg.exec()
-
-class BarcodeWindow(QDialog):
-
- def __init__(self, rsl_num:str):
- super().__init__()
- # set the title
- self.setWindowTitle("Image")
- self.layout = QVBoxLayout()
- # setting the geometry of window
- self.setGeometry(0, 0, 400, 300)
- # creating label
- self.label = QLabel()
- self.img = make_plate_barcode(rsl_num)
- self.pixmap = QPixmap()
- self.pixmap.loadFromData(self.img)
- # adding image to label
- self.label.setPixmap(self.pixmap)
- # show all the widgets]
- QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
- self.buttonBox = QDialogButtonBox(QBtn)
- self.buttonBox.accepted.connect(self.accept)
- self.buttonBox.rejected.connect(self.reject)
- self.layout.addWidget(self.label)
- self.layout.addWidget(self.buttonBox)
- self.setLayout(self.layout)
- self._createActions()
- self._createToolBar()
- self._connectActions()
-
-
-
- def _createToolBar(self):
- """
- adds items to menu bar
- """
- toolbar = QToolBar("My main toolbar")
- toolbar.addAction(self.printAction)
-
-
- def _createActions(self):
- """
- creates actions
- """
- self.printAction = QAction("&Print", self)
-
-
- def _connectActions(self):
- """
- connect menu and tool bar item to functions
- """
- self.printAction.triggered.connect(self.print_barcode)
-
-
- def print_barcode(self):
- """
- Sends barcode image to printer.
- """
- printer = QtPrintSupport.QPrinter()
- dialog = QtPrintSupport.QPrintDialog(printer)
- if dialog.exec():
- self.handle_paint_request(printer, self.pixmap.toImage())
-
-
- def handle_paint_request(self, printer:QtPrintSupport.QPrinter, im):
- logger.debug(f"Hello from print handler.")
- painter = QPainter(printer)
- image = QPixmap.fromImage(im)
- painter.drawPixmap(120, -20, image)
- painter.end()
-
-class SubmissionComment(QDialog):
- """
- a window for adding comment text to a submission
- """
- def __init__(self, parent, rsl:str) -> None:
-
- super().__init__(parent)
- # self.ctx = ctx
- try:
- self.app = parent.parent().parent().parent().parent().parent().parent
- print(f"App: {self.app}")
- except AttributeError:
- pass
- self.rsl = rsl
- self.setWindowTitle(f"{self.rsl} Submission Comment")
- # create text field
- self.txt_editor = QTextEdit(self)
- self.txt_editor.setReadOnly(False)
- self.txt_editor.setText("Add Comment")
- QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
- self.buttonBox = QDialogButtonBox(QBtn)
- self.buttonBox.accepted.connect(self.accept)
- self.buttonBox.rejected.connect(self.reject)
- self.layout = QVBoxLayout()
- self.setFixedSize(400, 300)
- self.layout.addWidget(self.txt_editor)
- self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
- self.setLayout(self.layout)
-
- def add_comment(self):
- """
- Adds comment to submission object.
- """
- commenter = getuser()
- comment = self.txt_editor.toPlainText()
- dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
-
- full_comment = [{"name":commenter, "time": dt, "text": comment}]
- logger.debug(f"Full comment: {full_comment}")
- sub = BasicSubmission.query(rsl_number=self.rsl)
- try:
- # For some reason .append results in new comment being ignores, so have to concatenate lists.
- sub.comment = sub.comment + full_comment
- except (AttributeError, TypeError) as e:
- logger.error(f"Hit error {e} creating comment")
- sub.comment = full_comment
- # logger.debug(sub.comment)
- sub.save(original=False)
- # logger.debug(f"Save result: {result}")
-
-
\ No newline at end of file
diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py
index 239aa36..10e6f0c 100644
--- a/src/submissions/frontend/widgets/submission_widget.py
+++ b/src/submissions/frontend/widgets/submission_widget.py
@@ -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):
diff --git a/src/submissions/templates/submission_details.html b/src/submissions/templates/submission_details.html
index 097ae53..ce811d0 100644
--- a/src/submissions/templates/submission_details.html
+++ b/src/submissions/templates/submission_details.html
@@ -48,7 +48,7 @@
{% if sub['equipment'] %}
Equipment:
') }}
+ {{ item['role'] }}: {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '
') }}
{% endfor %}
{% for entry in sub['comments'] %} +
{% for entry in sub['comment'] %}
{{ entry['name'] }}:
{{ entry['text'] }}
- {{ entry['time'] }}
{% endfor %}