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})"