From 319f72cab2d2969ed45c48e4741883ffd7e2bb0e Mon Sep 17 00:00:00 2001
From: Landon Wark
Date: Fri, 19 Jan 2024 15:17:07 -0600
Subject: [PATCH] Midway through disaster of changing table names.
---
.../10c47a04559d_adding_in_processes.py | 46 --
...863_submissionreagentassociations_added.py | 44 --
...c_link_controls_with_bacterial_culture_.py | 34 --
.../30aab47d6f12_adding_role_tag_to_.py | 40 --
.../36a47d8837ca_adding_in_equipment.py | 52 --
...4fecbbe91_adding_times_to_equipsubassoc.py | 34 --
.../4606a7be32e8_adding_submissiontype_to_.py | 34 --
..._adjusting_process_submissionequipassoc.py | 38 --
...61baf9d7842_adding_equipment_clustering.py | 32 --
...ca468_add_templates_to_submission_types.py | 32 --
.../94289d4e63e6_updating_primary_key_for_.py | 36 --
.../bc7a74476609_adding_equipment_roles.py | 59 --
...ed_adding_static_option_to_equipstassoc.py | 32 --
...25b5d2a_adding_process_to_equipsubassoc.py | 32 --
.../e08a69a0f381_attaching_process_to_.py | 32 --
src/submissions/__init__.py | 2 +-
src/submissions/backend/db/models/__init__.py | 15 +-
src/submissions/backend/db/models/controls.py | 9 +-
src/submissions/backend/db/models/kits.py | 460 +++++-----------
.../backend/db/models/organizations.py | 7 +-
.../backend/db/models/submissions.py | 509 +++++++++++-------
src/submissions/backend/excel/parser.py | 17 +-
.../backend/validators/__init__.py | 11 +-
src/submissions/backend/validators/pydant.py | 135 +++--
.../frontend/widgets/equipment_usage.py | 78 ++-
.../frontend/widgets/gel_checker.py | 93 ++++
.../frontend/widgets/submission_details.py | 198 +++++++
.../frontend/widgets/submission_table.py | 329 +++--------
.../frontend/widgets/submission_widget.py | 30 +-
.../templates/submission_details.html | 6 +-
src/submissions/tools.py | 21 +
31 files changed, 1040 insertions(+), 1457 deletions(-)
delete mode 100644 alembic/versions/10c47a04559d_adding_in_processes.py
delete mode 100644 alembic/versions/238c3c3e5863_submissionreagentassociations_added.py
delete mode 100644 alembic/versions/2684f065037c_link_controls_with_bacterial_culture_.py
delete mode 100644 alembic/versions/30aab47d6f12_adding_role_tag_to_.py
delete mode 100644 alembic/versions/36a47d8837ca_adding_in_equipment.py
delete mode 100644 alembic/versions/3e94fecbbe91_adding_times_to_equipsubassoc.py
delete mode 100644 alembic/versions/4606a7be32e8_adding_submissiontype_to_.py
delete mode 100644 alembic/versions/67fa77849024_adjusting_process_submissionequipassoc.py
delete mode 100644 alembic/versions/761baf9d7842_adding_equipment_clustering.py
delete mode 100644 alembic/versions/7e7b6eeca468_add_templates_to_submission_types.py
delete mode 100644 alembic/versions/94289d4e63e6_updating_primary_key_for_.py
delete mode 100644 alembic/versions/bc7a74476609_adding_equipment_roles.py
delete mode 100644 alembic/versions/cd11db3794ed_adding_static_option_to_equipstassoc.py
delete mode 100644 alembic/versions/cd5c225b5d2a_adding_process_to_equipsubassoc.py
delete mode 100644 alembic/versions/e08a69a0f381_attaching_process_to_.py
create mode 100644 src/submissions/frontend/widgets/gel_checker.py
create mode 100644 src/submissions/frontend/widgets/submission_details.py
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""
-
-# @validates('required')
-# def validate_age(self, key, value):
-# """
-# Ensures only 1 & 0 used in 'required'
-
-# Args:
-# key (str): name of attribute
-# value (_type_): value of attribute
-
-# Raises:
-# ValueError: Raised if bad value given
-
-# Returns:
-# _type_: value
-# """
-# if not 0 <= value < 2:
-# raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
-# return value
-# @validates('reagenttype')
-# def validate_reagenttype(self, key, value):
-# """
-# Ensures reagenttype is an actual ReagentType
-
-# Args:
-# key (str)): name of attribute
-# value (_type_): value of attribute
-
-# Raises:
-# ValueError: raised if reagenttype is not a ReagentType
-
-# Returns:
-# _type_: ReagentType
-# """
-# if not isinstance(value, ReagentType):
-# raise ValueError(f'{value} is not a reagenttype')
-# return value
-
-# @classmethod
-# @setup_lookup
-# def query(cls,
-# kit_type:KitType|str|None=None,
-# reagent_type:ReagentType|str|None=None,
-# limit:int=0
-# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
-# """
-# Lookup junction of ReagentType and KitType
-
-# Args:
-# kit_type (models.KitType | str | None): KitType of interest.
-# reagent_type (models.ReagentType | str | None): ReagentType of interest.
-# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
-
-# Returns:
-# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
-# """
-# query: Query = cls.__database_session__.query(cls)
-# match kit_type:
-# case KitType():
-# query = query.filter(cls.kit_type==kit_type)
-# case str():
-# query = query.join(KitType).filter(KitType.name==kit_type)
-# case _:
-# pass
-# match reagent_type:
-# case ReagentType():
-# query = query.filter(cls.reagent_type==reagent_type)
-# case str():
-# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
-# case _:
-# pass
-# if kit_type != None and reagent_type != None:
-# limit = 1
-# return query_return(query=query, limit=limit)
-
-# def save(self) -> Report:
-# """
-# Adds this instance to the database and commits.
-
-# Returns:
-# Report: Result of save action
-# """
-# report = Report()
-# self.__database_session__.add(self)
-# self.__database_session__.commit()
-# return report
+ @check_authorization
+ def save(self, ctx:Settings):
+ super().save()
class Reagent(BaseClass):
"""
Concrete reagent instance
"""
- __tablename__ = "_reagents"
+ # __tablename__ = "_reagents"
id = Column(INTEGER, primary_key=True) #: primary key
type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type
@@ -412,7 +308,6 @@ class Reagent(BaseClass):
name = Column(String(64)) #: reagent name
lot = Column(String(64)) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
- # submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in
reagent_submission_associations = relationship(
"SubmissionReagentAssociation",
@@ -497,6 +392,7 @@ class Reagent(BaseClass):
def query(cls,
reagent_type:str|ReagentType|None=None,
lot_number:str|None=None,
+ name:str|None=None,
limit:int=0
) -> Reagent|List[Reagent]:
"""
@@ -505,6 +401,7 @@ class Reagent(BaseClass):
Args:
reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None.
lot_number (str | None, optional): Reagent lot number. Defaults to None.
+ name (str | None, optional): Reagent name. Defaults to None.
limit (int, optional): limit of results returned. Defaults to 0.
Returns:
@@ -521,6 +418,12 @@ class Reagent(BaseClass):
query = query.filter(cls.type.contains(reagent_type))
case _:
pass
+ match name:
+ case str():
+ logger.debug(f"Looking up reagent by name: {name}")
+ query = query.filter(cls.name==name)
+ case _:
+ pass
match lot_number:
case str():
logger.debug(f"Looking up reagent by lot number: {lot_number}")
@@ -530,19 +433,12 @@ class Reagent(BaseClass):
case _:
pass
return query_return(query=query, limit=limit)
-
- def save(self):
- """
- Add this instance to the database and commit
- """
- self.__database_session__.add(self)
- self.__database_session__.commit()
class Discount(BaseClass):
"""
Relationship table for client labs for certain kits.
"""
- __tablename__ = "_discounts"
+ # __tablename__ = "_discounts"
id = Column(INTEGER, primary_key=True) #: primary key
kit = relationship("KitType") #: joined parent reagent type
@@ -604,17 +500,20 @@ class Discount(BaseClass):
pass
return query.all()
+ @check_authorization
+ def save(self, ctx:Settings):
+ super().save()
+
class SubmissionType(BaseClass):
"""
Abstract of types of submissions.
"""
- __tablename__ = "_submission_types"
+ # __tablename__ = "_submission_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(128), unique=True) #: name of submission type
info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type.
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
- # regex = Column(String(512))
template_file = Column(BLOB) #: Blank form for this type stored as binary.
processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes)
@@ -664,20 +563,25 @@ class SubmissionType(BaseClass):
output = []
for item in self.submissiontype_equipmentrole_associations:
map = item.uses
- map['role'] = item.equipment_role.name
+ if map == None:
+ map = {}
+ try:
+ map['role'] = item.equipment_role.name
+ except TypeError:
+ pass
output.append(map)
return output
# return [item.uses for item in self.submissiontype_equipmentrole_associations]
- def get_equipment(self) -> List['PydEquipmentRole']:
- return [item.to_pydantic(submission_type=self) for item in self.equipment]
+ def get_equipment(self, extraction_kit:str|KitType|None=None) -> List['PydEquipmentRole']:
+ return [item.to_pydantic(submission_type=self, extraction_kit=extraction_kit) for item in self.equipment]
- def get_processes_for_role(self, equipment_role:str|EquipmentRole):
+ def get_processes_for_role(self, equipment_role:str|EquipmentRole, kit:str|KitType|None=None):
match equipment_role:
case str():
- relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
+ relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
case EquipmentRole():
- relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
+ relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
case _:
raise TypeError(f"Type {type(equipment_role)} is not allowed")
return list(set([item for items in relevant for item in items if item != None ]))
@@ -728,7 +632,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
"""
Abstract of relationship between kits and their submission type.
"""
- __tablename__ = "_submissiontypes_kittypes"
+ # __tablename__ = "_submissiontypes_kittypes"
submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of joined submission type
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of joined kit
@@ -801,7 +705,7 @@ class KitTypeReagentTypeAssociation(BaseClass):
table containing reagenttype/kittype associations
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
"""
- __tablename__ = "_reagenttypes_kittypes"
+ # __tablename__ = "_reagenttypes_kittypes"
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
@@ -902,22 +806,9 @@ class KitTypeReagentTypeAssociation(BaseClass):
limit = 1
return query_return(query=query, limit=limit)
- def save(self) -> Report:
- """
- Adds this instance to the database and commits.
-
- Returns:
- Report: Result of save action
- """
- report = Report()
- self.__database_session__.add(self)
- self.__database_session__.commit()
- return report
-
-
class SubmissionReagentAssociation(BaseClass):
- __tablename__ = "_reagents_submissions"
+ # __tablename__ = "_reagents_submissions"
reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True)
@@ -961,8 +852,6 @@ class SubmissionReagentAssociation(BaseClass):
# logger.debug(f"Filtering query with reagent: {reagent}")
reagent = Reagent.query(lot_number=reagent)
query = query.filter(cls.reagent==reagent)
- # logger.debug([item.reagent.lot for item in query.all()])
- # query = query.join(Reagent).filter(Reagent.lot==reagent)
case _:
pass
# logger.debug(f"Result of query after reagent: {query.all()}")
@@ -976,7 +865,6 @@ class SubmissionReagentAssociation(BaseClass):
case _:
pass
# logger.debug(f"Result of query after submission: {query.all()}")
- # limit = query.count()
return query_return(query=query, limit=limit)
def to_sub_dict(self, extraction_kit):
@@ -989,13 +877,14 @@ class Equipment(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
- __tablename__ = "_equipment"
+ # __tablename__ = "_equipment"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
nickname = Column(String(64))
asset_number = Column(String(16))
roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment)
+ processes = relationship("Process", back_populates="equipment", secondary=equipment_processes)
equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation",
@@ -1008,10 +897,30 @@ class Equipment(BaseClass):
def __repr__(self):
return f""
- def get_processes(self, submission_type:SubmissionType):
- processes = [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
+ def to_dict(self, processes:bool=False):
+ if not processes:
+ return {k:v for k,v in self.__dict__.items() if k != 'processes'}
+ else:
+ return {k:v for k,v in self.__dict__.items()}
+
+ def get_processes(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
+ processes = [process for process in self.processes if submission_type in process.submission_types]
+ match extraction_kit:
+ case str():
+ processes = [process for process in processes if extraction_kit in [kit.name for kit in process.kit_types]]
+ case KitType():
+ processes = [process for process in processes if extraction_kit in process.kit_types]
+ case _:
+ pass
+ processes = [process.name for process in processes]
+ # try:
+ assert all([isinstance(process, str) for process in processes])
+ # except AssertionError as e:
+ # logger.error(processes)
+ # raise e
if len(processes) == 0:
processes = ['']
+ # logger.debug(f"Processes: {processes}")
return processes
@classmethod
@@ -1043,10 +952,10 @@ class Equipment(BaseClass):
pass
return query_return(query=query, limit=limit)
- def to_pydantic(self, submission_type:SubmissionType):
+ def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipment
- # return PydEquipment(process=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
- return PydEquipment(process=None, role=None, **self.__dict__)
+ return PydEquipment(processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=None, **self.to_dict(processes=False))
+ # return PydEquipment(process=None, role=None, **self.__dict__)
def save(self):
self.__database_session__.add(self)
@@ -1064,12 +973,12 @@ class Equipment(BaseClass):
class EquipmentRole(BaseClass):
- __tablename__ = "_equipment_roles"
+ # __tablename__ = "_equipment_roles"
id = Column(INTEGER, primary_key=True)
name = Column(String(32))
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
- processes = relationship("Process", back_populates="equipment_roles", secondary=equipmentroles_processes)
+ processes = relationship("Process", back_populates='equipment_roles', secondary=equipmentroles_processes)
equipmentrole_submissiontype_associations = relationship(
"SubmissionTypeEquipmentRoleAssociation",
@@ -1081,12 +990,24 @@ class EquipmentRole(BaseClass):
def __repr__(self):
return f""
+
+ def to_dict(self):
+ output = {}
+ for key, value in self.__dict__.items():
+ match key:
+ case "processes":
+ pass
+ case _:
+ value = value
+ output[key] = value
+ return output
- def to_pydantic(self, submission_type:SubmissionType):
+ def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipmentRole
- equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
- pyd_dict = self.__dict__
- pyd_dict['processes'] = self.get_processes(submission_type=submission_type)
+ equipment = [item.to_pydantic(submission_type=submission_type, extraction_kit=extraction_kit) for item in self.instances]
+ # processes = [item.name for item in self.processes]
+ pyd_dict = self.to_dict()
+ pyd_dict['processes'] = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit)
return PydEquipmentRole(equipment=equipment, **pyd_dict)
@classmethod
@@ -1107,31 +1028,36 @@ class EquipmentRole(BaseClass):
pass
return query_return(query=query, limit=limit)
- def get_processes(self, submission_type:str|SubmissionType|None) -> List[Process]:
+ def get_processes(self, submission_type:str|SubmissionType|None, extraction_kit:str|KitType|None=None) -> List[Process]:
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
+ # assert all([isinstance(process, Process) for process in self.processes])
+ # logger.debug(self.processes)
if submission_type != None:
- output = [process.name for process in self.processes if submission_type in process.submission_types]
+ # for process in self.processes:
+ # logger.debug(f"Process: {type(process)}: {process}")
+ processes = [process for process in self.processes if submission_type in process.submission_types]
else:
- output = [process.name for process in self.processes]
+ processes = self.processes
+ match extraction_kit:
+ case str():
+ processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
+ case KitType():
+ processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
+ case _:
+ pass
+ output = [item.name for item in processes]
if len(output) == 0:
return ['']
else:
return output
-
- def save(self):
- try:
- self.__database_session__.add(self)
- self.__database_session__.commit()
- except:
- self.__database_session__.rollback()
class SubmissionEquipmentAssociation(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
- __tablename__ = "_equipment_submissions"
+ # __tablename__ = "_equipment_submissions"
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
@@ -1144,14 +1070,14 @@ class SubmissionEquipmentAssociation(BaseClass):
submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission
- equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated submission
+ equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
def __init__(self, submission, equipment):
self.submission = submission
self.equipment = equipment
def to_sub_dict(self) -> dict:
- output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=self.process.name, role=self.role, nickname=self.equipment.nickname)
+ output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, processes=[self.process.name], role=self.role, nickname=self.equipment.nickname)
return output
def save(self):
@@ -1162,7 +1088,7 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
# __abstract__ = True
- __tablename__ = "_submissiontype_equipmentrole"
+ # __tablename__ = "_submissiontype_equipmentrole"
equipmentrole_id = Column(INTEGER, ForeignKey("_equipment_roles.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
@@ -1192,25 +1118,38 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value
- def get_all_processes(self):
+ def get_all_processes(self, extraction_kit:KitType|str|None=None):
processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances]
+ # flatten list
processes = [item for items in processes for item in items if item != None ]
+ match extraction_kit:
+ case str():
+ processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
+ case KitType():
+ processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
+ case _:
+ pass
return processes
@check_authorization
def save(self, ctx:Settings):
- self.__database_session__.add(self)
- self.__database_session__.commit()
+ # self.__database_session__.add(self)
+ # self.__database_session__.commit()
+ super().save()
class Process(BaseClass):
-
- __tablename__ = "_process"
+ """
+ A Process is a method used by a piece of equipment.
+ """
+ # __tablename__ = "_process"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes)
+ equipment = relationship("Equipment", back_populates='processes', secondary=equipment_processes)
equipment_roles = relationship("EquipmentRole", back_populates='processes', secondary=equipmentroles_processes)
submissions = relationship("SubmissionEquipmentAssociation", backref='process')
+ kit_types = relationship("KitType", back_populates='processes', secondary=kittypes_processes)
def __repr__(self):
return f" str:
-# return f""
-
-# @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
diff --git a/src/submissions/backend/db/models/organizations.py b/src/submissions/backend/db/models/organizations.py
index ab6c1e3..4af9f7a 100644
--- a/src/submissions/backend/db/models/organizations.py
+++ b/src/submissions/backend/db/models/organizations.py
@@ -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
diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py
index 98faa16..c115f89 100644
--- a/src/submissions/backend/db/models/submissions.py
+++ b/src/submissions/backend/db/models/submissions.py
@@ -13,23 +13,24 @@ from json.decoder import JSONDecodeError
from sqlalchemy.ext.associationproxy import association_proxy
import pandas as pd
from openpyxl import Workbook
-from . import BaseClass
+from . import BaseClass, Equipment
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
from datetime import datetime, date, time
-from typing import List
+from typing import List, Any
from dateutil.parser import parse
from dateutil.parser._parser import ParserError
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from pathlib import Path
+
logger = logging.getLogger(f"submissions.{__name__}")
class BasicSubmission(BaseClass):
"""
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
"""
- __tablename__ = "_submissions"
+ # __tablename__ = "_submissions"
id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
@@ -96,7 +97,7 @@ class BasicSubmission(BaseClass):
"""
return f"{self.rsl_plate_num} - {self.submitter_plate_num}"
- def to_dict(self, full_data:bool=False) -> dict:
+ def to_dict(self, full_data:bool=False, backup:bool=False) -> dict:
"""
Constructs dictionary used in submissions summary
@@ -137,7 +138,7 @@ class BasicSubmission(BaseClass):
logger.error(f"We got an error retrieving reagents: {e}")
reagents = None
# samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations]
- samples = [item.to_sub_dict() for item in self.submission_sample_associations]
+ samples = self.adjust_to_dict_samples(backup=backup)
try:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if len(equipment) == 0:
@@ -255,18 +256,7 @@ class BasicSubmission(BaseClass):
Returns:
list: list of htipick dictionaries for each sample
"""
- output_list = []
- for assoc in self.submission_sample_associations:
- samp = assoc.sample.to_hitpick(submission_rsl=self.rsl_plate_num)
- if samp != None:
- if plate_number != None:
- samp['plate_number'] = plate_number
- samp['row'] = assoc.row
- samp['column'] = assoc.column
- samp['plate_name'] = self.rsl_plate_num
- output_list.append(samp)
- else:
- continue
+ output_list = [assoc.to_hitpick() for assoc in self.submission_sample_associations]
return output_list
@classmethod
@@ -548,7 +538,7 @@ class BasicSubmission(BaseClass):
result = assoc.save()
return result
- def to_pydantic(self):
+ def to_pydantic(self, backup:bool=False):
"""
Converts this instance into a PydSubmission
@@ -556,7 +546,7 @@ class BasicSubmission(BaseClass):
PydSubmission: converted object.
"""
from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment
- dicto = self.to_dict(full_data=True)
+ dicto = self.to_dict(full_data=True, backup=backup)
logger.debug(f"Backup dictionary: {pformat(dicto)}")
# dicto['filepath'] = Path(tempfile.TemporaryFile().name)
new_dict = {}
@@ -567,7 +557,11 @@ class BasicSubmission(BaseClass):
case "samples":
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
case "equipment":
- new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
+ # logger.debug(f"\n\nEquipment: {dicto['equipment']}\n\n")
+ try:
+ new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
+ except TypeError as e:
+ logger.error(f"Possible no equipment error: {e}")
case "Plate Number":
new_dict['rsl_plate_num'] = dict(value=value, missing=True)
case "Submitter Plate Number":
@@ -582,25 +576,6 @@ class BasicSubmission(BaseClass):
# sys.exit()
return PydSubmission(**new_dict)
- def backup(self, fname:Path, full_backup:bool=True):
- """
- Exports xlsx and yml info files for this instance.
-
- Args:
- fname (Path): Filename of xlsx file.
- """
- if full_backup:
- backup = self.to_dict(full_data=True)
- try:
- with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
- yaml.dump(backup, f)
- except KeyError as e:
- logger.error(f"Problem saving yml backup file: {e}")
- pyd = self.to_pydantic()
- wb = pyd.autofill_excel()
- wb = pyd.autofill_samples(wb)
- wb.save(filename=fname.with_suffix(".xlsx"))
-
def save(self, original:bool=True):
"""
Adds this instance to database and commits.
@@ -610,24 +585,7 @@ class BasicSubmission(BaseClass):
"""
if original:
self.uploaded_by = getuser()
- self.__database_session__.add(self)
- self.__database_session__.commit()
-
- def delete(self):
- """
- Performs backup and deletes this instance from database.
-
- Raises:
- e: Raised in something goes wrong.
- """
- fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
- self.backup(fname=fname)
- self.__database_session__.delete(self)
- try:
- self.__database_session__.commit()
- except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
- self.__database_session__.rollback()
- raise e
+ super().save()
@classmethod
@setup_lookup
@@ -782,15 +740,121 @@ class BasicSubmission(BaseClass):
def get_used_equipment(self) -> List[str]:
return [item.role for item in self.submission_equipment_associations]
+ @classmethod
+ def adjust_autofill_samples(cls, samples:List[Any]) -> List[Any]:
+ logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler")
+ return samples
+
+ def adjust_to_dict_samples(self, backup:bool=False):
+ logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
+ return [item.to_sub_dict() for item in self.submission_sample_associations]
+
+ # Custom context events for the ui
+
+ def custom_context_events(self):
+ names = ["Delete", "Details", "Add Comment", "Add Equipment", "Export"]
+ funcs = [self.delete, self.show_details, self.add_comment, self.add_equipment, self.backup]
+ dicto = {item[0]:item[1] for item in zip(names, funcs)}
+ return dicto
+
+ def delete(self, obj=None):
+ """
+ Performs backup and deletes this instance from database.
+
+ Raises:
+ e: Raised in something goes wrong.
+ """
+ logger.debug("Hello from delete")
+ fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
+ self.backup(fname=fname, full_backup=True)
+ self.__database_session__.delete(self)
+ try:
+ self.__database_session__.commit()
+ except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
+ self.__database_session__.rollback()
+ raise e
+
+ def show_details(self, obj):
+ logger.debug("Hello from details")
+ from frontend.widgets.submission_details import SubmissionDetails
+ dlg = SubmissionDetails(parent=obj, sub=self)
+ if dlg.exec():
+ pass
+
+ def add_comment(self, obj):
+ from frontend.widgets.submission_details import SubmissionComment
+ dlg = SubmissionComment(parent=obj, submission=self)
+ if dlg.exec():
+ comment = dlg.parse_form()
+ try:
+ # For some reason .append results in new comment being ignored, so have to concatenate lists.
+ self.comment = self.comment + comment
+ except (AttributeError, TypeError) as e:
+ logger.error(f"Hit error ({e}) creating comment")
+ self.comment = comment
+ logger.debug(self.comment)
+ self.save(original=False)
+ # logger.debug(f"Save result: {result}")
+
+ def add_equipment(self, obj):
+ # submission_type = submission.submission_type_name
+ from frontend.widgets.equipment_usage import EquipmentUsage
+ dlg = EquipmentUsage(parent=obj, submission_type=self.submission_type_name, submission=self)
+ if dlg.exec():
+ equipment = dlg.parse_form()
+ logger.debug(f"We've got equipment: {equipment}")
+ for equip in equipment:
+ # e = Equipment.query(name=equip.name)
+ # assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
+ # process = Process.query(name=equip.processes)
+ # assoc.process = process
+ # assoc.role = equip.role
+ _, assoc = equip.toSQL(submission=self)
+ # submission.submission_equipment_associations.append(assoc)
+ logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
+ # submission.save()
+ assoc.save()
+ else:
+ pass
+
+ def backup(self, obj=None, fname:Path|None=None, full_backup:bool=False):
+ """
+ Exports xlsx and yml info files for this instance.
+
+ Args:
+ fname (Path): Filename of xlsx file.
+ """
+ logger.debug("Hello from backup.")
+ if fname == None:
+ from frontend.widgets.functions import select_save_file
+ from backend.validators import RSLNamer
+ abbreviation = self.get_abbreviation()
+ file_data = dict(rsl_plate_num=self.rsl_plate_num, submission_type=self.submission_type_name, submitted_date=self.submitted_date, abbreviation=abbreviation)
+ fname = select_save_file(default_name=RSLNamer.construct_new_plate_name(data=file_data), extension="xlsx", obj=obj)
+ if full_backup:
+ backup = self.to_dict(full_data=True)
+ try:
+ with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
+ yaml.dump(backup, f)
+ except KeyError as e:
+ logger.error(f"Problem saving yml backup file: {e}")
+ pyd = self.to_pydantic(backup=True)
+ wb = pyd.autofill_excel()
+ wb = pyd.autofill_samples(wb)
+ wb = pyd.autofill_equipment(wb)
+ wb.save(filename=fname.with_suffix(".xlsx"))
+
# Below are the custom submission types
class BacterialCulture(BasicSubmission):
"""
derivative submission type from BasicSubmission
"""
- # id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
+ id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission
- __mapper_args__ = {"polymorphic_identity": "Bacterial Culture", "polymorphic_load": "inline"}
+ __mapper_args__ = dict(polymorphic_identity="Bacterial Culture",
+ polymorphic_load="inline",
+ inherit_condition=(id == BasicSubmission.id))
def to_dict(self, full_data:bool=False) -> dict:
"""
@@ -804,6 +868,10 @@ class BacterialCulture(BasicSubmission):
output['controls'] = [item.to_sub_dict() for item in self.controls]
return output
+ @classmethod
+ def get_abbreviation(cls):
+ return "BC"
+
@classmethod
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
"""
@@ -853,7 +921,7 @@ class BacterialCulture(BasicSubmission):
Extends parent
"""
from backend.validators import RSLNamer
- data['abbreviation'] = "BC"
+ data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None) -> str:
# """
@@ -932,10 +1000,12 @@ class Wastewater(BasicSubmission):
"""
derivative submission type from BasicSubmission
"""
- # id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
+ id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
ext_technician = Column(String(64))
pcr_technician = Column(String(64))
- __mapper_args__ = {"polymorphic_identity": "Wastewater", "polymorphic_load": "inline"}
+ __mapper_args__ = __mapper_args__ = dict(polymorphic_identity="Wastewater",
+ polymorphic_load="inline",
+ inherit_condition=(id == BasicSubmission.id))
def to_dict(self, full_data:bool=False) -> dict:
"""
@@ -952,6 +1022,10 @@ class Wastewater(BasicSubmission):
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
return output
+ @classmethod
+ def get_abbreviation(cls):
+ return "WW"
+
@classmethod
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
"""
@@ -1012,7 +1086,7 @@ class Wastewater(BasicSubmission):
Extends parent
"""
from backend.validators import RSLNamer
- data['abbreviation'] = "WW"
+ data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None):
# if "submitted_date" in data.keys():
@@ -1066,13 +1140,21 @@ class Wastewater(BasicSubmission):
# return "(?PRSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)"
return "(?PRSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
+ @classmethod
+ def adjust_autofill_samples(cls, samples: List[Any]) -> List[Any]:
+ samples = super().adjust_autofill_samples(samples)
+ return [item for item in samples if not item.submitter_id.startswith("EN")]
+
class WastewaterArtic(BasicSubmission):
"""
derivative submission type for artic wastewater
"""
- # id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
- __mapper_args__ = {"polymorphic_identity": "Wastewater Artic", "polymorphic_load": "inline"}
+ id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
+ __mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
+ polymorphic_load="inline",
+ inherit_condition=(id == BasicSubmission.id))
artic_technician = Column(String(64))
+ dna_core_submission_number = Column(String(64))
def calculate_base_cost(self):
"""
@@ -1093,6 +1175,10 @@ class WastewaterArtic(BasicSubmission):
except Exception as e:
logger.error(f"Calculation error: {e}")
+ @classmethod
+ def get_abbreviation(cls):
+ return "AR"
+
@classmethod
def parse_samples(cls, input_dict: dict) -> dict:
"""
@@ -1109,15 +1195,45 @@ class WastewaterArtic(BasicSubmission):
# Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
# at the end, this has to be done here. No moving to sqlalchemy object :(
input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip()
+ if "ENC" in input_dict['submitter_id']:
+ input_dict['submitter_id'] = cls.en_adapter(input_str=input_dict['submitter_id'])
return input_dict
+ @classmethod
+ def en_adapter(cls, input_str) -> str:
+ processed = re.sub(r"[A-Z]", "", input_str)
+ try:
+ en_num = re.search(r"\-\d{1}$", processed).group()
+ processed = processed.replace(en_num, "", -1)
+ except AttributeError:
+ en_num = "1"
+ en_num = en_num.strip("-")
+ logger.debug(f"Processed after en-num: {processed}")
+ try:
+ plate_num = re.search(r"\-\d{1}$", processed).group()
+ processed = processed.replace(plate_num, "", -1)
+ except AttributeError:
+ plate_num = "1"
+ plate_num = plate_num.strip("-")
+ logger.debug(f"Processed after plate-num: {processed}")
+ day = re.search(r"\d{2}$", processed).group()
+ processed = processed.replace(day, "", -1)
+ logger.debug(f"Processed after day: {processed}")
+ month = re.search(r"\d{2}$", processed).group()
+ processed = processed.replace(month, "", -1)
+ processed = processed.replace("--", "")
+ logger.debug(f"Processed after month: {processed}")
+ year = re.search(r'^(?:\d{2})?\d{2}', processed).group()
+ year = f"20{year}"
+ return f"EN{year}{month}{day}-{en_num}"
+
@classmethod
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
"""
Extends parent
"""
from backend.validators import RSLNamer
- data['abbreviation'] = "AR"
+ data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None):
# today = datetime.now()
@@ -1240,6 +1356,36 @@ class WastewaterArtic(BasicSubmission):
worksheet.cell(row=iii, column=jjj, value=value)
return input_excel
+ def adjust_to_dict_samples(self, backup:bool=False):
+ logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
+ if backup:
+ output = []
+ for assoc in self.submission_sample_associations:
+ dicto = assoc.to_sub_dict()
+ old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
+ try:
+ dicto['plate_name'] = old_sub.rsl_plate_num
+ except AttributeError:
+ dicto['plate_name'] = ""
+ old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
+ dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
+ output.append(dicto)
+ else:
+ output = super().adjust_to_dict_samples(backup=False)
+ return output
+
+ def custom_context_events(self):
+ events = super().custom_context_events()
+ events['Gel Box'] = self.gel_box
+ return events
+
+ def gel_box(self, obj):
+ from frontend.widgets.gel_checker import GelBox
+ dlg = GelBox(parent=obj)
+ if dlg.exec():
+ output = dlg.parse_form()
+ print(output)
+
# Sample Classes
class BasicSample(BaseClass):
@@ -1247,7 +1393,7 @@ class BasicSample(BaseClass):
Base of basic sample which polymorphs into BCSample and WWSample
"""
- __tablename__ = "_samples"
+ # __tablename__ = "_samples"
id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
@@ -1295,6 +1441,18 @@ class BasicSample(BaseClass):
def __repr__(self) -> str:
return f"<{self.sample_type.replace('_', ' ').title().replace(' ', '')}({self.submitter_id})>"
+ def to_sub_dict(self, submission_rsl:str) -> dict:
+ """
+ gui friendly dictionary, extends parent method.
+
+ Returns:
+ dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
+ """
+ sample = {}
+ sample['submitter_id'] = self.submitter_id
+ sample['sample_type'] = self.sample_type
+ return sample
+
def set_attribute(self, name:str, value):
"""
Custom attribute setter
@@ -1307,59 +1465,6 @@ class BasicSample(BaseClass):
setattr(self, name, value)
except AttributeError:
logger.error(f"Attribute {name} not found")
-
- def to_sub_dict(self, submission_rsl:str|BasicSubmission) -> dict:
- """
- Returns a dictionary of locations.
-
- Args:
- submission_rsl (str): Submission RSL number.
-
- Returns:
- dict: 'well' and sample submitter_id as 'name'
- """
- match submission_rsl:
- case BasicSubmission():
- assoc = [item for item in self.sample_submission_associations if item.submission==submission_rsl][0]
- case str():
- assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
- sample = {}
- try:
- sample['well'] = f"{row_map[assoc.row]}{assoc.column}"
- except KeyError as e:
- logger.error(f"Unable to find row {assoc.row} in row_map.")
- sample['well'] = None
- sample['name'] = self.submitter_id
- sample['submitter_id'] = self.submitter_id
- sample['sample_type'] = self.sample_type
- if isinstance(assoc.row, list):
- sample['row'] = assoc.row[0]
- else:
- sample['row'] = assoc.row
- if isinstance(assoc.column, list):
- sample['column'] = assoc.column[0]
- else:
- sample['column'] = assoc.column
- return sample
-
- def to_hitpick(self, submission_rsl:str|None=None) -> dict|None:
- """
- Outputs a dictionary usable for html plate maps.
-
- Returns:
- dict: dictionary of sample id, row and column in elution plate
- """
- # Since there is no PCR, negliable result is necessary.
- # assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
- fields = self.to_sub_dict(submission_rsl=submission_rsl)
- env = jinja_template_loading()
- template = env.get_template("tooltip.html")
- tooltip_text = template.render(fields=fields)
- # tooltip_text = f"""
- # Sample name: {self.submitter_id}
- # 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:
{% for item in sub['equipment'] %}
- {{ item['role'] }}: {{ item['name'] }}({{ item['asset_number'] }}): {{ item['process']|replace('\n\t', '
') }}
+ {{ item['role'] }}: {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '
') }}
{% endfor %}
{% endif %}
{% if sub['samples'] %}
@@ -97,9 +97,9 @@
{% endfor %}
{% endfor %}
{% endif %}
- {% if sub['comments'] %}
+ {% if sub['comment'] %}
Comments:
- {% for entry in sub['comments'] %}
+
{% for entry in sub['comment'] %}
{{ entry['name'] }}:
{{ entry['text'] }}
- {{ entry['time'] }}
{% endfor %}
{% endif %}
diff --git a/src/submissions/tools.py b/src/submissions/tools.py
index 818369b..9a3b6ef 100644
--- a/src/submissions/tools.py
+++ b/src/submissions/tools.py
@@ -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})"