Added Postgres support.
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
## 202407.04
|
||||
|
||||
- Added support for postgresql databases (auto backup not functional).
|
||||
|
||||
## 202407.02
|
||||
|
||||
- HTML template for 'About'.
|
||||
- More flexible custom parsers/writers due to custom info items.
|
||||
- Vastly increased portability of the reporting functions.
|
||||
|
||||
## 202407.01
|
||||
|
||||
|
||||
16
README.md
16
README.md
@@ -1,7 +1,8 @@
|
||||
## Startup:
|
||||
1. Open the app using the shortcut in the Submissions folder. For example: L:\\Robotics Laboratory Support\\Submissions\\submissions_v122b.exe - Shortcut.lnk (Version may have changed).
|
||||
1. Open the app using the shortcut in the Submissions folder: L:\Robotics Laboratory Support\Submissions\Submissions App.lnk.
|
||||
1. Ignore the large black window of fast scrolling text, it is there for debugging purposes.
|
||||
2. The 'Submissions' tab should be open by default.
|
||||
3. Default settings (config.yml) will be copied to C:\Users\{YOUR USERNAME}\AppData\Local\submissions\config
|
||||
|
||||
## Logging in New Run:
|
||||
*should fit 90% of usage cases*
|
||||
@@ -109,3 +110,16 @@ This is meant to import .xslx files created from the Design & Analysis Software
|
||||
1. Click "Monthly" -> "Link PCR Logs".
|
||||
2. Chose the .csv file taken from the PCR table runlogs folder.
|
||||
|
||||
## SETUP:
|
||||
|
||||
## Download:
|
||||
|
||||
1. Clone or download from github.
|
||||
2. Enter the downloaded folder.
|
||||
|
||||
## Database:
|
||||
|
||||
1. Copy 'alembic_default.ini' to 'alembic.ini' in the same folder.
|
||||
2. Open 'alembic.ini' and edit 'sqlalchemy.url' to the desired path of the database.
|
||||
1. The path by default is sqlite based. Postgresql support is available.
|
||||
2. Postgres path
|
||||
4
TODO.md
4
TODO.md
@@ -2,7 +2,7 @@
|
||||
- The hardest part of this is going to be the sample parsing. I'm onto using the cell formulas in the plate map to suss out the location in the lookup table, but it could get a little recursive up in here.
|
||||
- [ ] Create a default info return function.
|
||||
- [x] Parse comment from excel sheet.
|
||||
- [ ] Make reporting better.
|
||||
- [x] Make reporting better.
|
||||
- [x] Build master query method?
|
||||
- Obviously there will need to be extensions, but I feel the attr method I have in Submissions could work.
|
||||
- [x] Fix Artic RSLNamer
|
||||
@@ -23,7 +23,7 @@
|
||||
- [x] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass
|
||||
- [x] Fix updating of Extraction Kit in submission form widget.
|
||||
- [x] Fix cropping of gel image.
|
||||
- [ ] Create Tips ... *sigh*.
|
||||
- [x] Create Tips ... *sigh*.
|
||||
- [x] Create platemap image from html for export to pdf.
|
||||
- [x] Move plate map maker to submission.
|
||||
- [x] Finish Equipment Parser (add in regex to id asset_number)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""First Commit
|
||||
"""rebuild 20240723
|
||||
|
||||
Revision ID: e3f6770ef515
|
||||
Revision ID: 0746f7e2c10e
|
||||
Revises:
|
||||
Create Date: 2024-01-22 14:01:02.958292
|
||||
Create Date: 2024-07-23 14:08:37.436400
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
@@ -10,7 +10,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e3f6770ef515'
|
||||
revision = '0746f7e2c10e'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
@@ -25,6 +25,12 @@ def upgrade() -> None:
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('submitter_id')
|
||||
)
|
||||
op.create_table('_configitem',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('key', sa.String(length=32), nullable=True),
|
||||
sa.Column('value', sa.JSON(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_contact',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
@@ -66,9 +72,10 @@ def upgrade() -> None:
|
||||
op.create_table('_process',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('_reagenttype',
|
||||
op.create_table('_reagentrole',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.Column('eol_ext', sa.Interval(), nullable=True),
|
||||
@@ -78,10 +85,17 @@ def upgrade() -> None:
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=True),
|
||||
sa.Column('info_map', sa.JSON(), nullable=True),
|
||||
sa.Column('template_file', sa.BLOB(), nullable=True),
|
||||
sa.Column('defaults', sa.JSON(), nullable=True),
|
||||
sa.Column('template_file', sa.LargeBinary(), nullable=True),
|
||||
sa.Column('sample_map', sa.JSON(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('_tiprole',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_bacterialculturesample',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('organism', sa.String(length=64), nullable=True),
|
||||
@@ -117,17 +131,17 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(['equipmentrole_id'], ['_equipmentrole.id'], ),
|
||||
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
|
||||
)
|
||||
op.create_table('_kittypereagenttypeassociation',
|
||||
sa.Column('reagent_types_id', sa.INTEGER(), nullable=False),
|
||||
op.create_table('_kittypereagentroleassociation',
|
||||
sa.Column('reagent_roles_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('kits_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_type_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('uses', sa.JSON(), nullable=True),
|
||||
sa.Column('required', sa.INTEGER(), nullable=True),
|
||||
sa.Column('last_used', sa.String(length=32), nullable=True),
|
||||
sa.ForeignKeyConstraint(['kits_id'], ['_kittype.id'], ),
|
||||
sa.ForeignKeyConstraint(['reagent_types_id'], ['_reagenttype.id'], ),
|
||||
sa.ForeignKeyConstraint(['reagent_roles_id'], ['_reagentrole.id'], ),
|
||||
sa.ForeignKeyConstraint(['submission_type_id'], ['_submissiontype.id'], ),
|
||||
sa.PrimaryKeyConstraint('reagent_types_id', 'kits_id', 'submission_type_id')
|
||||
sa.PrimaryKeyConstraint('reagent_roles_id', 'kits_id', 'submission_type_id')
|
||||
)
|
||||
op.create_table('_kittypes_processes',
|
||||
sa.Column('process_id', sa.INTEGER(), nullable=True),
|
||||
@@ -141,13 +155,19 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(['contact_id'], ['_contact.id'], ),
|
||||
sa.ForeignKeyConstraint(['org_id'], ['_organization.id'], )
|
||||
)
|
||||
op.create_table('_process_tiprole',
|
||||
sa.Column('process_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], ),
|
||||
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], )
|
||||
)
|
||||
op.create_table('_reagent',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('type_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('role_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.Column('lot', sa.String(length=64), nullable=True),
|
||||
sa.Column('expiry', sa.TIMESTAMP(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['type_id'], ['_reagenttype.id'], name='fk_reagent_type_id', ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['_reagentrole.id'], name='fk_reagent_role_id', ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_submissiontypeequipmentroleassociation',
|
||||
@@ -175,6 +195,23 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_submissiontype.id'], ),
|
||||
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
|
||||
)
|
||||
op.create_table('_submissiontypetiproleassociation',
|
||||
sa.Column('tiprole_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(['submissiontype_id'], ['_submissiontype.id'], ),
|
||||
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
|
||||
sa.PrimaryKeyConstraint('tiprole_id', 'submissiontype_id')
|
||||
)
|
||||
op.create_table('_tips',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('role_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.Column('lot', sa.String(length=64), nullable=True),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['_tiprole.id'], name='fk_tip_role_id', ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_wastewatersample',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('ww_processing_num', sa.String(length=64), nullable=True),
|
||||
@@ -197,13 +234,15 @@ def upgrade() -> None:
|
||||
sa.Column('extraction_kit_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('submission_type_name', sa.String(), nullable=True),
|
||||
sa.Column('technician', sa.String(length=64), nullable=True),
|
||||
sa.Column('reagents_id', sa.String(), nullable=True),
|
||||
sa.Column('reagents_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('extraction_info', sa.JSON(), nullable=True),
|
||||
sa.Column('pcr_info', sa.JSON(), nullable=True),
|
||||
sa.Column('run_cost', sa.FLOAT(precision=2), nullable=True),
|
||||
sa.Column('uploaded_by', sa.String(length=32), nullable=True),
|
||||
sa.Column('signed_by', sa.String(length=32), nullable=True),
|
||||
sa.Column('comment', sa.JSON(), nullable=True),
|
||||
sa.Column('submission_category', sa.String(length=64), nullable=True),
|
||||
sa.Column('cost_centre', sa.String(length=64), nullable=True),
|
||||
sa.Column('contact_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['contact_id'], ['_contact.id'], name='fk_BS_contact_id', ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['extraction_kit_id'], ['_kittype.id'], name='fk_BS_extkit_id', ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['reagents_id'], ['_reagent.id'], name='fk_BS_reagents_id', ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['submission_type_name'], ['_submissiontype.name'], name='fk_BS_subtype_name', ondelete='SET NULL'),
|
||||
@@ -212,11 +251,23 @@ def upgrade() -> None:
|
||||
sa.UniqueConstraint('rsl_plate_num'),
|
||||
sa.UniqueConstraint('submitter_plate_num')
|
||||
)
|
||||
op.create_table('_reagenttypes_reagents',
|
||||
op.create_table('_equipment_tips',
|
||||
sa.Column('equipment_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tips_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
|
||||
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
|
||||
)
|
||||
op.create_table('_reagentroles_reagents',
|
||||
sa.Column('reagent_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('reagenttype_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('reagentrole_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['reagent_id'], ['_reagent.id'], ),
|
||||
sa.ForeignKeyConstraint(['reagenttype_id'], ['_reagenttype.id'], )
|
||||
sa.ForeignKeyConstraint(['reagentrole_id'], ['_reagentrole.id'], )
|
||||
)
|
||||
op.create_table('_tiproles_tips',
|
||||
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tips_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
|
||||
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
|
||||
)
|
||||
op.create_table('_bacterialculture',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
@@ -225,7 +276,7 @@ def upgrade() -> None:
|
||||
)
|
||||
op.create_table('_control',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('parent_id', sa.String(), nullable=True),
|
||||
sa.Column('parent_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('submitted_date', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('contains', sa.JSON(), nullable=True),
|
||||
@@ -264,19 +315,31 @@ def upgrade() -> None:
|
||||
sa.PrimaryKeyConstraint('reagent_id', 'submission_id')
|
||||
)
|
||||
op.create_table('_submissionsampleassociation',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('sample_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('row', sa.INTEGER(), nullable=False),
|
||||
sa.Column('column', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_rank', sa.INTEGER(), nullable=False),
|
||||
sa.Column('base_sub_type', sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
|
||||
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
sa.PrimaryKeyConstraint('submission_id', 'row', 'column')
|
||||
sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
|
||||
sa.UniqueConstraint('id')
|
||||
)
|
||||
op.create_table('_submissiontipsassociation',
|
||||
sa.Column('tip_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('role_name', sa.String(length=32), nullable=False),
|
||||
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
sa.ForeignKeyConstraint(['tip_id'], ['_tips.id'], ),
|
||||
sa.PrimaryKeyConstraint('tip_id', 'submission_id', 'role_name')
|
||||
)
|
||||
op.create_table('_wastewater',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('ext_technician', sa.String(length=64), nullable=True),
|
||||
sa.Column('pcr_technician', sa.String(length=64), nullable=True),
|
||||
sa.Column('pcr_info', sa.JSON(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['_basicsubmission.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
@@ -284,20 +347,36 @@ def upgrade() -> None:
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('artic_technician', sa.String(length=64), nullable=True),
|
||||
sa.Column('dna_core_submission_number', sa.String(length=64), nullable=True),
|
||||
sa.Column('pcr_info', sa.JSON(), nullable=True),
|
||||
sa.Column('gel_image', sa.String(length=64), nullable=True),
|
||||
sa.Column('gel_info', sa.JSON(), nullable=True),
|
||||
sa.Column('gel_controls', sa.JSON(), nullable=True),
|
||||
sa.Column('source_plates', sa.JSON(), nullable=True),
|
||||
sa.Column('artic_date', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('ngs_date', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('gel_date', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('gel_barcode', sa.String(length=16), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['_basicsubmission.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_wastewaterarticassociation',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('source_plate', sa.String(length=16), nullable=True),
|
||||
sa.Column('source_plate_number', sa.INTEGER(), nullable=True),
|
||||
sa.Column('source_well', sa.String(length=8), nullable=True),
|
||||
sa.Column('ct', sa.String(length=8), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['_submissionsampleassociation.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_wastewaterassociation',
|
||||
sa.Column('sample_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('ct_n1', sa.FLOAT(precision=2), nullable=True),
|
||||
sa.Column('ct_n2', sa.FLOAT(precision=2), nullable=True),
|
||||
sa.Column('n1_status', sa.String(length=32), nullable=True),
|
||||
sa.Column('n2_status', sa.String(length=32), nullable=True),
|
||||
sa.Column('pcr_results', sa.JSON(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['sample_id'], ['_submissionsampleassociation.sample_id'], ),
|
||||
sa.ForeignKeyConstraint(['submission_id'], ['_submissionsampleassociation.submission_id'], ),
|
||||
sa.PrimaryKeyConstraint('sample_id', 'submission_id')
|
||||
sa.ForeignKeyConstraint(['id'], ['_submissionsampleassociation.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@@ -305,30 +384,38 @@ def upgrade() -> None:
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('_wastewaterassociation')
|
||||
op.drop_table('_wastewaterarticassociation')
|
||||
op.drop_table('_wastewaterartic')
|
||||
op.drop_table('_wastewater')
|
||||
op.drop_table('_submissiontipsassociation')
|
||||
op.drop_table('_submissionsampleassociation')
|
||||
op.drop_table('_submissionreagentassociation')
|
||||
op.drop_table('_submissionequipmentassociation')
|
||||
op.drop_table('_control')
|
||||
op.drop_table('_bacterialculture')
|
||||
op.drop_table('_reagenttypes_reagents')
|
||||
op.drop_table('_tiproles_tips')
|
||||
op.drop_table('_reagentroles_reagents')
|
||||
op.drop_table('_equipment_tips')
|
||||
op.drop_table('_basicsubmission')
|
||||
op.drop_table('_wastewatersample')
|
||||
op.drop_table('_tips')
|
||||
op.drop_table('_submissiontypetiproleassociation')
|
||||
op.drop_table('_submissiontypes_processes')
|
||||
op.drop_table('_submissiontypekittypeassociation')
|
||||
op.drop_table('_submissiontypeequipmentroleassociation')
|
||||
op.drop_table('_reagent')
|
||||
op.drop_table('_process_tiprole')
|
||||
op.drop_table('_orgs_contacts')
|
||||
op.drop_table('_kittypes_processes')
|
||||
op.drop_table('_kittypereagenttypeassociation')
|
||||
op.drop_table('_kittypereagentroleassociation')
|
||||
op.drop_table('_equipmentroles_processes')
|
||||
op.drop_table('_equipmentroles_equipment')
|
||||
op.drop_table('_equipment_processes')
|
||||
op.drop_table('_discount')
|
||||
op.drop_table('_bacterialculturesample')
|
||||
op.drop_table('_tiprole')
|
||||
op.drop_table('_submissiontype')
|
||||
op.drop_table('_reagenttype')
|
||||
op.drop_table('_reagentrole')
|
||||
op.drop_table('_process')
|
||||
op.drop_table('_organization')
|
||||
op.drop_table('_kittype')
|
||||
@@ -336,5 +423,6 @@ def downgrade() -> None:
|
||||
op.drop_table('_equipment')
|
||||
op.drop_table('_controltype')
|
||||
op.drop_table('_contact')
|
||||
op.drop_table('_configitem')
|
||||
op.drop_table('_basicsample')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Made WastewaterArticAssociation
|
||||
|
||||
Revision ID: 16b20d4368e6
|
||||
Revises: d2b094cfa308
|
||||
Create Date: 2024-06-13 12:16:48.385516
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '16b20d4368e6'
|
||||
down_revision = 'd2b094cfa308'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('_wastewaterarticassociation',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('source_plate', sa.String(length=16), nullable=True),
|
||||
sa.Column('source_plate_number', sa.INTEGER(), nullable=True),
|
||||
sa.Column('source_well', sa.String(length=8), nullable=True),
|
||||
sa.Column('ct', sa.String(length=8), nullable=True),
|
||||
sa.ForeignKeyConstraint(['id'], ['_submissionsampleassociation.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
#
|
||||
# with op.batch_alter_table('_submissiontipsassociation', schema=None) as batch_op:
|
||||
# batch_op.alter_column('role_name',
|
||||
# existing_type=sa.INTEGER(),
|
||||
# type_=sa.String(length=32),
|
||||
# existing_nullable=True)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissiontipsassociation', schema=None) as batch_op:
|
||||
batch_op.alter_column('role_name',
|
||||
existing_type=sa.String(length=32),
|
||||
type_=sa.INTEGER(),
|
||||
existing_nullable=True)
|
||||
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
op.drop_table('_wastewaterarticassociation')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,51 +0,0 @@
|
||||
"""adding cost centre storage to basicsubmission
|
||||
|
||||
Revision ID: 6d2a357860ef
|
||||
Revises: e6647bd661d9
|
||||
Create Date: 2024-04-24 13:01:14.923814
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6d2a357860ef'
|
||||
down_revision = 'e6647bd661d9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# op.drop_table('_alembic_tmp__submissionsampleassociation')
|
||||
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('cost_centre', sa.String(length=64), nullable=True))
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
|
||||
batch_op.drop_column('used_cost_centre')
|
||||
|
||||
op.create_table('_alembic_tmp__submissionsampleassociation',
|
||||
sa.Column('sample_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('row', sa.INTEGER(), nullable=False),
|
||||
sa.Column('column', sa.INTEGER(), nullable=False),
|
||||
sa.Column('base_sub_type', sa.VARCHAR(), nullable=True),
|
||||
sa.Column('id', sa.INTEGER(), server_default=sa.text('1'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
|
||||
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
|
||||
sa.UniqueConstraint('id', name='ssa_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,34 +0,0 @@
|
||||
"""adding gel image, info. Again
|
||||
|
||||
Revision ID: 70426df72f80
|
||||
Revises: c4201b0ea9fe
|
||||
Create Date: 2024-01-30 08:47:22.809841
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '70426df72f80'
|
||||
down_revision = 'c4201b0ea9fe'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('gel_image', sa.String(length=64), nullable=True))
|
||||
batch_op.add_column(sa.Column('gel_info', sa.JSON(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.drop_column('gel_info')
|
||||
batch_op.drop_column('gel_image')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,38 +0,0 @@
|
||||
"""tweaking submission sample association
|
||||
|
||||
Revision ID: 70d5a751f579
|
||||
Revises: 97392dda5436
|
||||
Create Date: 2024-01-25 13:39:34.163501
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '70d5a751f579'
|
||||
down_revision = '97392dda5436'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.alter_column('id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
batch_op.create_unique_constraint("ssa_id_unique", ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.drop_constraint("ssa_id_unique", type_='unique')
|
||||
batch_op.alter_column('id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Adding fields to Artic
|
||||
|
||||
Revision ID: 861b52a2004e
|
||||
Revises: b744e8a452fd
|
||||
Create Date: 2024-06-05 13:35:19.012337
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '861b52a2004e'
|
||||
down_revision = 'b744e8a452fd'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('_alembic_tmp__basicsubmission')
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('artic_date', sa.TIMESTAMP(), nullable=True))
|
||||
batch_op.add_column(sa.Column('ngs_date', sa.TIMESTAMP(), nullable=True))
|
||||
batch_op.add_column(sa.Column('gel_date', sa.TIMESTAMP(), nullable=True))
|
||||
batch_op.add_column(sa.Column('gel_barcode', sa.String(length=16), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.drop_column('gel_barcode')
|
||||
batch_op.drop_column('gel_date')
|
||||
batch_op.drop_column('ngs_date')
|
||||
batch_op.drop_column('artic_date')
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
op.create_table('_alembic_tmp__basicsubmission',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('rsl_plate_num', sa.VARCHAR(length=32), nullable=False),
|
||||
sa.Column('submitter_plate_num', sa.VARCHAR(length=127), nullable=True),
|
||||
sa.Column('submitted_date', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('submitting_lab_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('sample_count', sa.INTEGER(), nullable=True),
|
||||
sa.Column('extraction_kit_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('submission_type_name', sa.VARCHAR(), nullable=True),
|
||||
sa.Column('technician', sa.VARCHAR(length=64), nullable=True),
|
||||
sa.Column('reagents_id', sa.VARCHAR(), nullable=True),
|
||||
sa.Column('extraction_info', sqlite.JSON(), nullable=True),
|
||||
sa.Column('run_cost', sa.FLOAT(), nullable=True),
|
||||
sa.Column('signed_by', sa.VARCHAR(length=32), nullable=True),
|
||||
sa.Column('comment', sqlite.JSON(), nullable=True),
|
||||
sa.Column('submission_category', sa.VARCHAR(length=64), nullable=True),
|
||||
sa.Column('cost_centre', sa.VARCHAR(length=64), nullable=True),
|
||||
sa.Column('contact_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['contact_id'], ['_contact.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['extraction_kit_id'], ['_kittype.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['reagents_id'], ['_reagent.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['submission_type_name'], ['_submissiontype.name'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['submitting_lab_id'], ['_organization.id'], ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('rsl_plate_num'),
|
||||
sa.UniqueConstraint('submitter_plate_num')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,34 +0,0 @@
|
||||
"""Adding ranking to SubmissionSampleAssociation
|
||||
|
||||
Revision ID: 874af342c82c
|
||||
Revises: a04c25fd9138
|
||||
Create Date: 2024-05-03 15:08:20.194275
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '874af342c82c'
|
||||
down_revision = 'a04c25fd9138'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('submission_rank', sa.INTEGER(), nullable=False, default=1))
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
batch_op.drop_column('submission_rank')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Update to submissionsampleassociation
|
||||
|
||||
Revision ID: 97392dda5436
|
||||
Revises: e3f6770ef515
|
||||
Create Date: 2024-01-25 09:10:04.384194
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '97392dda5436'
|
||||
down_revision = 'e3f6770ef515'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=True))
|
||||
batch_op.create_unique_constraint("submissionsampleassociation_id", ['id'])
|
||||
|
||||
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('id', sa.INTEGER(), nullable=False))
|
||||
# batch_op.drop_constraint("sample_id", type_='foreignkey')
|
||||
# batch_op.drop_constraint("submission_id", type_='foreignkey')
|
||||
batch_op.create_foreign_key("fk_subsampassoc_id", '_submissionsampleassociation', ['id'], ['id'])
|
||||
# batch_op.drop_column('sample_id')
|
||||
# batch_op.drop_column('submission_id')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('submission_id', sa.INTEGER(), nullable=False))
|
||||
batch_op.add_column(sa.Column('sample_id', sa.INTEGER(), nullable=False))
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.create_foreign_key(None, '_submissionsampleassociation', ['submission_id'], ['submission_id'])
|
||||
batch_op.create_foreign_key(None, '_submissionsampleassociation', ['sample_id'], ['sample_id'])
|
||||
batch_op.drop_column('id')
|
||||
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='unique')
|
||||
batch_op.drop_column('id')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,38 +0,0 @@
|
||||
"""splitting off sample info in SubmissionType
|
||||
|
||||
Revision ID: a04c25fd9138
|
||||
Revises: 6d2a357860ef
|
||||
Create Date: 2024-05-01 09:11:44.957532
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a04c25fd9138'
|
||||
down_revision = '6d2a357860ef'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('sample_map', sa.JSON(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
|
||||
batch_op.drop_column('sample_map')
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Attaching contact to submission
|
||||
|
||||
Revision ID: b744e8a452fd
|
||||
Revises: f829a8ab292f
|
||||
Create Date: 2024-06-04 14:21:38.163431
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b744e8a452fd'
|
||||
down_revision = 'f829a8ab292f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('contact_id', sa.INTEGER(), nullable=True))
|
||||
batch_op.create_foreign_key('fk_BS_contact_id', '_contact', ['contact_id'], ['id'], ondelete='SET NULL')
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('fk_BS_contact_id', type_='foreignkey')
|
||||
batch_op.drop_column('contact_id')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,42 +0,0 @@
|
||||
"""adding gel image, info
|
||||
|
||||
Revision ID: c4201b0ea9fe
|
||||
Revises: 70d5a751f579
|
||||
Create Date: 2024-01-30 08:42:03.928933
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c4201b0ea9fe'
|
||||
down_revision = '70d5a751f579'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint("unique_ssa_id", ['id'])
|
||||
|
||||
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
|
||||
batch_op.alter_column('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('_wastewaterassociation', schema=None) as batch_op:
|
||||
batch_op.alter_column('id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
|
||||
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
batch_op.drop_constraint("unique_ssa_id", type_='unique')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,95 +0,0 @@
|
||||
"""Adding tips
|
||||
|
||||
Revision ID: d2b094cfa308
|
||||
Revises: 861b52a2004e
|
||||
Create Date: 2024-06-11 13:16:57.319769
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd2b094cfa308'
|
||||
down_revision = '861b52a2004e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('_tiprole',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_process_tiprole',
|
||||
sa.Column('process_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], ),
|
||||
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], )
|
||||
)
|
||||
op.create_table('_submissiontypetiproleassociation',
|
||||
sa.Column('tiprole_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(['submissiontype_id'], ['_submissiontype.id'], ),
|
||||
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
|
||||
sa.PrimaryKeyConstraint('tiprole_id', 'submissiontype_id')
|
||||
)
|
||||
op.create_table('_tips',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('role_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.Column('lot', sa.String(length=64), nullable=True),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['_tiprole.id'], name='fk_tip_role_id', ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('_equipment_tips',
|
||||
sa.Column('equipment_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tips_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
|
||||
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
|
||||
)
|
||||
op.create_table('_submissiontipsassociation',
|
||||
sa.Column('tip_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('role_name', sa.String(), nullable=False),
|
||||
# sa.ForeignKeyConstraint(['role_name'], ['_tiprole.name'], ),
|
||||
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
sa.ForeignKeyConstraint(['tip_id'], ['_tips.id'], ),
|
||||
sa.PrimaryKeyConstraint('tip_id', 'submission_id', 'role_name')
|
||||
)
|
||||
op.create_table('_tiproles_tips',
|
||||
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tips_id', sa.INTEGER(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
|
||||
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
|
||||
)
|
||||
# op.create_table('_submissions_tips',
|
||||
# sa.Column('submission_id', sa.INTEGER(), nullable=True),
|
||||
# sa.Column('tips_id', sa.INTEGER(), nullable=True),
|
||||
# sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
# sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
|
||||
# )
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint("unique_ssa_id", ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
op.drop_table('_submissions_tips')
|
||||
op.drop_table('_tiproles_tips')
|
||||
op.drop_table('_submissiontipassociation')
|
||||
op.drop_table('_equipment_tips')
|
||||
op.drop_table('_tips')
|
||||
op.drop_table('_submissiontypetiproleassociation')
|
||||
op.drop_table('_process_tiprole')
|
||||
op.drop_table('_tiprole')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,33 +0,0 @@
|
||||
"""adding default info to submissiontype
|
||||
|
||||
Revision ID: e6647bd661d9
|
||||
Revises: f18487b41f45
|
||||
Create Date: 2024-04-22 12:02:21.512781
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e6647bd661d9'
|
||||
down_revision = 'f18487b41f45'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('defaults', sa.JSON(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
|
||||
batch_op.drop_column('defaults')
|
||||
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,51 +0,0 @@
|
||||
"""adding source plates to Artic submission...again
|
||||
|
||||
Revision ID: f18487b41f45
|
||||
Revises: fabf697c721d
|
||||
Create Date: 2024-04-17 10:42:30.508213
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f18487b41f45'
|
||||
down_revision = 'fabf697c721d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# op.drop_table('_alembic_tmp__submissionsampleassociation')
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint("ssa_id", ['id'])
|
||||
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.drop_column('source_plates')
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
# op.create_table('_alembic_tmp__submissionsampleassociation',
|
||||
# sa.Column('sample_id', sa.INTEGER(), nullable=False),
|
||||
# sa.Column('submission_id', sa.INTEGER(), nullable=False),
|
||||
# sa.Column('row', sa.INTEGER(), nullable=False),
|
||||
# sa.Column('column', sa.INTEGER(), nullable=False),
|
||||
# sa.Column('base_sub_type', sa.VARCHAR(), nullable=True),
|
||||
# sa.Column('id', sa.INTEGER(), server_default=sa.text('1'), nullable=False),
|
||||
# sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
|
||||
# sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
|
||||
# sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
|
||||
# sa.UniqueConstraint('id', name='ssa_unique')
|
||||
# )
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Adding configitems to db
|
||||
|
||||
Revision ID: f829a8ab292f
|
||||
Revises: 874af342c82c
|
||||
Create Date: 2024-05-15 14:03:11.767480
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f829a8ab292f'
|
||||
down_revision = '874af342c82c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('_configitem',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('key', sa.String(), nullable=True),
|
||||
sa.Column('value', sa.JSON(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint(None, ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint(None, type_='unique')
|
||||
|
||||
op.drop_table('_configitem')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,48 +0,0 @@
|
||||
"""adding source plates to Artic submission
|
||||
|
||||
Revision ID: fabf697c721d
|
||||
Revises: 70426df72f80
|
||||
Create Date: 2024-03-06 11:01:34.794411
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fabf697c721d'
|
||||
down_revision = '70426df72f80'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.create_unique_constraint("ssa_unique", ['id'])
|
||||
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
|
||||
batch_op.alter_column('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('_wastewaterassociation', schema=None) as batch_op:
|
||||
batch_op.alter_column('id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
|
||||
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||
batch_op.drop_column('source_plates')
|
||||
|
||||
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||
# batch_op.drop_constraint("ssa_unique", type_='unique')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
104
alembic_default.ini
Normal file
104
alembic_default.ini
Normal file
@@ -0,0 +1,104 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = ./alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to ./alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:./alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -17,7 +17,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
connection_record (_type_): _description_
|
||||
"""
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
# cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close()
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@ Contains all models for sqlalchemy
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, logging
|
||||
from sqlalchemy import Column, INTEGER, String, JSON
|
||||
from sqlalchemy import Column, INTEGER, String, JSON, inspect
|
||||
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
from typing import Any, List
|
||||
from pathlib import Path
|
||||
from tools import report_result
|
||||
|
||||
# Load testing environment
|
||||
if 'pytest' in sys.modules:
|
||||
@@ -90,7 +91,7 @@ class BaseClass(Base):
|
||||
|
||||
Returns:
|
||||
dict | list | str: Output of key:value dict or single (list, str) desired variable
|
||||
"""
|
||||
"""
|
||||
dicto = dict(singles=['id'])
|
||||
output = {}
|
||||
for k, v in dicto.items():
|
||||
@@ -110,7 +111,7 @@ class BaseClass(Base):
|
||||
|
||||
Returns:
|
||||
Any | List[Any]: Result of query execution.
|
||||
"""
|
||||
"""
|
||||
return cls.execute_query(**kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -152,38 +153,43 @@ class BaseClass(Base):
|
||||
case _:
|
||||
return query.limit(limit).all()
|
||||
|
||||
@report_result
|
||||
def save(self):
|
||||
"""
|
||||
Add the object to the database and commit
|
||||
"""
|
||||
# logger.debug(f"Saving object: {pformat(self.__dict__)}")
|
||||
report = Report()
|
||||
try:
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
# self.__database_session__.merge(self)
|
||||
except Exception as e:
|
||||
logger.critical(f"Problem saving object: {e}")
|
||||
self.__database_session__.rollback()
|
||||
report.add_result(Result(msg=f"Problem saving object {e}", status="Critical"))
|
||||
return report
|
||||
|
||||
|
||||
class ConfigItem(BaseClass):
|
||||
"""
|
||||
Key:JSON objects to store config settings in database.
|
||||
"""
|
||||
"""
|
||||
id = Column(INTEGER, primary_key=True)
|
||||
key = Column(String(32)) #: Name of the configuration item.
|
||||
value = Column(JSON) #: Value associated with the config item.
|
||||
key = Column(String(32)) #: Name of the configuration item.
|
||||
value = Column(JSON) #: Value associated with the config item.
|
||||
|
||||
def __repr__(self):
|
||||
return f"ConfigItem({self.key} : {self.value})"
|
||||
|
||||
@classmethod
|
||||
def get_config_items(cls, *args) -> ConfigItem|List[ConfigItem]:
|
||||
def get_config_items(cls, *args) -> ConfigItem | List[ConfigItem]:
|
||||
"""
|
||||
Get desired config items from database
|
||||
|
||||
Returns:
|
||||
ConfigItem|List[ConfigItem]: Config item(s)
|
||||
"""
|
||||
"""
|
||||
config_items = cls.__database_session__.query(cls).all()
|
||||
config_items = [item for item in config_items if item.key in args]
|
||||
if len(args) == 1:
|
||||
@@ -196,4 +202,5 @@ from .controls import *
|
||||
from .organizations import *
|
||||
from .kits import *
|
||||
from .submissions import *
|
||||
|
||||
BasicSubmission.reagents.creator = lambda reg: SubmissionReagentAssociation(reagent=reg)
|
||||
|
||||
@@ -1532,7 +1532,7 @@ class Process(BaseClass):
|
||||
query = cls.__database_session__.query(cls)
|
||||
match name:
|
||||
case str():
|
||||
logger.debug(f"Lookup Process with name str {name}")
|
||||
# logger.debug(f"Lookup Process with name str {name}")
|
||||
query = query.filter(cls.name == name)
|
||||
limit = 1
|
||||
case _:
|
||||
|
||||
@@ -23,7 +23,7 @@ import pandas as pd
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from openpyxl.drawing.image import Image as OpenpyxlImage
|
||||
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr
|
||||
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report
|
||||
from datetime import datetime, date
|
||||
from typing import List, Any, Tuple, Literal
|
||||
from dateutil.parser import parse
|
||||
@@ -691,7 +691,8 @@ class BasicSubmission(BaseClass):
|
||||
|
||||
Args:
|
||||
input_dict (dict): Input sample dictionary
|
||||
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
|
||||
xl (Workbook): original xl workbook, used for child classes mostly
|
||||
custom_fields: Dictionary of locations, ranges, etc to be used by this function
|
||||
|
||||
Returns:
|
||||
dict: Updated sample dictionary
|
||||
@@ -739,6 +740,7 @@ class BasicSubmission(BaseClass):
|
||||
input_excel (Workbook): initial workbook.
|
||||
info (dict | None, optional): dictionary of additional info. Defaults to None.
|
||||
backup (bool, optional): Whether this is part of a backup operation. Defaults to False.
|
||||
custom_fields: Dictionary of locations, ranges, etc to be used by this function
|
||||
|
||||
Returns:
|
||||
Workbook: Updated workbook
|
||||
@@ -1046,14 +1048,16 @@ class BasicSubmission(BaseClass):
|
||||
"""
|
||||
code = 0
|
||||
msg = ""
|
||||
report = Report()
|
||||
disallowed = ["id"]
|
||||
if kwargs == {}:
|
||||
raise ValueError("Need to narrow down query or the first available instance will be returned.")
|
||||
for key in kwargs.keys():
|
||||
if key in disallowed:
|
||||
raise ValueError(
|
||||
f"{key} is not allowed as a query argument as it could lead to creation of duplicate objects. Use .query() instead.")
|
||||
instance = cls.query(submission_type=submission_type, limit=1, **kwargs)
|
||||
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
|
||||
# for key in kwargs.keys():
|
||||
# if key in disallowed:
|
||||
# raise ValueError(
|
||||
# f"{key} is not allowed as a query argument as it could lead to creation of duplicate objects. Use .query() instead.")
|
||||
instance = cls.query(submission_type=submission_type, limit=1, **sanitized_kwargs)
|
||||
# logger.debug(f"Retrieved instance: {instance}")
|
||||
if instance is None:
|
||||
used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submission_type)
|
||||
@@ -1070,7 +1074,8 @@ class BasicSubmission(BaseClass):
|
||||
else:
|
||||
code = 1
|
||||
msg = "This submission already exists.\nWould you like to overwrite?"
|
||||
return instance, code, msg
|
||||
report.add_result(Result(msg=msg, code=code))
|
||||
return instance, report
|
||||
|
||||
# Custom context events for the ui
|
||||
|
||||
@@ -1135,7 +1140,7 @@ class BasicSubmission(BaseClass):
|
||||
# logger.debug(widg)
|
||||
widg.setParent(None)
|
||||
pyd = self.to_pydantic(backup=True)
|
||||
form = pyd.to_form(parent=obj)
|
||||
form = pyd.to_form(parent=obj, disable=['rsl_plate_num'])
|
||||
obj.app.table_widget.formwidget.layout().addWidget(form)
|
||||
|
||||
def add_comment(self, obj):
|
||||
@@ -1352,13 +1357,29 @@ class Wastewater(BasicSubmission):
|
||||
|
||||
Args:
|
||||
input_dict (dict): Input sample dictionary
|
||||
xl (Workbook): xl (Workbook): original xl workbook, used for child classes mostly.
|
||||
custom_fields: Dictionary of locations, ranges, etc to be used by this function
|
||||
|
||||
Returns:
|
||||
dict: Updated sample dictionary
|
||||
"""
|
||||
input_dict = super().custom_info_parser(input_dict)
|
||||
logger.debug(f"Input dict: {pformat(input_dict)}")
|
||||
if xl is not None:
|
||||
input_dict['csv'] = xl["Copy to import file"]
|
||||
try:
|
||||
input_dict['csv'] = xl["Copy to import file"]
|
||||
except KeyError as e:
|
||||
logger.error(e)
|
||||
try:
|
||||
match input_dict['rsl_plate_num']:
|
||||
case dict():
|
||||
input_dict['csv'] = xl[input_dict['rsl_plate_num']['value']]
|
||||
case str():
|
||||
input_dict['csv'] = xl[input_dict['rsl_plate_num']]
|
||||
case _:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling couldn't get csv due to: {e}")
|
||||
return input_dict
|
||||
|
||||
@classmethod
|
||||
@@ -1604,11 +1625,12 @@ class WastewaterArtic(BasicSubmission):
|
||||
Args:
|
||||
input_dict (dict): Input sample dictionary
|
||||
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
|
||||
custom_fields: Dictionary of locations, ranges, etc to be used by this function
|
||||
|
||||
Returns:
|
||||
dict: Updated sample dictionary
|
||||
"""
|
||||
# TODO: Clean up and move range start/stops to db somehow.
|
||||
from backend.validators import RSLNamer
|
||||
input_dict = super().custom_info_parser(input_dict)
|
||||
egel_section = custom_fields['egel_results']
|
||||
ws = xl[egel_section['sheet']]
|
||||
@@ -1621,12 +1643,11 @@ class WastewaterArtic(BasicSubmission):
|
||||
source_plates_section = custom_fields['source_plates']
|
||||
ws = xl[source_plates_section['sheet']]
|
||||
data = [dict(plate=ws.cell(row=ii, column=source_plates_section['plate_column']).value, starting_sample=ws.cell(row=ii, column=source_plates_section['starting_sample_column']).value) for ii in
|
||||
range(source_plates_section['start_row'], source_plates_section['end_row'])]
|
||||
range(source_plates_section['start_row'], source_plates_section['end_row']+1)]
|
||||
for datum in data:
|
||||
if datum['plate'] in ["None", None, ""]:
|
||||
continue
|
||||
else:
|
||||
from backend.validators import RSLNamer
|
||||
datum['plate'] = RSLNamer(filename=datum['plate'], sub_type="Wastewater").parsed_name
|
||||
input_dict['source_plates'] = data
|
||||
return input_dict
|
||||
@@ -1820,6 +1841,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
input_excel (Workbook): initial workbook.
|
||||
info (dict | None, optional): dictionary of additional info. Defaults to None.
|
||||
backup (bool, optional): Whether this is part of a backup operation. Defaults to False.
|
||||
custom_fields: Dictionary of locations, ranges, etc to be used by this function
|
||||
|
||||
Returns:
|
||||
Workbook: Updated workbook
|
||||
@@ -2798,7 +2820,7 @@ class WastewaterArticAssociation(SubmissionSampleAssociation):
|
||||
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||
"""
|
||||
id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True)
|
||||
source_plate = Column(String(16))
|
||||
source_plate = Column(String(32))
|
||||
source_plate_number = Column(INTEGER)
|
||||
source_well = Column(String(8))
|
||||
ct = Column(String(8)) #: AKA ct for N1
|
||||
|
||||
@@ -108,7 +108,7 @@ class PydReagent(BaseModel):
|
||||
|
||||
Returns:
|
||||
dict: Information dictionary
|
||||
"""
|
||||
"""
|
||||
try:
|
||||
extras = list(self.model_extra.keys())
|
||||
except AttributeError:
|
||||
@@ -161,7 +161,7 @@ class PydReagent(BaseModel):
|
||||
# reagent.reagent_submission_associations.append(assoc)
|
||||
else:
|
||||
assoc = None
|
||||
report.add_result(Result(owner = __name__, code=0, msg="New reagent created.", status="Information"))
|
||||
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information"))
|
||||
else:
|
||||
if submission is not None and reagent not in submission.reagents:
|
||||
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
|
||||
@@ -217,7 +217,7 @@ class PydSample(BaseModel, extra='allow'):
|
||||
|
||||
Returns:
|
||||
dict: Information dictionary
|
||||
"""
|
||||
"""
|
||||
fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
|
||||
return {k: getattr(self, k) for k in fields}
|
||||
|
||||
@@ -254,7 +254,8 @@ class PydSample(BaseModel, extra='allow'):
|
||||
submission=submission,
|
||||
sample=instance,
|
||||
row=row, column=column, id=aid,
|
||||
submission_rank=submission_rank, **self.model_extra)
|
||||
submission_rank=submission_rank,
|
||||
**self.model_extra)
|
||||
# logger.debug(f"Using submission_sample_association: {association}")
|
||||
try:
|
||||
# instance.sample_submission_associations.append(association)
|
||||
@@ -270,7 +271,7 @@ class PydSample(BaseModel, extra='allow'):
|
||||
|
||||
Returns:
|
||||
dict: Information dictionary
|
||||
"""
|
||||
"""
|
||||
try:
|
||||
extras = list(self.model_extra.keys())
|
||||
except AttributeError:
|
||||
@@ -281,10 +282,10 @@ class PydSample(BaseModel, extra='allow'):
|
||||
|
||||
class PydTips(BaseModel):
|
||||
name: str
|
||||
lot: str|None = Field(default=None)
|
||||
lot: str | None = Field(default=None)
|
||||
role: str
|
||||
|
||||
def to_sql(self, submission:BasicSubmission) -> SubmissionTipsAssociation:
|
||||
def to_sql(self, submission: BasicSubmission) -> SubmissionTipsAssociation:
|
||||
"""
|
||||
Con
|
||||
|
||||
@@ -293,7 +294,7 @@ class PydTips(BaseModel):
|
||||
|
||||
Returns:
|
||||
SubmissionTipsAssociation: Association between queried tips and submission
|
||||
"""
|
||||
"""
|
||||
tips = Tips.query(name=self.name, lot=self.lot, limit=1)
|
||||
assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
|
||||
return assoc
|
||||
@@ -305,7 +306,7 @@ class PydEquipment(BaseModel, extra='ignore'):
|
||||
nickname: str | None
|
||||
processes: List[str] | None
|
||||
role: str | None
|
||||
tips: List[PydTips]|None = Field(default=None)
|
||||
tips: List[PydTips] | None = Field(default=None)
|
||||
|
||||
@field_validator('processes', mode='before')
|
||||
@classmethod
|
||||
@@ -338,23 +339,19 @@ class PydEquipment(BaseModel, extra='ignore'):
|
||||
if equipment is None:
|
||||
return
|
||||
if submission is not None:
|
||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
|
||||
process = Process.query(name=self.processes[0])
|
||||
if process is None:
|
||||
logger.error(f"Found unknown process: {process}.")
|
||||
# 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
|
||||
# NOTE: Need to make sure the same association is not added to the submission
|
||||
|
||||
assoc = SubmissionEquipmentAssociation.query(equipment_id=equipment.id, submission_id=submission.id,
|
||||
role=self.role, limit=1)
|
||||
if assoc is None:
|
||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
|
||||
process = Process.query(name=self.processes[0])
|
||||
if process is None:
|
||||
logger.error(f"Found unknown process: {process}.")
|
||||
assoc.process = process
|
||||
assoc.role = self.role
|
||||
else:
|
||||
assoc = None
|
||||
else:
|
||||
assoc = None
|
||||
return equipment, assoc
|
||||
@@ -365,7 +362,7 @@ class PydEquipment(BaseModel, extra='ignore'):
|
||||
|
||||
Returns:
|
||||
dict: Information dictionary
|
||||
"""
|
||||
"""
|
||||
try:
|
||||
extras = list(self.model_extra.keys())
|
||||
except AttributeError:
|
||||
@@ -441,7 +438,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
return value.date()
|
||||
case int():
|
||||
return dict(value=datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value['value'] - 2).date(),
|
||||
missing=True)
|
||||
missing=True)
|
||||
case str():
|
||||
string = re.sub(r"(_|-)\d$", "", value['value'])
|
||||
try:
|
||||
@@ -508,7 +505,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
output = "RSL-BS-Test001"
|
||||
else:
|
||||
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type,
|
||||
data=values.data).parsed_name
|
||||
data=values.data).parsed_name
|
||||
return dict(value=output, missing=True)
|
||||
|
||||
@field_validator("technician", mode="before")
|
||||
@@ -637,14 +634,14 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
self.submission_object = BasicSubmission.find_polymorphic_subclass(
|
||||
polymorphic_identity=self.submission_type['value'])
|
||||
|
||||
def set_attribute(self, key:str, value):
|
||||
def set_attribute(self, key: str, value):
|
||||
"""
|
||||
Better handling of attribute setting.
|
||||
|
||||
Args:
|
||||
key (str): Name of field to set
|
||||
value (_type_): Value to set field to.
|
||||
"""
|
||||
"""
|
||||
self.__setattr__(name=key, value=value)
|
||||
|
||||
def handle_duplicate_samples(self):
|
||||
@@ -710,7 +707,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
missing_reagents = [reagent for reagent in self.reagents if reagent.missing]
|
||||
return missing_info, missing_reagents
|
||||
|
||||
def to_sql(self) -> Tuple[BasicSubmission, Result]:
|
||||
def to_sql(self) -> Tuple[BasicSubmission, Report]:
|
||||
"""
|
||||
Converts this instance into a backend.db.models.submissions.BasicSubmission instance
|
||||
|
||||
@@ -718,13 +715,13 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object
|
||||
"""
|
||||
# self.__dict__.update(self.model_extra)
|
||||
report = Report()
|
||||
dicto = self.improved_dict()
|
||||
instance, code, msg = BasicSubmission.query_or_create(submission_type=self.submission_type['value'],
|
||||
rsl_plate_num=self.rsl_plate_num['value'])
|
||||
result = Result(msg=msg, code=code)
|
||||
instance, result = BasicSubmission.query_or_create(submission_type=self.submission_type['value'],
|
||||
rsl_plate_num=self.rsl_plate_num['value'])
|
||||
report.add_result(result)
|
||||
self.handle_duplicate_samples()
|
||||
# logger.debug(f"Here's our list of duplicate removed samples: {self.samples}")
|
||||
# for key, value in self.__dict__.items():
|
||||
for key, value in dicto.items():
|
||||
if isinstance(value, dict):
|
||||
value = value['value']
|
||||
@@ -733,13 +730,13 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
# logger.debug(f"Setting {key} to {value}")
|
||||
match key:
|
||||
case "reagents":
|
||||
if code == 1:
|
||||
if report.results[0].code == 1:
|
||||
instance.submission_reagent_associations = []
|
||||
# logger.debug(f"Looking through {self.reagents}")
|
||||
for reagent in self.reagents:
|
||||
reagent, assoc, _ = reagent.toSQL(submission=instance)
|
||||
# logger.debug(f"Association: {assoc}")
|
||||
if assoc is not None:# and assoc not in instance.submission_reagent_associations:
|
||||
if assoc is not None: # and assoc not in instance.submission_reagent_associations:
|
||||
instance.submission_reagent_associations.append(assoc)
|
||||
# instance.reagents.append(reagent)
|
||||
case "samples":
|
||||
@@ -755,10 +752,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
if equip is None:
|
||||
continue
|
||||
equip, association = equip.toSQL(submission=instance)
|
||||
if association is not None and association not in instance.submission_equipment_associations:
|
||||
# association.save()
|
||||
# logger.debug(
|
||||
# f"Equipment association SQL object to be added to submission: {association.__dict__}")
|
||||
if association is not None:
|
||||
instance.submission_equipment_associations.append(association)
|
||||
case "tips":
|
||||
for tips in self.tips:
|
||||
@@ -817,9 +811,9 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
# except AttributeError as e:
|
||||
# logger.debug(f"Something went wrong constructing instance {self.rsl_plate_num}: {e}")
|
||||
# logger.debug(f"Constructed submissions message: {msg}")
|
||||
return instance, result
|
||||
return instance, report
|
||||
|
||||
def to_form(self, parent: QWidget):
|
||||
def to_form(self, parent: QWidget, disable:list|None=None):
|
||||
"""
|
||||
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
|
||||
|
||||
@@ -830,7 +824,8 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
SubmissionFormWidget: Submission form widget
|
||||
"""
|
||||
from frontend.widgets.submission_widget import SubmissionFormWidget
|
||||
return SubmissionFormWidget(parent=parent, submission=self)
|
||||
logger.debug(f"Disbable: {disable}")
|
||||
return SubmissionFormWidget(parent=parent, submission=self, disable=disable)
|
||||
|
||||
def to_writer(self) -> "SheetWriter":
|
||||
"""
|
||||
@@ -838,7 +833,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
|
||||
Returns:
|
||||
SheetWriter: Sheetwriter object that will perform writing.
|
||||
"""
|
||||
"""
|
||||
from backend.excel.writer import SheetWriter
|
||||
return SheetWriter(self)
|
||||
|
||||
@@ -896,8 +891,8 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
status="Warning")
|
||||
report.add_result(result)
|
||||
return output_reagents, report
|
||||
|
||||
def export_csv(self, filename:Path|str):
|
||||
|
||||
def export_csv(self, filename: Path | str):
|
||||
try:
|
||||
worksheet = self.csv
|
||||
except AttributeError:
|
||||
@@ -1024,4 +1019,3 @@ class PydEquipmentRole(BaseModel):
|
||||
"""
|
||||
from frontend.widgets.equipment_usage import RoleComboBox
|
||||
return RoleComboBox(parent=parent, role=self, used=used)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'''
|
||||
"""
|
||||
Constructs main application.
|
||||
'''
|
||||
"""
|
||||
from PyQt6.QtWidgets import (
|
||||
QTabWidget, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QScrollArea, QMainWindow,
|
||||
@@ -13,7 +13,7 @@ from markdown import markdown
|
||||
|
||||
from tools import check_if_app, Settings, Report, jinja_template_loading
|
||||
from datetime import date
|
||||
from .pop_ups import AlertPop, HTMLPop
|
||||
from .pop_ups import HTMLPop
|
||||
from .misc import LogParser
|
||||
import logging, webbrowser, sys, shutil
|
||||
from .submission_table import SubmissionsSheet
|
||||
@@ -36,7 +36,7 @@ class App(QMainWindow):
|
||||
self.report = Report()
|
||||
# NOTE: indicate version and connected database in title bar
|
||||
try:
|
||||
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}"
|
||||
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_session.get_bind().url}"
|
||||
except (AttributeError, KeyError):
|
||||
self.title = f"Submissions App"
|
||||
# NOTE: set initial app position and size
|
||||
@@ -164,27 +164,6 @@ class App(QMainWindow):
|
||||
instr = HTMLPop(html=html, title="Instructions")
|
||||
instr.exec()
|
||||
|
||||
|
||||
def result_reporter(self):
|
||||
"""
|
||||
Report any anomolous results - if any - to the user
|
||||
|
||||
Args:
|
||||
result (dict | None, optional): The result from a function. Defaults to None.
|
||||
"""
|
||||
# logger.debug(f"Running results reporter for: {self.report.results}")
|
||||
if len(self.report.results) > 0:
|
||||
# logger.debug(f"We've got some results!")
|
||||
for result in self.report.results:
|
||||
# logger.debug(f"Showing result: {result}")
|
||||
if result is not None:
|
||||
alert = result.report()
|
||||
if alert.exec():
|
||||
pass
|
||||
self.report = Report()
|
||||
else:
|
||||
self.statusBar().showMessage("Action completed sucessfully.", 5000)
|
||||
|
||||
def runSearch(self):
|
||||
dlg = LogParser(self)
|
||||
dlg.exec()
|
||||
@@ -201,12 +180,19 @@ class App(QMainWindow):
|
||||
Copies the database into the backup directory the first time it is opened every month.
|
||||
"""
|
||||
month = date.today().strftime("%Y-%m")
|
||||
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve()
|
||||
# logger.debug(f"Here is the db directory: {self.ctx.database_path}")
|
||||
# logger.debug(f"Here is the backup directory: {self.ctx.backup_path}")
|
||||
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db")
|
||||
if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__():
|
||||
logger.info("No backup found for this month, backing up database.")
|
||||
shutil.copyfile(self.ctx.database_path, current_month_bak)
|
||||
match self.ctx.database_schema:
|
||||
case "sqlite":
|
||||
current_month_bak = current_month_bak.with_suffix(".db")
|
||||
if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__():
|
||||
logger.info("No backup found for this month, backing up database.")
|
||||
shutil.copyfile(self.ctx.database_path, current_month_bak)
|
||||
case "postgresql+psycopg2":
|
||||
logger.warning(f"Backup function not yet implemented for psql")
|
||||
current_month_bak = current_month_bak.with_suffix(".psql")
|
||||
|
||||
|
||||
|
||||
class AddSubForm(QWidget):
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class EquipmentUsage(QDialog):
|
||||
|
||||
def __init__(self, parent, submission: BasicSubmission) -> QDialog:
|
||||
def __init__(self, parent, submission: BasicSubmission):
|
||||
super().__init__(parent)
|
||||
self.submission = submission
|
||||
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
|
||||
@@ -139,7 +139,7 @@ class RoleComboBox(QWidget):
|
||||
Changes what tips are available when process is changed
|
||||
"""
|
||||
process = self.process.currentText().strip()
|
||||
logger.debug(f"Checking process: {process} for equipment {self.role.name}")
|
||||
# logger.debug(f"Checking process: {process} for equipment {self.role.name}")
|
||||
process = Process.query(name=process)
|
||||
if process.tip_roles:
|
||||
for iii, tip_role in enumerate(process.tip_roles):
|
||||
|
||||
@@ -21,10 +21,10 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
# Main window class
|
||||
class GelBox(QDialog):
|
||||
|
||||
def __init__(self, parent, img_path:str|Path, submission:WastewaterArtic):
|
||||
def __init__(self, parent, img_path: str | Path, submission: WastewaterArtic):
|
||||
super().__init__(parent)
|
||||
# NOTE: setting title
|
||||
self.setWindowTitle("PyQtGraph")
|
||||
self.setWindowTitle(f"Gel - {img_path}")
|
||||
self.img_path = img_path
|
||||
self.submission = submission
|
||||
# NOTE: setting geometry
|
||||
@@ -41,7 +41,7 @@ class GelBox(QDialog):
|
||||
def UiComponents(self):
|
||||
"""
|
||||
Create widgets in ui
|
||||
"""
|
||||
"""
|
||||
# NOTE: setting configuration options
|
||||
pg.setConfigOptions(antialias=True)
|
||||
# NOTE: creating image view object
|
||||
@@ -49,41 +49,42 @@ class GelBox(QDialog):
|
||||
# NOTE: Create image.
|
||||
# NOTE: For some reason, ImageView wants to flip the image, so we have to rotate and flip the array first.
|
||||
# NOTE: Using the Image.rotate function results in cropped image, so using np.
|
||||
img = np.flip(np.rot90(np.array(Image.open(self.img_path)),1),0)
|
||||
img = np.flip(np.rot90(np.array(Image.open(self.img_path)), 1), 0)
|
||||
self.imv.setImage(img)
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
|
||||
layout.addWidget(QLabel("DNA Core Submission Number"), 21, 1)
|
||||
self.core_number = QLineEdit()
|
||||
self.core_number.setText(self.submission.dna_core_submission_number)
|
||||
layout.addWidget(self.core_number, 0,2)
|
||||
layout.addWidget(QLabel("Gel Barcode"),0,3)
|
||||
layout.addWidget(self.core_number, 21, 2)
|
||||
layout.addWidget(QLabel("Gel Barcode"), 21, 3)
|
||||
self.gel_barcode = QLineEdit()
|
||||
self.gel_barcode.setText(self.submission.gel_barcode)
|
||||
layout.addWidget(self.gel_barcode, 0, 4)
|
||||
layout.addWidget(self.gel_barcode, 21, 4)
|
||||
# NOTE: setting this layout to the widget
|
||||
# NOTE: plot window goes on right side, spanning 3 rows
|
||||
layout.addWidget(self.imv, 1, 1,20,20)
|
||||
layout.addWidget(self.imv, 0, 1, 20, 20)
|
||||
# NOTE: setting this widget as central widget of the main window
|
||||
try:
|
||||
control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
|
||||
except KeyError:
|
||||
control_info = None
|
||||
self.form = ControlsForm(parent=self, control_info=control_info)
|
||||
layout.addWidget(self.form,22,1,1,4)
|
||||
layout.addWidget(self.form, 22, 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, 23, 1, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
|
||||
layout.addWidget(self.buttonBox, 23, 1, 1, 1) #, alignment=Qt.AlignmentFlag.AlignTop)
|
||||
self.setLayout(layout)
|
||||
|
||||
def parse_form(self) -> Tuple[str, str|Path, list]:
|
||||
|
||||
def parse_form(self) -> Tuple[str, str | Path, list]:
|
||||
"""
|
||||
Get relevant values from self/form
|
||||
|
||||
Returns:
|
||||
Tuple[str, str|Path, list]: output values
|
||||
"""
|
||||
"""
|
||||
dna_core_submission_number = self.core_number.text()
|
||||
gel_barcode = self.gel_barcode.text()
|
||||
values, comment = self.form.parse_form()
|
||||
@@ -92,7 +93,7 @@ class GelBox(QDialog):
|
||||
|
||||
class ControlsForm(QWidget):
|
||||
|
||||
def __init__(self, parent, control_info:List=None) -> None:
|
||||
def __init__(self, parent, control_info: List = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.layout = QGridLayout()
|
||||
columns = []
|
||||
@@ -101,9 +102,10 @@ class ControlsForm(QWidget):
|
||||
tt_text = "\n".join([f"{item['sample_id']} - CELL {item['location']}" for item in control_info])
|
||||
except TypeError:
|
||||
tt_text = None
|
||||
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
|
||||
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)
|
||||
self.layout.addWidget(label, 0, iii, 1, 1)
|
||||
if iii > 1:
|
||||
columns.append(item)
|
||||
elif iii == 0:
|
||||
@@ -114,7 +116,8 @@ class ControlsForm(QWidget):
|
||||
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):
|
||||
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):
|
||||
@@ -125,11 +128,11 @@ class ControlsForm(QWidget):
|
||||
widge.setCurrentIndex(0)
|
||||
widge.setEditable(True)
|
||||
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
|
||||
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
|
||||
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
|
||||
self.layout.addWidget(widge, iii + 1, jjj + 2, 1, 1)
|
||||
self.layout.addWidget(QLabel("Comments:"), 0, 5, 1, 1)
|
||||
self.comment_field = QTextEdit(self)
|
||||
self.comment_field.setFixedHeight(50)
|
||||
self.layout.addWidget(self.comment_field, 1,5,4,1)
|
||||
self.layout.addWidget(self.comment_field, 1, 5, 4, 1)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self) -> List[dict]:
|
||||
@@ -138,12 +141,12 @@ class ControlsForm(QWidget):
|
||||
|
||||
Returns:
|
||||
List[dict]: output of values
|
||||
"""
|
||||
"""
|
||||
output = []
|
||||
for le in self.findChildren(QComboBox):
|
||||
label = [item.strip() for item in le.objectName().split(" : ")]
|
||||
try:
|
||||
dicto = [item for item in output if item['name']==label[0]][0]
|
||||
dicto = [item for item in output if item['name'] == label[0]][0]
|
||||
except IndexError:
|
||||
dicto = dict(name=label[0], values=[])
|
||||
dicto['values'].append(dict(name=label[1], value=le.currentText()))
|
||||
|
||||
@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import (
|
||||
QDialogButtonBox, QDateEdit, QPushButton, QFormLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QDate
|
||||
from tools import jinja_template_loading, Settings
|
||||
from tools import jinja_template_loading
|
||||
from backend.db.models import *
|
||||
import logging
|
||||
from .pop_ups import AlertPop
|
||||
@@ -45,18 +45,19 @@ class AddReagentForm(QDialog):
|
||||
self.exp_input.setObjectName('expiry')
|
||||
# NOTE: if expiry is not passed in from gui, use today
|
||||
if expiry is None:
|
||||
self.exp_input.setDate(QDate.currentDate())
|
||||
# self.exp_input.setDate(QDate.currentDate())
|
||||
self.exp_input.setDate(QDate(1970, 1, 1))
|
||||
else:
|
||||
try:
|
||||
self.exp_input.setDate(expiry)
|
||||
except TypeError:
|
||||
self.exp_input.setDate(QDate.currentDate())
|
||||
self.exp_input.setDate(QDate(1970, 1, 1))
|
||||
# NOTE: widget to get reagent type info
|
||||
self.type_input = QComboBox()
|
||||
self.type_input.setObjectName('type')
|
||||
self.type_input.addItems([item.name for item in ReagentRole.query()])
|
||||
# logger.debug(f"Trying to find index of {reagent_type}")
|
||||
# NOTE: convert input to user friendly string?
|
||||
# NOTE: convert input to user-friendly string?
|
||||
try:
|
||||
reagent_role = reagent_role.replace("_", " ").title()
|
||||
except AttributeError:
|
||||
|
||||
@@ -7,8 +7,8 @@ from PyQt6.QtWidgets import QTableView, QMenu
|
||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||
from PyQt6.QtGui import QAction, QCursor
|
||||
from backend.db.models import BasicSubmission
|
||||
from backend.excel import make_report_html, make_report_xlsx, ReportMaker
|
||||
from tools import Report, Result, row_map, get_first_blank_df_row, html_to_pdf
|
||||
from backend.excel import ReportMaker
|
||||
from tools import Report, Result, report_result
|
||||
from .functions import select_save_file, select_open_file
|
||||
from .misc import ReportDatePicker
|
||||
import pandas as pd
|
||||
@@ -129,14 +129,15 @@ class SubmissionsSheet(QTableView):
|
||||
func = self.con_actions[action_name]
|
||||
func(obj=self)
|
||||
|
||||
@report_result
|
||||
def link_extractions(self):
|
||||
"""
|
||||
Pull extraction logs into the db
|
||||
"""
|
||||
self.link_extractions_function()
|
||||
self.app.report.add_result(self.report)
|
||||
"""
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
self.link_extractions_function()
|
||||
self.report.add_result(self.report)
|
||||
return self.report
|
||||
|
||||
def link_extractions_function(self):
|
||||
"""
|
||||
@@ -179,6 +180,7 @@ class SubmissionsSheet(QTableView):
|
||||
sub.save()
|
||||
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
|
||||
|
||||
@report_result
|
||||
def link_pcr(self):
|
||||
"""
|
||||
Pull pcr logs into the db
|
||||
@@ -186,7 +188,7 @@ class SubmissionsSheet(QTableView):
|
||||
self.link_pcr_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
return self.report
|
||||
|
||||
def link_pcr_function(self):
|
||||
"""
|
||||
@@ -225,15 +227,15 @@ class SubmissionsSheet(QTableView):
|
||||
# NOTE: check if pcr_info already exists
|
||||
sub.save()
|
||||
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
|
||||
|
||||
|
||||
@report_result
|
||||
def generate_report(self):
|
||||
"""
|
||||
Make a report
|
||||
"""
|
||||
self.generate_report_function()
|
||||
self.app.report.add_result(self.report)
|
||||
"""
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
self.generate_report_function()
|
||||
return self.report
|
||||
|
||||
def generate_report_function(self):
|
||||
"""
|
||||
@@ -250,43 +252,7 @@ class SubmissionsSheet(QTableView):
|
||||
dlg = ReportDatePicker()
|
||||
if dlg.exec():
|
||||
info = dlg.parse_form()
|
||||
# logger.debug(f"Report info: {info}")
|
||||
# NOTE: find submissions based on date range
|
||||
subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date'])
|
||||
# NOTE: convert each object to dict
|
||||
records = [item.to_dict(report=True) for item in subs]
|
||||
# logger.debug(f"Records: {pformat(records)}")
|
||||
# NOTE: make dataframe from record dictionaries
|
||||
detailed_df, summary_df = make_report_xlsx(records=records)
|
||||
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
||||
# NOTE: get save location of report
|
||||
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx")
|
||||
# html_to_pdf(html=html, output_file=fname)
|
||||
# writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
||||
# summary_df.to_excel(writer, sheet_name="Report")
|
||||
# detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
||||
# worksheet: Worksheet = writer.sheets['Report']
|
||||
# for idx, col in enumerate(summary_df, start=1): # loop through all columns
|
||||
# series = summary_df[col]
|
||||
# max_len = max((
|
||||
# series.astype(str).map(len).max(), # len of largest item
|
||||
# len(str(series.name)) # len of column name/header
|
||||
# )) + 20 # adding a little extra space
|
||||
# try:
|
||||
# # NOTE: Convert idx to letter
|
||||
# col_letter = chr(ord('@') + idx)
|
||||
# worksheet.column_dimensions[col_letter].width = max_len
|
||||
# except ValueError:
|
||||
# pass
|
||||
# blank_row = get_first_blank_df_row(summary_df) + 1
|
||||
# # logger.debug(f"Blank row index = {blank_row}")
|
||||
# for col in range(3,6):
|
||||
# col_letter = row_map[col]
|
||||
# worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
|
||||
# for cell in worksheet['D']:
|
||||
# if cell.row > 1:
|
||||
# cell.style = 'Currency'
|
||||
# writer.close()
|
||||
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
|
||||
rp.write_report(filename=fname, obj=self)
|
||||
self.report.add_result(report)
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
from . import select_open_file, select_save_file
|
||||
import logging, difflib, inspect
|
||||
from pathlib import Path
|
||||
from tools import Report, Result, check_not_nan, workbook_2_csv, main_form_style
|
||||
from tools import Report, Result, check_not_nan, workbook_2_csv, main_form_style, report_result
|
||||
from backend.excel.parser import SheetParser
|
||||
from backend.validators import PydSubmission, PydReagent
|
||||
from backend.db import (
|
||||
@@ -59,17 +59,16 @@ class SubmissionFormContainer(QWidget):
|
||||
self.app.last_dir = fname.parent
|
||||
self.import_drag.emit(fname)
|
||||
|
||||
@report_result
|
||||
def importSubmission(self, fname: Path | None = None):
|
||||
"""
|
||||
import submission from excel sheet into form
|
||||
"""
|
||||
self.app.raise_()
|
||||
self.app.activateWindow()
|
||||
self.import_submission_function(fname)
|
||||
# logger.debug(f"Result from result reporter: {self.report.results}")
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
self.import_submission_function(fname)
|
||||
return self.report
|
||||
|
||||
def import_submission_function(self, fname: Path | None = None):
|
||||
"""
|
||||
@@ -115,8 +114,9 @@ class SubmissionFormContainer(QWidget):
|
||||
# logger.debug(f"Outgoing report: {self.report.results}")
|
||||
# logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
||||
|
||||
@report_result
|
||||
def add_reagent(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
|
||||
name: str | None = None):
|
||||
name: str | None = None) -> Tuple[PydReagent, Report]:
|
||||
"""
|
||||
Action to create new reagent in DB.
|
||||
|
||||
@@ -144,16 +144,18 @@ class SubmissionFormContainer(QWidget):
|
||||
sqlobj, assoc, result = reagent.toSQL()
|
||||
sqlobj.save()
|
||||
report.add_result(result)
|
||||
self.app.report.add_result(report)
|
||||
self.app.result_reporter()
|
||||
return reagent
|
||||
# logger.debug(f"Reagent: {reagent}, Report: {report}")
|
||||
return reagent, report
|
||||
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, submission: PydSubmission) -> None:
|
||||
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
# self.report = Report()
|
||||
# logger.debug(f"Disable: {disable}")
|
||||
if disable is None:
|
||||
disable = []
|
||||
self.app = parent.app
|
||||
self.pyd = submission
|
||||
self.missing_info = []
|
||||
@@ -166,12 +168,19 @@ class SubmissionFormWidget(QWidget):
|
||||
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
||||
if k in self.ignore:
|
||||
continue
|
||||
try:
|
||||
# logger.debug(f"Key: {k}, Disable: {disable}")
|
||||
check = k in disable
|
||||
# logger.debug(f"Check: {check}")
|
||||
except TypeError:
|
||||
check = False
|
||||
try:
|
||||
value = self.pyd.__getattribute__(k)
|
||||
except AttributeError:
|
||||
logger.error(f"Couldn't get attribute from pyd: {k}")
|
||||
value = dict(value=None, missing=True)
|
||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'], sub_obj=st)
|
||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'],
|
||||
sub_obj=st, disable=check)
|
||||
if add_widget is not None:
|
||||
self.layout.addWidget(add_widget)
|
||||
if k == "extraction_kit":
|
||||
@@ -180,11 +189,13 @@ class SubmissionFormWidget(QWidget):
|
||||
self.scrape_reagents(self.pyd.extraction_kit)
|
||||
|
||||
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | None = None,
|
||||
extraction_kit: str | None = None, sub_obj:BasicSubmission|None=None) -> "self.InfoItem":
|
||||
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None,
|
||||
disable: bool = False) -> "self.InfoItem":
|
||||
"""
|
||||
Make an InfoItem widget to hold a field
|
||||
|
||||
Args:
|
||||
disable ():
|
||||
key (str): Name of the field
|
||||
value (dict): Value of field
|
||||
submission_type (str | None, optional): Submissiontype as str. Defaults to None.
|
||||
@@ -192,18 +203,25 @@ class SubmissionFormWidget(QWidget):
|
||||
Returns:
|
||||
self.InfoItem: Form widget to hold name:value
|
||||
"""
|
||||
# logger.debug(f"Key: {key}, Disable: {disable}")
|
||||
if key not in self.ignore:
|
||||
match value:
|
||||
case PydReagent():
|
||||
if value.name.lower() != "not applicable":
|
||||
widget = self.ReagentFormWidget(self, reagent=value, extraction_kit=extraction_kit)
|
||||
|
||||
else:
|
||||
widget = None
|
||||
case _:
|
||||
widget = self.InfoItem(self, key=key, value=value, submission_type=submission_type, sub_obj=sub_obj)
|
||||
# logger.debug(f"Setting widget enabled to: {not disable}")
|
||||
if disable:
|
||||
widget.input.setEnabled(False)
|
||||
widget.input.setToolTip("Widget disabled to protect database integrity.")
|
||||
return widget
|
||||
return None
|
||||
|
||||
@report_result
|
||||
def scrape_reagents(self, *args, **kwargs): #extraction_kit:str, caller:str|None=None):
|
||||
"""
|
||||
Extracted scrape reagents function that will run when
|
||||
@@ -250,8 +268,7 @@ class SubmissionFormWidget(QWidget):
|
||||
self.layout.addWidget(submit_btn)
|
||||
submit_btn.clicked.connect(self.submit_new_sample_function)
|
||||
self.setLayout(self.layout)
|
||||
self.app.report.add_result(report)
|
||||
self.app.result_reporter()
|
||||
return report
|
||||
|
||||
def clear_form(self):
|
||||
"""
|
||||
@@ -275,7 +292,8 @@ class SubmissionFormWidget(QWidget):
|
||||
query = [widget for widget in query if widget.objectName() == object_name]
|
||||
return query
|
||||
|
||||
def submit_new_sample_function(self) -> QWidget:
|
||||
@report_result
|
||||
def submit_new_sample_function(self, *args) -> Report:
|
||||
"""
|
||||
Parse forms and add sample to the database.
|
||||
|
||||
@@ -294,37 +312,40 @@ class SubmissionFormWidget(QWidget):
|
||||
_, result = self.pyd.check_kit_integrity()
|
||||
report.add_result(result)
|
||||
if len(result.results) > 0:
|
||||
self.app.report.add_result(report)
|
||||
self.app.result_reporter()
|
||||
# self.app.report.add_result(report)
|
||||
# self.app.report_result()
|
||||
return
|
||||
# logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n")
|
||||
base_submission, result = self.pyd.to_sql()
|
||||
# logger.debug(f"SQL object: {pformat(base_submission.__dict__)}")
|
||||
# logger.debug(f"Base submission: {base_submission.to_dict()}")
|
||||
# NOTE: check output message for issues
|
||||
match result.code:
|
||||
try:
|
||||
code = report.results[-1].code
|
||||
except IndexError:
|
||||
code = 0
|
||||
match code:
|
||||
# NOTE: code 0: everything is fine.
|
||||
case 0:
|
||||
report.add_result(None)
|
||||
pass
|
||||
# NOTE: code 1: ask for overwrite
|
||||
case 1:
|
||||
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result.msg)
|
||||
if dlg.exec():
|
||||
# NOTE: Do not add duplicate reagents.
|
||||
result = None
|
||||
|
||||
pass
|
||||
else:
|
||||
self.app.ctx.database_session.rollback()
|
||||
report.add_result(Result(msg="Overwrite cancelled", status="Information"))
|
||||
self.app.report.add_result(report)
|
||||
self.app.result_reporter()
|
||||
return
|
||||
# self.app.report.add_result(report)
|
||||
# self.app.report_result()
|
||||
return report
|
||||
# NOTE: code 2: No RSL plate number given
|
||||
case 2:
|
||||
report.add_result(result)
|
||||
self.app.report.add_result(report)
|
||||
self.app.result_reporter()
|
||||
return
|
||||
# self.app.report.add_result(report)
|
||||
# self.app.report_result()
|
||||
return report
|
||||
case _:
|
||||
pass
|
||||
# NOTE: add reagents to submission object
|
||||
@@ -338,8 +359,7 @@ class SubmissionFormWidget(QWidget):
|
||||
# NOTE: reset form
|
||||
self.setParent(None)
|
||||
# logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
|
||||
self.app.report.add_result(report)
|
||||
self.app.result_reporter()
|
||||
return report
|
||||
|
||||
def export_csv_function(self, fname: Path | None = None):
|
||||
"""
|
||||
@@ -352,7 +372,6 @@ class SubmissionFormWidget(QWidget):
|
||||
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
|
||||
try:
|
||||
self.pyd.export_csv(fname)
|
||||
# workbook_2_csv(worksheet=self.pyd.csv, filename=fname)
|
||||
except PermissionError:
|
||||
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
except AttributeError:
|
||||
@@ -398,11 +417,13 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
class InfoItem(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None, sub_obj:BasicSubmission|None=None) -> None:
|
||||
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None,
|
||||
sub_obj: BasicSubmission | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
layout = QVBoxLayout()
|
||||
self.label = self.ParsedQLabel(key=key, value=value)
|
||||
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type, sub_obj=sub_obj)
|
||||
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type,
|
||||
sub_obj=sub_obj)
|
||||
self.setObjectName(key)
|
||||
try:
|
||||
self.missing: bool = value['missing']
|
||||
@@ -439,7 +460,8 @@ class SubmissionFormWidget(QWidget):
|
||||
return None, None
|
||||
return self.input.objectName(), dict(value=value, missing=self.missing)
|
||||
|
||||
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None, sub_obj:BasicSubmission|None=None) -> QWidget:
|
||||
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None,
|
||||
sub_obj: BasicSubmission | None = None) -> QWidget:
|
||||
"""
|
||||
Creates form widget
|
||||
|
||||
@@ -472,6 +494,7 @@ class SubmissionFormWidget(QWidget):
|
||||
pass
|
||||
# set combobox values to lookedup values
|
||||
add_widget.addItems(labs)
|
||||
add_widget.setToolTip("Select submitting lab.")
|
||||
case 'extraction_kit':
|
||||
# if extraction kit not available, all other values fail
|
||||
if not check_not_nan(value):
|
||||
@@ -493,15 +516,7 @@ class SubmissionFormWidget(QWidget):
|
||||
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
||||
obj.ext_kit = uses[0]
|
||||
add_widget.addItems(uses)
|
||||
# case 'submitted_date':
|
||||
# # NOTE: uses base calendar
|
||||
# add_widget = QDateEdit(calendarPopup=True)
|
||||
# # NOTE: sets submitted date based on date found in excel sheet
|
||||
# try:
|
||||
# add_widget.setDate(value)
|
||||
# # NOTE: if not found, use today
|
||||
# except:
|
||||
# add_widget.setDate(date.today())
|
||||
add_widget.setToolTip("Select extraction kit.")
|
||||
case 'submission_category':
|
||||
add_widget = QComboBox()
|
||||
cats = ['Diagnostic', "Surveillance", "Research"]
|
||||
@@ -511,6 +526,7 @@ class SubmissionFormWidget(QWidget):
|
||||
except ValueError:
|
||||
cats.insert(0, cats.pop(cats.index(submission_type)))
|
||||
add_widget.addItems(cats)
|
||||
add_widget.setToolTip("Enter submission category or select from list.")
|
||||
case _:
|
||||
if key in sub_obj.timestamps():
|
||||
add_widget = QDateEdit(calendarPopup=True)
|
||||
@@ -520,11 +536,13 @@ class SubmissionFormWidget(QWidget):
|
||||
# NOTE: if not found, use today
|
||||
except:
|
||||
add_widget.setDate(date.today())
|
||||
add_widget.setToolTip(f"Select date for {key}")
|
||||
else:
|
||||
# NOTE: anything else gets added in as a line edit
|
||||
add_widget = QLineEdit()
|
||||
# logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
|
||||
add_widget.setText(str(value).replace("_", " "))
|
||||
add_widget.setToolTip(f"Enter value for {key}")
|
||||
if add_widget is not None:
|
||||
add_widget.setObjectName(key)
|
||||
add_widget.setParent(parent)
|
||||
@@ -594,13 +612,14 @@ class SubmissionFormWidget(QWidget):
|
||||
# NOTE: If changed set self.missing to True and update self.label
|
||||
self.lot.currentTextChanged.connect(self.updated)
|
||||
|
||||
def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||
def parse_form(self) -> Tuple[PydReagent | None, Report]:
|
||||
"""
|
||||
Pulls form info into PydReagent
|
||||
|
||||
Returns:
|
||||
Tuple[PydReagent, dict]: PydReagent and Report(?)
|
||||
"""
|
||||
report = Report()
|
||||
lot = self.lot.currentText()
|
||||
# logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
|
||||
wanted_reagent = Reagent.query(lot_number=lot, reagent_role=self.reagent.role)
|
||||
@@ -609,14 +628,16 @@ class SubmissionFormWidget(QWidget):
|
||||
dlg = QuestionAsker(title=f"Add {lot}?",
|
||||
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
|
||||
if dlg.exec():
|
||||
wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_role=self.reagent.role,
|
||||
expiry=self.reagent.expiry,
|
||||
name=self.reagent.name)
|
||||
return wanted_reagent, None
|
||||
wanted_reagent, _ = self.parent().parent().add_reagent(reagent_lot=lot,
|
||||
reagent_role=self.reagent.role,
|
||||
expiry=self.reagent.expiry,
|
||||
name=self.reagent.name)
|
||||
return wanted_reagent, report
|
||||
else:
|
||||
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||
# logger.debug("Will not add reagent.")
|
||||
return None, Result(msg="Failed integrity check", status="Critical")
|
||||
report.add_result(Result(msg="Failed integrity check", status="Critical"))
|
||||
return None, report
|
||||
else:
|
||||
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
||||
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
||||
@@ -624,7 +645,7 @@ class SubmissionFormWidget(QWidget):
|
||||
if rt is None:
|
||||
rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name,
|
||||
expiry=wanted_reagent.expiry, missing=False), None
|
||||
expiry=wanted_reagent.expiry, missing=False), report
|
||||
|
||||
def updated(self):
|
||||
"""
|
||||
@@ -708,4 +729,5 @@ class SubmissionFormWidget(QWidget):
|
||||
# logger.debug(f"New relevant reagents: {relevant_reagents}")
|
||||
self.setObjectName(f"lot_{reagent.role}")
|
||||
self.addItems(relevant_reagents)
|
||||
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}")
|
||||
# self.setStyleSheet(main_form_style)
|
||||
|
||||
@@ -4,6 +4,8 @@ Contains miscellaenous functions used by both frontend and backend.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
import jinja2
|
||||
import numpy as np
|
||||
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
|
||||
import pandas as pd
|
||||
@@ -18,7 +20,6 @@ from typing import Any, Tuple, Literal, List
|
||||
from PyQt6.QtGui import QPageSize
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
# from PyQt6 import QtPrintSupport, QtCore, QtWebEngineWidgets
|
||||
from PyQt6.QtPrintSupport import QPrinter
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -74,7 +75,7 @@ def check_key_or_attr(key: str, interest: dict | object, check_none: bool = Fals
|
||||
|
||||
Returns:
|
||||
bool: True if exists, else False
|
||||
"""
|
||||
"""
|
||||
match interest:
|
||||
case dict():
|
||||
if key in interest.keys():
|
||||
@@ -175,7 +176,7 @@ def is_missing(value: Any) -> Tuple[Any, bool]:
|
||||
|
||||
Returns:
|
||||
Tuple[Any, bool]: Value, True if nan, else False
|
||||
"""
|
||||
"""
|
||||
if check_not_nan(value):
|
||||
return value, False
|
||||
else:
|
||||
@@ -222,7 +223,11 @@ class Settings(BaseSettings, extra="allow"):
|
||||
FileNotFoundError: Error if database not found.
|
||||
|
||||
"""
|
||||
database_schema: str
|
||||
directory_path: Path
|
||||
database_user: str | None = None
|
||||
database_password: str | None = None
|
||||
database_name: str
|
||||
database_path: Path | str | None = None
|
||||
backup_path: Path | str | None = None
|
||||
# super_users: list|None = None
|
||||
@@ -260,17 +265,26 @@ class Settings(BaseSettings, extra="allow"):
|
||||
@field_validator('database_path', mode="before")
|
||||
@classmethod
|
||||
def ensure_database_exists(cls, value, values):
|
||||
if value == ":memory:":
|
||||
return value
|
||||
match value:
|
||||
case str():
|
||||
value = Path(value)
|
||||
case None:
|
||||
value = values.data['directory_path'].joinpath("submissions.db")
|
||||
if value.exists():
|
||||
return value
|
||||
else:
|
||||
raise FileNotFoundError(f"Couldn't find database at {value}")
|
||||
# if value == ":memory:":
|
||||
# return value
|
||||
match values.data['database_schema']:
|
||||
case "sqlite":
|
||||
value = f"/{Path(value).absolute().__str__()}/{values.data['database_name']}.db"
|
||||
# db_name = f"{values.data['database_name']}.db"
|
||||
case _:
|
||||
value = f"@{value}/{values.data['database_name']}"
|
||||
# db_name = values.data['database_name']
|
||||
# match value:
|
||||
# case str():
|
||||
# value = Path(value)
|
||||
# case None:
|
||||
# value = values.data['directory_path'].joinpath("submissions.db")
|
||||
# if value.exists():
|
||||
# return value
|
||||
# else:
|
||||
# raise FileNotFoundError(f"Couldn't find database at {value}")
|
||||
|
||||
return value
|
||||
|
||||
@field_validator('database_session', mode="before")
|
||||
@classmethod
|
||||
@@ -278,27 +292,33 @@ class Settings(BaseSettings, extra="allow"):
|
||||
if value is not None:
|
||||
return value
|
||||
else:
|
||||
database_path = values.data['database_path']
|
||||
if database_path is None:
|
||||
# NOTE: check in user's .submissions directory for submissions.db
|
||||
if Path.home().joinpath(".submissions", "submissions.db").exists():
|
||||
database_path = Path.home().joinpath(".submissions", "submissions.db")
|
||||
# NOTE: finally, look in the local dir
|
||||
else:
|
||||
database_path = package_dir.joinpath("submissions.db")
|
||||
else:
|
||||
if database_path == ":memory:":
|
||||
pass
|
||||
# NOTE: check if user defined path is directory
|
||||
elif database_path.is_dir():
|
||||
database_path = database_path.joinpath("submissions.db")
|
||||
# NOTE: check if user defined path is a file
|
||||
elif database_path.is_file():
|
||||
database_path = database_path
|
||||
else:
|
||||
raise FileNotFoundError("No database file found. Exiting program.")
|
||||
template = jinja_template_loading().from_string(
|
||||
"{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ values['database_path'] }}")
|
||||
database_path = template.render(values=values.data)
|
||||
# print(f"Using {database_path} for database path")
|
||||
# database_path = values.data['database_path']
|
||||
# if database_path is None:
|
||||
# # NOTE: check in user's .submissions directory for submissions.db
|
||||
# if Path.home().joinpath(".submissions", "submissions.db").exists():
|
||||
# database_path = Path.home().joinpath(".submissions", "submissions.db")
|
||||
# # NOTE: finally, look in the local dir
|
||||
# else:
|
||||
# database_path = package_dir.joinpath("submissions.db")
|
||||
# else:
|
||||
# if database_path == ":memory:":
|
||||
# pass
|
||||
# # NOTE: check if user defined path is directory
|
||||
# elif database_path.is_dir():
|
||||
# database_path = database_path.joinpath("submissions.db")
|
||||
# # NOTE: check if user defined path is a file
|
||||
# elif database_path.is_file():
|
||||
# database_path = database_path
|
||||
# else:
|
||||
# raise FileNotFoundError("No database file found. Exiting program.")
|
||||
logger.info(f"Using {database_path} for database file.")
|
||||
engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
|
||||
# engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
|
||||
# engine = create_engine("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions")
|
||||
engine = create_engine(database_path)
|
||||
session = Session(engine)
|
||||
return session
|
||||
|
||||
@@ -316,13 +336,21 @@ class Settings(BaseSettings, extra="allow"):
|
||||
|
||||
def set_from_db(self, db_path: Path):
|
||||
if 'pytest' in sys.modules:
|
||||
config_items = dict(power_users=['lwark', 'styson', 'ruwang'])
|
||||
output = dict(power_users=['lwark', 'styson', 'ruwang'])
|
||||
else:
|
||||
session = Session(create_engine(f"sqlite:///{db_path}"))
|
||||
# session = Session(create_engine(f"sqlite:///{db_path}"))
|
||||
session = self.database_session
|
||||
config_items = session.execute(text("SELECT * FROM _configitem")).all()
|
||||
session.close()
|
||||
config_items = {item[1]: json.loads(item[2]) for item in config_items}
|
||||
for k, v in config_items.items():
|
||||
# print(config_items)
|
||||
output = {}
|
||||
for item in config_items:
|
||||
try:
|
||||
output[item[1]] = json.loads(item[2])
|
||||
except (JSONDecodeError, TypeError):
|
||||
output[item[1]] = item[2]
|
||||
# config_items = {item[1]: json.loads(item[2]) for item in config_items}
|
||||
for k, v in output.items():
|
||||
if not hasattr(self, k):
|
||||
self.__setattr__(k, v)
|
||||
|
||||
@@ -355,7 +383,6 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
|
||||
CONFIGDIR.mkdir(parents=True)
|
||||
except FileExistsError:
|
||||
logger.warning(f"Config directory {CONFIGDIR} already exists.")
|
||||
|
||||
try:
|
||||
LOGDIR.mkdir(parents=True)
|
||||
except FileExistsError:
|
||||
@@ -373,7 +400,7 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
|
||||
if check_if_app():
|
||||
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
|
||||
else:
|
||||
settings_path = package_dir.joinpath('config.yml')
|
||||
settings_path = package_dir.joinpath('src', 'config.yml')
|
||||
with open(settings_path, "r") as dset:
|
||||
default_settings = yaml.load(dset, Loader=yaml.Loader)
|
||||
# NOTE: Tell program we need to copy the config.yml to the user directory
|
||||
@@ -502,7 +529,7 @@ def setup_logger(verbosity: int = 3):
|
||||
# NOTE: create console handler with a higher log level
|
||||
# NOTE: create custom logger with STERR -> log
|
||||
ch = logging.StreamHandler(stream=sys.stdout)
|
||||
# NOTE: set looging level based on verbosity
|
||||
# NOTE: set logging level based on verbosity
|
||||
match verbosity:
|
||||
case 3:
|
||||
ch.setLevel(logging.DEBUG)
|
||||
@@ -542,10 +569,10 @@ def copy_settings(settings_path: Path, settings: dict) -> dict:
|
||||
dict: output dictionary for use in first run
|
||||
"""
|
||||
# NOTE: if the current user is not a superuser remove the superusers entry
|
||||
if not getpass.getuser() in settings['super_users']:
|
||||
del settings['super_users']
|
||||
if not getpass.getuser() in settings['power_users']:
|
||||
del settings['power_users']
|
||||
# if not getpass.getuser() in settings['super_users']:
|
||||
# del settings['super_users']
|
||||
# if not getpass.getuser() in settings['power_users']:
|
||||
# del settings['power_users']
|
||||
if not settings_path.exists():
|
||||
with open(settings_path, 'w') as f:
|
||||
yaml.dump(settings, f)
|
||||
@@ -651,7 +678,7 @@ class Report(BaseModel):
|
||||
|
||||
Args:
|
||||
result (Result | Report | None): Results to be added.
|
||||
"""
|
||||
"""
|
||||
match result:
|
||||
case Result():
|
||||
logger.info(f"Adding {result} to results.")
|
||||
@@ -668,7 +695,7 @@ class Report(BaseModel):
|
||||
logger.error(f"Unknown variable type: {type(result)} for <Result> entry into <Report>")
|
||||
|
||||
|
||||
def rreplace(s:str, old:str, new:str) -> str:
|
||||
def rreplace(s: str, old: str, new: str) -> str:
|
||||
"""
|
||||
Removes rightmost occurence of a substring
|
||||
|
||||
@@ -679,18 +706,18 @@ def rreplace(s:str, old:str, new:str) -> str:
|
||||
|
||||
Returns:
|
||||
str: updated string
|
||||
"""
|
||||
"""
|
||||
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
||||
|
||||
|
||||
def html_to_pdf(html:str, output_file: Path | str):
|
||||
def html_to_pdf(html: str, output_file: Path | str):
|
||||
"""
|
||||
Attempts to print an html string as a PDF. (currently not working)
|
||||
|
||||
Args:
|
||||
html (str): Input html string.
|
||||
output_file (Path | str): Output PDF file path.
|
||||
"""
|
||||
"""
|
||||
if isinstance(output_file, str):
|
||||
output_file = Path(output_file)
|
||||
logger.debug(f"Printing PDF to {output_file}")
|
||||
@@ -732,7 +759,7 @@ def workbook_2_csv(worksheet: Worksheet, filename: Path):
|
||||
Args:
|
||||
worksheet (Worksheet): Incoming worksheet
|
||||
filename (Path): Output csv filepath.
|
||||
"""
|
||||
"""
|
||||
with open(filename, 'w', newline="") as f:
|
||||
c = csv.writer(f)
|
||||
for r in worksheet.rows:
|
||||
@@ -748,7 +775,7 @@ def is_power_user() -> bool:
|
||||
|
||||
Returns:
|
||||
bool: True if yes, False if no.
|
||||
"""
|
||||
"""
|
||||
try:
|
||||
check = getpass.getuser() in ctx.power_users
|
||||
except:
|
||||
@@ -773,3 +800,32 @@ def check_authorization(func):
|
||||
return dict(code=1, message="This user does not have permission for this function.", status="warning")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def report_result(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
logger.debug(f"Arguments: {args}")
|
||||
logger.debug(f"Keyword arguments: {kwargs}")
|
||||
output = func(*args, **kwargs)
|
||||
if isinstance(output, tuple):
|
||||
report = [item for item in output if isinstance(item, Report)][0]
|
||||
else:
|
||||
report = None
|
||||
logger.debug(f"Got report: {report}")
|
||||
try:
|
||||
results = report.results
|
||||
except AttributeError:
|
||||
logger.error("No results available")
|
||||
results = []
|
||||
for iii, result in enumerate(results):
|
||||
logger.debug(f"Result {iii}: {result}")
|
||||
try:
|
||||
dlg = result.report()
|
||||
dlg.exec()
|
||||
except Exception as e:
|
||||
logger.error(f"Problem reporting due to {e}")
|
||||
logger.error(result.msg)
|
||||
logger.debug(f"Returning: {output}")
|
||||
return output
|
||||
|
||||
return wrapper
|
||||
|
||||
Reference in New Issue
Block a user