Before making a big mistake.

This commit is contained in:
Landon Wark
2023-11-16 14:55:55 -06:00
parent 6e865f1551
commit 74957ee318
22 changed files with 137 additions and 924 deletions

View File

@@ -1,3 +1,8 @@
## 202311.03
- Added in tabular log parser.
- Split main_window_functions into object specific functions.
## 202311.02 ## 202311.02
- Construct first strand integrated into Artic Import. - Construct first strand integrated into Artic Import.

0
None Normal file
View File

View File

@@ -1,5 +1,7 @@
- [ ] Buuuuuuhh. Split polymorphic objects into different tables... and rebuild DB.... FFFFF
- https://stackoverflow.com/questions/16910782/sqlalchemy-nested-inheritance-polymorphic-relationships
- [x] Create a result object to facilitate returning function results. - [x] Create a result object to facilitate returning function results.
- [ ] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up. - [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up.
- [x] Integrate 'Construct First Strand' into the Artic import. - [x] Integrate 'Construct First Strand' into the Artic import.
- [x] Clear out any unnecessary ctx passes now that queries are improved. - [x] Clear out any unnecessary ctx passes now that queries are improved.
- [x] Make a 'query or create' method in all db objects to go with new query. - [x] Make a 'query or create' method in all db objects to go with new query.

View File

@@ -24,7 +24,9 @@ if config.config_file_name is not None:
# from myapp import mymodel # from myapp import mymodel
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
from submissions.backend.db.models import Base from submissions.backend.db.models import Base
target_metadata = [Base.metadata] # META_DATA = MetaData(bind=CONN, reflect=True)
# base = ctx.database_session.get_bind()
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,
# can be acquired: # can be acquired:

View File

@@ -1,34 +0,0 @@
"""added in other ww techs
Revision ID: 3d9a88bd4ecd
Revises: f7f46e72f057
Create Date: 2023-08-30 11:03:41.575219
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3d9a88bd4ecd'
down_revision = 'f7f46e72f057'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('ext_technician', sa.String(length=64), nullable=True))
batch_op.add_column(sa.Column('pcr_technician', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.drop_column('pcr_technician')
batch_op.drop_column('ext_technician')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""adding artic_technician to Artic
Revision ID: 8a5bc2924ef9
Revises: b95478ffb4a3
Create Date: 2023-10-31 13:59:47.746122
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8a5bc2924ef9'
down_revision = 'b95478ffb4a3'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('artic_technician', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.drop_column('artic_technician')
# ### end Alembic commands ###

View File

@@ -1,33 +0,0 @@
"""adjusting reagents/reagenttypes to many-to-many
Revision ID: 9a133efb3ffd
Revises: 3d9a88bd4ecd
Create Date: 2023-09-01 10:28:22.335890
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9a133efb3ffd'
down_revision = '3d9a88bd4ecd'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_reagenttypes_reagents',
sa.Column('reagent_id', sa.INTEGER(), nullable=True),
sa.Column('reagenttype_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['reagent_id'], ['_reagents.id'], ),
sa.ForeignKeyConstraint(['reagenttype_id'], ['_reagent_types.id'], )
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_reagenttypes_reagents')
# ### end Alembic commands ###

View File

@@ -1,32 +0,0 @@
"""adding submission category
Revision ID: b95478ffb4a3
Revises: 9a133efb3ffd
Create Date: 2023-10-03 14:00:09.663055
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b95478ffb4a3'
down_revision = '9a133efb3ffd'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('submission_category', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.drop_column('submission_category')
# ### end Alembic commands ###

View File

@@ -1,204 +0,0 @@
"""rebuild database
Revision ID: f7f46e72f057
Revises:
Create Date: 2023-08-30 09:47:18.071070
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f7f46e72f057'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_contacts',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('email', sa.String(length=64), nullable=True),
sa.Column('phone', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_control_types',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('targets', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('_kits',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('_organizations',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('cost_centre', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_reagent_types',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('eol_ext', sa.Interval(), nullable=True),
sa.Column('last_used', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_samples',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('submitter_id', sa.String(length=64), nullable=False),
sa.Column('sample_type', sa.String(length=32), nullable=True),
sa.Column('ww_processing_num', sa.String(length=64), nullable=True),
sa.Column('ww_sample_full_id', sa.String(length=64), nullable=True),
sa.Column('rsl_number', sa.String(length=64), nullable=True),
sa.Column('collection_date', sa.TIMESTAMP(), nullable=True),
sa.Column('received_date', sa.TIMESTAMP(), nullable=True),
sa.Column('notes', sa.String(length=2000), nullable=True),
sa.Column('sample_location', sa.String(length=8), nullable=True),
sa.Column('organism', sa.String(length=64), nullable=True),
sa.Column('concentration', sa.String(length=16), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('submitter_id')
)
op.create_table('_submission_types',
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.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('_discounts',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('kit_id', sa.INTEGER(), nullable=True),
sa.Column('client_id', sa.INTEGER(), nullable=True),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('amount', sa.FLOAT(precision=2), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['_organizations.id'], name='fk_org_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['kit_id'], ['_kits.id'], name='fk_kit_type_id', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_orgs_contacts',
sa.Column('org_id', sa.INTEGER(), nullable=True),
sa.Column('contact_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['contact_id'], ['_contacts.id'], ),
sa.ForeignKeyConstraint(['org_id'], ['_organizations.id'], )
)
op.create_table('_reagents',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('type_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'], ['_reagent_types.id'], name='fk_reagent_type_id', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_reagenttypes_kittypes',
sa.Column('reagent_types_id', sa.INTEGER(), nullable=False),
sa.Column('kits_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.Column('required', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['kits_id'], ['_kits.id'], ),
sa.ForeignKeyConstraint(['reagent_types_id'], ['_reagent_types.id'], ),
sa.PrimaryKeyConstraint('reagent_types_id', 'kits_id')
)
op.create_table('_submissiontypes_kittypes',
sa.Column('submission_types_id', sa.INTEGER(), nullable=False),
sa.Column('kits_id', sa.INTEGER(), nullable=False),
sa.Column('mutable_cost_column', sa.FLOAT(precision=2), nullable=True),
sa.Column('mutable_cost_sample', sa.FLOAT(precision=2), nullable=True),
sa.Column('constant_cost', sa.FLOAT(precision=2), nullable=True),
sa.ForeignKeyConstraint(['kits_id'], ['_kits.id'], ),
sa.ForeignKeyConstraint(['submission_types_id'], ['_submission_types.id'], ),
sa.PrimaryKeyConstraint('submission_types_id', 'kits_id')
)
op.create_table('_submissions',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('rsl_plate_num', sa.String(length=32), nullable=False),
sa.Column('submitter_plate_num', sa.String(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.String(), nullable=True),
sa.Column('technician', sa.String(length=64), nullable=True),
sa.Column('reagents_id', sa.String(), nullable=True),
sa.Column('extraction_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('comment', sa.JSON(), nullable=True),
sa.Column('pcr_info', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['extraction_kit_id'], ['_kits.id'], name='fk_BS_extkit_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['reagents_id'], ['_reagents.id'], name='fk_BS_reagents_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['submission_type_name'], ['_submission_types.name'], name='fk_BS_subtype_name', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['submitting_lab_id'], ['_organizations.id'], name='fk_BS_sublab_id', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('rsl_plate_num'),
sa.UniqueConstraint('submitter_plate_num')
)
op.create_table('_control_samples',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('parent_id', sa.String(), 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),
sa.Column('matches', sa.JSON(), nullable=True),
sa.Column('kraken', sa.JSON(), nullable=True),
sa.Column('submission_id', sa.INTEGER(), nullable=True),
sa.Column('refseq_version', sa.String(length=16), nullable=True),
sa.Column('kraken2_version', sa.String(length=16), nullable=True),
sa.Column('kraken2_db_version', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['parent_id'], ['_control_types.id'], name='fk_control_parent_id'),
sa.ForeignKeyConstraint(['submission_id'], ['_submissions.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('_reagents_submissions',
sa.Column('reagent_id', sa.INTEGER(), nullable=True),
sa.Column('submission_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['reagent_id'], ['_reagents.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submissions.id'], )
)
op.create_table('_submission_sample',
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.String(), nullable=True),
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'], ['_samples.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submissions.id'], ),
sa.PrimaryKeyConstraint('submission_id', 'row', 'column')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_submission_sample')
op.drop_table('_reagents_submissions')
op.drop_table('_control_samples')
op.drop_table('_submissions')
op.drop_table('_submissiontypes_kittypes')
op.drop_table('_reagenttypes_kittypes')
op.drop_table('_reagents')
op.drop_table('_orgs_contacts')
op.drop_table('_discounts')
op.drop_table('_submission_types')
op.drop_table('_samples')
op.drop_table('_reagent_types')
op.drop_table('_organizations')
op.drop_table('_kits')
op.drop_table('_control_types')
op.drop_table('_contacts')
# ### end Alembic commands ###

View File

@@ -1,5 +1,5 @@
''' '''
All database related operations. All database related operations.
''' '''
from .models import *
from .functions import * from .functions import *
from .models import *

View File

@@ -1,10 +1,10 @@
''' '''
Contains all models for sqlalchemy Contains all models for sqlalchemy
''' '''
from .controls import Control, ControlType
# import order must go: orgs, kit, subs due to circular import issues
from .organizations import Organization, Contact
from .kits import KitType, ReagentType, Reagent, Discount, KitTypeReagentTypeAssociation, SubmissionType, SubmissionTypeKitTypeAssociation
from .submissions import (BasicSubmission, BacterialCulture, Wastewater, WastewaterArtic, WastewaterSample, BacterialCultureSample,
BasicSample, SubmissionSampleAssociation, WastewaterAssociation)
from tools import Base
from .controls import *
# import order must go: orgs, kit, subs due to circular import issues
from .organizations import *
from .kits import *
from .submissions import *

View File

@@ -7,7 +7,8 @@ from sqlalchemy.orm import relationship, Query
import logging import logging
from operator import itemgetter from operator import itemgetter
import json import json
from tools import Base, setup_lookup, query_return from . import Base
from tools import setup_lookup, query_return
from datetime import date, datetime from datetime import date, datetime
from typing import List from typing import List
from dateutil.parser import parse from dateutil.parser import parse
@@ -19,7 +20,7 @@ class ControlType(Base):
Base class of a control archetype. Base class of a control archetype.
""" """
__tablename__ = '_control_types' __tablename__ = '_control_types'
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS) name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -58,6 +59,7 @@ class Control(Base):
""" """
__tablename__ = '_control_samples' __tablename__ = '_control_samples'
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type

View File

@@ -2,24 +2,31 @@
All kit and reagent related models All kit and reagent related models
''' '''
from __future__ import annotations from __future__ import annotations
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date from datetime import date
import logging import logging
from tools import check_authorization, Base, setup_lookup, query_return, Report, Result from tools import check_authorization, setup_lookup, query_return, Report, Result
from typing import List from typing import List
from . import Organization from . import Base, Organization
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
reagenttypes_reagents = Table("_reagenttypes_reagents", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("reagenttype_id", INTEGER, ForeignKey("_reagent_types.id"))) reagenttypes_reagents = Table(
"_reagenttypes_reagents",
Base.metadata,
Column("reagent_id", INTEGER, ForeignKey("_reagents.id")),
Column("reagenttype_id", INTEGER, ForeignKey("_reagent_types.id")),
extend_existing = True
)
class KitType(Base): class KitType(Base):
""" """
Base of kits used in submission processing Base of kits used in submission processing
""" """
__tablename__ = "_kits" __tablename__ = "_kits"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit name = Column(String(64), unique=True) #: name of kit
@@ -162,6 +169,7 @@ class ReagentType(Base):
Base of reagent type abstract Base of reagent type abstract
""" """
__tablename__ = "_reagent_types" __tablename__ = "_reagent_types"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type name = Column(String(64)) #: name of reagent type
@@ -251,6 +259,8 @@ class KitTypeReagentTypeAssociation(Base):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
__tablename__ = "_reagenttypes_kittypes" __tablename__ = "_reagenttypes_kittypes"
__table_args__ = {'extend_existing': True}
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True)
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True)
uses = Column(JSON) uses = Column(JSON)
@@ -333,6 +343,7 @@ class Reagent(Base):
Concrete reagent instance Concrete reagent instance
""" """
__tablename__ = "_reagents" __tablename__ = "_reagents"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type
@@ -491,6 +502,7 @@ class Discount(Base):
Relationship table for client labs for certain kits. Relationship table for client labs for certain kits.
""" """
__tablename__ = "_discounts" __tablename__ = "_discounts"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
kit = relationship("KitType") #: joined parent reagent type kit = relationship("KitType") #: joined parent reagent type
@@ -558,12 +570,14 @@ class SubmissionType(Base):
Abstract of types of submissions. Abstract of types of submissions.
""" """
__tablename__ = "_submission_types" __tablename__ = "_submission_types"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(128), unique=True) #: name of submission type name = Column(String(128), unique=True) #: name of submission type
info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type. info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type.
instances = relationship("BasicSubmission", backref="submission_type") instances = relationship("BasicSubmission", backref="submission_type")
# regex = Column(String(512)) # regex = Column(String(512))
template_file = Column(BLOB)
submissiontype_kit_associations = relationship( submissiontype_kit_associations = relationship(
"SubmissionTypeKitTypeAssociation", "SubmissionTypeKitTypeAssociation",
@@ -619,6 +633,8 @@ class SubmissionTypeKitTypeAssociation(Base):
Abstract of relationship between kits and their submission type. Abstract of relationship between kits and their submission type.
""" """
__tablename__ = "_submissiontypes_kittypes" __tablename__ = "_submissiontypes_kittypes"
__table_args__ = {'extend_existing': True}
submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True)
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True)
mutable_cost_column = Column(FLOAT(2)) #: dollar amount per 96 well plate that can change with number of columns (reagents, tips, etc) mutable_cost_column = Column(FLOAT(2)) #: dollar amount per 96 well plate that can change with number of columns (reagents, tips, etc)

View File

@@ -4,20 +4,29 @@ All client organization related models.
from __future__ import annotations from __future__ import annotations
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship, Query from sqlalchemy.orm import relationship, Query
from tools import Base, check_authorization, setup_lookup, query_return from . import Base
from tools import check_authorization, setup_lookup, query_return
from typing import List from typing import List
import logging import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# table containing organization/contact relationship # table containing organization/contact relationship
orgs_contacts = Table("_orgs_contacts", Base.metadata, Column("org_id", INTEGER, ForeignKey("_organizations.id")), Column("contact_id", INTEGER, ForeignKey("_contacts.id"))) orgs_contacts = Table(
"_orgs_contacts",
Base.metadata,
Column("org_id", INTEGER, ForeignKey("_organizations.id")),
Column("contact_id", INTEGER, ForeignKey("_contacts.id")),
# __table_args__ = {'extend_existing': True}
extend_existing = True
)
class Organization(Base): class Organization(Base):
""" """
Base of organization Base of organization
""" """
__tablename__ = "_organizations" __tablename__ = "_organizations"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name name = Column(String(64)) #: organization name
@@ -76,6 +85,7 @@ class Contact(Base):
Base of Contact Base of Contact
""" """
__tablename__ = "_contacts" __tablename__ = "_contacts"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name name = Column(String(64)) #: contact name
@@ -126,4 +136,5 @@ class Contact(Base):
limit = 1 limit = 1
case _: case _:
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)

View File

@@ -17,7 +17,8 @@ import uuid
import re import re
import pandas as pd import pandas as pd
from openpyxl import Workbook from openpyxl import Workbook
from tools import check_not_nan, row_map, Base, query_return, setup_lookup from . import Base
from tools import check_not_nan, row_map, query_return, setup_lookup
from datetime import datetime, date from datetime import datetime, date
from typing import List from typing import List
from dateutil.parser import parse from dateutil.parser import parse
@@ -29,13 +30,21 @@ from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as S
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# table containing reagents/submission relationships # table containing reagents/submission relationships
reagents_submissions = Table("_reagents_submissions", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("submission_id", INTEGER, ForeignKey("_submissions.id"))) reagents_submissions = Table(
"_reagents_submissions",
Base.metadata,
Column("reagent_id", INTEGER, ForeignKey("_reagents.id")),
Column("submission_id", INTEGER, ForeignKey("_submissions.id")),
extend_existing = True
)
class BasicSubmission(Base): class BasicSubmission(Base):
""" """
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
""" """
__tablename__ = "_submissions" __tablename__ = "_submissions"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012) rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
@@ -705,7 +714,6 @@ class Wastewater(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
""" """
# pcr_info = Column(JSON)
ext_technician = Column(String(64)) ext_technician = Column(String(64))
pcr_technician = Column(String(64)) pcr_technician = Column(String(64))
__mapper_args__ = {"polymorphic_identity": "Wastewater", "polymorphic_load": "inline"} __mapper_args__ = {"polymorphic_identity": "Wastewater", "polymorphic_load": "inline"}
@@ -948,6 +956,7 @@ class BasicSample(Base):
""" """
__tablename__ = "_samples" __tablename__ = "_samples"
__table_args__ = {'extend_existing': True}
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
@@ -1226,6 +1235,8 @@ class SubmissionSampleAssociation(Base):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
__tablename__ = "_submission_sample" __tablename__ = "_submission_sample"
__table_args__ = {'extend_existing': True}
sample_id = Column(INTEGER, ForeignKey("_samples.id"), nullable=False) sample_id = Column(INTEGER, ForeignKey("_samples.id"), nullable=False)
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True)
row = Column(INTEGER, primary_key=True) #: row on the 96 well plate row = Column(INTEGER, primary_key=True) #: row on the 96 well plate

View File

@@ -1,7 +1,7 @@
import logging, re import logging, re
from pathlib import Path from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
from backend.db import BasicSubmission, SubmissionType from backend.db.models import BasicSubmission, SubmissionType
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -16,7 +16,7 @@ from backend.validators import PydReagent
# ) # )
from tools import check_if_app, Settings, Report from tools import check_if_app, Settings, Report
from .pop_ups import AlertPop from .pop_ups import AlertPop
from .misc import AddReagentForm from .misc import AddReagentForm, LogParser
import logging import logging
from datetime import date from datetime import date
import webbrowser import webbrowser
@@ -69,7 +69,7 @@ class App(QMainWindow):
menuBar = self.menuBar() menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File") fileMenu = menuBar.addMenu("&File")
# Creating menus using a title # Creating menus using a title
# methodsMenu = menuBar.addMenu("&Methods") methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports") reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly") maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help") helpMenu = menuBar.addMenu("&Help")
@@ -77,7 +77,7 @@ class App(QMainWindow):
helpMenu.addAction(self.docsAction) helpMenu.addAction(self.docsAction)
fileMenu.addAction(self.importAction) fileMenu.addAction(self.importAction)
fileMenu.addAction(self.importPCRAction) fileMenu.addAction(self.importPCRAction)
# methodsMenu.addAction(self.constructFS) methodsMenu.addAction(self.searchLog)
reportMenu.addAction(self.generateReportAction) reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction) maintenanceMenu.addAction(self.joinPCRAction)
@@ -108,7 +108,7 @@ class App(QMainWindow):
self.joinPCRAction = QAction("Link PCR Logs") self.joinPCRAction = QAction("Link PCR Logs")
self.helpAction = QAction("&About", self) self.helpAction = QAction("&About", self)
self.docsAction = QAction("&Docs", self) self.docsAction = QAction("&Docs", self)
# self.constructFS = QAction("Make First Strand", self) self.searchLog = QAction("Search Log", self)
def _connectActions(self): def _connectActions(self):
""" """
@@ -127,6 +127,7 @@ class App(QMainWindow):
self.docsAction.triggered.connect(self.openDocs) self.docsAction.triggered.connect(self.openDocs)
# self.constructFS.triggered.connect(self.construct_first_strand) # self.constructFS.triggered.connect(self.construct_first_strand)
# self.table_widget.formwidget.import_drag.connect(self.importSubmission) # self.table_widget.formwidget.import_drag.connect(self.importSubmission)
self.searchLog.triggered.connect(self.runSearch)
def showAbout(self): def showAbout(self):
""" """
@@ -335,13 +336,16 @@ class App(QMainWindow):
# # from .main_window_functions import export_csv_function # # from .main_window_functions import export_csv_function
# export_csv_function(self, fname) # export_csv_function(self, fname)
def runSearch(self):
dlg = LogParser(self)
dlg.exec()
class AddSubForm(QWidget): class AddSubForm(QWidget):
def __init__(self, parent:QWidget): def __init__(self, parent:QWidget):
logger.debug(f"Initializating subform...") logger.debug(f"Initializating subform...")
super(QWidget, self).__init__(parent) super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.parent = parent
# Initialize tab screen # Initialize tab screen
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.tab1 = QWidget() self.tab1 = QWidget()

View File

@@ -17,7 +17,7 @@ class ControlsViewer(QWidget):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent) super().__init__(parent)
self.app = self.parent().parent self.app = self.parent().parent()
print(f"\n\n{self.app}\n\n") print(f"\n\n{self.app}\n\n")
self.report = Report() self.report = Report()
self.datepicker = ControlsDatePicker() self.datepicker = ControlsDatePicker()

View File

@@ -28,7 +28,7 @@ def select_open_file(obj:QMainWindow, file_extension:str) -> Path:
home_dir = obj.app.last_dir.resolve().__str__() home_dir = obj.app.last_dir.resolve().__str__()
fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0]) fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0])
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', filter = f"{file_extension}(*.{file_extension})")[0]) # fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', filter = f"{file_extension}(*.{file_extension})")[0])
obj.last_file = fname obj.last_dir = fname.parent
return fname return fname
def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path: def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:

View File

@@ -2,27 +2,18 @@
Contains miscellaneous widgets for frontend functions Contains miscellaneous widgets for frontend functions
''' '''
from datetime import date from datetime import date
from pprint import pformat
from PyQt6 import QtCore
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog, QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget, QDialogButtonBox, QDateEdit, QPushButton, QFormLayout
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
QHBoxLayout, QScrollArea, QFormLayout
) )
from PyQt6.QtCore import Qt, QDate, QSize, pyqtSignal from PyQt6.QtCore import Qt, QDate
from tools import check_not_nan, jinja_template_loading, Settings, Result from tools import jinja_template_loading, Settings
from backend.db.models import * from backend.db.models import *
from sqlalchemy import FLOAT, INTEGER
import logging import logging
import numpy as np from .pop_ups import AlertPop
from .pop_ups import AlertPop, QuestionAsker from .functions import select_open_file
from backend.validators import PydReagent, PydKit, PydReagentType, PydSubmission from tools import readInChunks
from typing import Tuple, List
from pprint import pformat
import difflib
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -137,235 +128,6 @@ class ReportDatePicker(QDialog):
def parse_form(self): def parse_form(self):
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate()) return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
# class KitAdder(QWidget):
# """
# dialog to get information to add kit
# """
# def __init__(self) -> None:
# super().__init__()
# # self.ctx = parent_ctx
# main_box = QVBoxLayout(self)
# scroll = QScrollArea(self)
# main_box.addWidget(scroll)
# scroll.setWidgetResizable(True)
# scrollContent = QWidget(scroll)
# self.grid = QGridLayout()
# # self.setLayout(self.grid)
# scrollContent.setLayout(self.grid)
# # insert submit button at top
# self.submit_btn = QPushButton("Submit")
# self.grid.addWidget(self.submit_btn,0,0,1,1)
# self.grid.addWidget(QLabel("Kit Name:"),2,0)
# # widget to get kit name
# kit_name = QLineEdit()
# kit_name.setObjectName("kit_name")
# self.grid.addWidget(kit_name,2,1)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# # widget to get uses of kit
# used_for = QComboBox()
# used_for.setObjectName("used_for")
# # Insert all existing sample types
# # used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
# used_for.addItems([item.name for item in SubmissionType.query()])
# used_for.setEditable(True)
# self.grid.addWidget(used_for,3,1)
# # Get all fields in SubmissionTypeKitTypeAssociation
# self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
# for iii, column in enumerate(self.columns):
# idx = iii + 4
# # convert field name to human readable.
# field_name = column.name.replace("_", " ").title()
# self.grid.addWidget(QLabel(field_name),idx,0)
# match column.type:
# case FLOAT():
# add_widget = QDoubleSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case INTEGER():
# add_widget = QSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case _:
# add_widget = QLineEdit()
# add_widget.setObjectName(column.name)
# self.grid.addWidget(add_widget, idx,1)
# self.add_RT_btn = QPushButton("Add Reagent Type")
# self.grid.addWidget(self.add_RT_btn)
# self.add_RT_btn.clicked.connect(self.add_RT)
# self.submit_btn.clicked.connect(self.submit)
# scroll.setWidget(scrollContent)
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn"
# ]
# def add_RT(self) -> None:
# """
# insert new reagent type row
# """
# # get bottommost row
# maxrow = self.grid.rowCount()
# reg_form = ReagentTypeForm()
# reg_form.setObjectName(f"ReagentForm_{maxrow}")
# # self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
# self.grid.addWidget(reg_form, maxrow,0,1,4)
# def submit(self) -> None:
# """
# send kit to database
# """
# # get form info
# info, reagents = self.parse_form()
# # info, reagents = extract_form_info(self)
# info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
# logger.debug(f"kit info: {pformat(info)}")
# logger.debug(f"kit reagents: {pformat(reagents)}")
# info['reagent_types'] = reagents
# logger.debug(pformat(info))
# # send to kit constructor
# kit = PydKit(name=info['kit_name'])
# for reagent in info['reagent_types']:
# uses = {
# info['used_for']:
# {'sheet':reagent['sheet'],
# 'name':reagent['name'],
# 'lot':reagent['lot'],
# 'expiry':reagent['expiry']
# }}
# kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
# logger.debug(f"Output pyd object: {kit.__dict__}")
# # result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
# sqlobj, result = kit.toSQL(self.ctx)
# sqlobj.save()
# msg = AlertPop(message=result['message'], status=result['status'])
# msg.exec()
# self.__init__(self.ctx)
# def parse_form(self) -> Tuple[dict, list]:
# logger.debug(f"Hello from {self.__class__} parser!")
# info = {}
# reagents = []
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
# for widget in widgets:
# # logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
# match widget:
# case ReagentTypeForm():
# reagents.append(widget.parse_form())
# case QLineEdit():
# info[widget.objectName()] = widget.text()
# case QComboBox():
# info[widget.objectName()] = widget.currentText()
# case QDateEdit():
# info[widget.objectName()] = widget.date().toPyDate()
# return info, reagents
# class ReagentTypeForm(QWidget):
# """
# custom widget to add information about a new reagenttype
# """
# def __init__(self) -> None:
# super().__init__()
# grid = QGridLayout()
# self.setLayout(grid)
# grid.addWidget(QLabel("Reagent Type Name"),0,0)
# # Widget to get reagent info
# self.reagent_getter = QComboBox()
# self.reagent_getter.setObjectName("rtname")
# # lookup all reagent type names from db
# # lookup = lookup_reagent_types(ctx=ctx)
# lookup = ReagentType.query()
# logger.debug(f"Looked up ReagentType names: {lookup}")
# self.reagent_getter.addItems([item.__str__() for item in lookup])
# self.reagent_getter.setEditable(True)
# grid.addWidget(self.reagent_getter,0,1)
# grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# # widget to get extension of life
# self.eol = QSpinBox()
# self.eol.setObjectName('eol')
# self.eol.setMinimum(0)
# grid.addWidget(self.eol, 0,3)
# grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
# self.location_sheet_name = QLineEdit()
# self.location_sheet_name.setObjectName("sheet")
# self.location_sheet_name.setText("e.g. 'Reagent Info'")
# grid.addWidget(self.location_sheet_name, 1,1)
# for iii, item in enumerate(["Name", "Lot", "Expiry"]):
# idx = iii + 2
# grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
# row = QSpinBox()
# row.setFixedWidth(50)
# row.setObjectName(f'{item.lower()}_row')
# row.setMinimum(0)
# grid.addWidget(row, idx, 1)
# grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
# col = QSpinBox()
# col.setFixedWidth(50)
# col.setObjectName(f'{item.lower()}_column')
# col.setMinimum(0)
# grid.addWidget(col, idx, 3)
# self.setFixedHeight(175)
# max_row = grid.rowCount()
# self.r_button = QPushButton("Remove")
# self.r_button.clicked.connect(self.remove)
# grid.addWidget(self.r_button,max_row,0,1,1)
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
# ]
# def remove(self):
# self.setParent(None)
# self.destroy()
# def parse_form(self) -> dict:
# logger.debug(f"Hello from {self.__class__} parser!")
# info = {}
# info['eol'] = self.eol.value()
# info['sheet'] = self.location_sheet_name.text()
# info['rtname'] = self.reagent_getter.currentText()
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
# for widget in widgets:
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
# match widget:
# case QLineEdit():
# info[widget.objectName()] = widget.text()
# case QComboBox():
# info[widget.objectName()] = widget.currentText()
# case QDateEdit():
# info[widget.objectName()] = widget.date().toPyDate()
# case QSpinBox() | QDoubleSpinBox():
# if "_" in widget.objectName():
# key, sub_key = widget.objectName().split("_")
# if key not in info.keys():
# info[key] = {}
# logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
# info[key][sub_key] = widget.value()
# return info
# class ControlsDatePicker(QWidget):
# """
# custom widget to pick start and end dates for controls graphs
# """
# def __init__(self) -> None:
# super().__init__()
# self.start_date = QDateEdit(calendarPopup=True)
# # start date is two months prior to end date by default
# twomonthsago = QDate.currentDate().addDays(-60)
# self.start_date.setDate(twomonthsago)
# self.end_date = QDateEdit(calendarPopup=True)
# self.end_date.setDate(QDate.currentDate())
# self.layout = QHBoxLayout()
# self.layout.addWidget(QLabel("Start Date"))
# self.layout.addWidget(self.start_date)
# self.layout.addWidget(QLabel("End Date"))
# self.layout.addWidget(self.end_date)
# self.setLayout(self.layout)
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# def sizeHint(self) -> QSize:
# return QSize(80,20)
class FirstStrandSalvage(QDialog): class FirstStrandSalvage(QDialog):
def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None: def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None:
@@ -429,321 +191,46 @@ class FirstStrandPlateList(QDialog):
output.append(plate.currentText()) output.append(plate.currentText())
return output return output
# class ReagentFormWidget(QWidget): class LogParser(QDialog):
# def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str): def __init__(self, parent):
# super().__init__(parent) super().__init__(parent)
# # self.setParent(parent) self.app = self.parent()
# self.reagent = reagent self.filebutton = QPushButton(self)
# self.extraction_kit = extraction_kit self.filebutton.setText("Import File")
# # self.ctx = reagent.ctx self.phrase_looker = QComboBox(self)
# layout = QVBoxLayout() self.phrase_looker.setEditable(True)
# self.label = self.ReagentParsedLabel(reagent=reagent) self.btn = QPushButton(self)
# layout.addWidget(self.label) self.btn.setText("Search")
# self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit) self.layout = QFormLayout(self)
# layout.addWidget(self.lot) self.layout.addRow(self.tr("&File:"), self.filebutton)
# # Remove spacing between reagents self.layout.addRow(self.tr("&Search Term:"), self.phrase_looker)
# layout.setContentsMargins(0,0,0,0) self.layout.addRow(self.btn)
# self.setLayout(layout) self.filebutton.clicked.connect(self.filelookup)
# self.setObjectName(reagent.name) self.btn.clicked.connect(self.runsearch)
# self.missing = reagent.missing self.setMinimumWidth(400)
# # If changed set self.missing to True and update self.label
# self.lot.currentTextChanged.connect(self.updated)
# def parse_form(self) -> Tuple[PydReagent, dict]:
# lot = self.lot.currentText()
# # wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
# wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
# # if reagent doesn't exist in database, off to add it (uses App.add_reagent)
# if wanted_reagent == None:
# dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
# if dlg.exec():
# wanted_reagent = self.parent().parent().parent().parent().parent().parent().parent().parent().parent.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
# return wanted_reagent, None
# else:
# # 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")
# else:
# # 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.
# # rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
# # rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
# rt = ReagentType.query(name=self.reagent.type)
# if rt == None:
# # rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
# rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
# return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
# def updated(self):
# self.missing = True
# self.label.updated(self.reagent.type)
# class ReagentParsedLabel(QLabel): def filelookup(self):
self.fname = select_open_file(self, "tabular")
# def __init__(self, reagent:PydReagent):
# super().__init__()
# try:
# check = not reagent.missing
# except:
# check = False
# self.setObjectName(f"{reagent.type}_label")
# if check:
# self.setText(f"Parsed {reagent.type}")
# else:
# self.setText(f"MISSING {reagent.type}")
# def updated(self, reagent_type:str):
# self.setText(f"UPDATED {reagent_type}")
# class ReagentLot(QComboBox): def runsearch(self):
count: int = 0
# def __init__(self, reagent, extraction_kit:str) -> None: total: int = 0
# super().__init__() logger.debug(f"Current search term: {self.phrase_looker.currentText()}")
# # self.ctx = reagent.ctx try:
# self.setEditable(True) with open(self.fname, "r") as f:
# # if reagent.parsed: for chunk in readInChunks(fileObj=f):
# # pass total += len(chunk)
# logger.debug(f"Attempting lookup of reagents by type: {reagent.type}") for line in chunk:
# # below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work. if self.phrase_looker.currentText().lower() in line.lower():
# # lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type) count += 1
# lookup = Reagent.query(reagent_type=reagent.type) percent = (count/total)*100
# relevant_reagents = [item.__str__() for item in lookup] msg = f"I found {count} instances of the search phrase out of {total} = {percent:.2f}%."
# output_reg = [] status = "Information"
# for rel_reagent in relevant_reagents: except AttributeError:
# # extract strings from any sets. msg = f"No file was selected."
# if isinstance(rel_reagent, set): status = "Error"
# for thing in rel_reagent: dlg = AlertPop(message=msg, status=status)
# output_reg.append(thing) dlg.exec()
# elif isinstance(rel_reagent, str):
# output_reg.append(rel_reagent)
# relevant_reagents = output_reg
# # if reagent in sheet is not found insert it into the front of relevant reagents so it shows
# logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}")
# if str(reagent.lot) not in relevant_reagents:
# if check_not_nan(reagent.lot):
# relevant_reagents.insert(0, str(reagent.lot))
# else:
# # TODO: look up the last used reagent of this type in the database
# # looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
# looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
# try:
# # looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
# looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
# except AttributeError:
# looked_up_reg = None
# logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
# if looked_up_reg != None:
# relevant_reagents.remove(str(looked_up_reg.lot))
# relevant_reagents.insert(0, str(looked_up_reg.lot))
# else:
# if len(relevant_reagents) > 1:
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
# idx = relevant_reagents.index(str(reagent.lot))
# logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}")
# moved_reag = relevant_reagents.pop(idx)
# relevant_reagents.insert(0, moved_reag)
# else:
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
# logger.debug(f"New relevant reagents: {relevant_reagents}")
# self.setObjectName(f"lot_{reagent.type}")
# self.addItems(relevant_reagents)
# class SubmissionFormWidget(QWidget):
# def __init__(self, parent: QWidget, **kwargs) -> None:
# super().__init__(parent)
# # self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# # "qt_scrollarea_vcontainer", "submit_btn"
# # ]
# self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx']
# layout = QVBoxLayout()
# for k, v in kwargs.items():
# if k not in self.ignore:
# add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type'])
# if add_widget != None:
# layout.addWidget(add_widget)
# else:
# setattr(self, k, v)
# self.setLayout(layout)
# def create_widget(self, key:str, value:dict, submission_type:str|None=None):
# if key not in self.ignore:
# return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
# return None
# def clear_form(self):
# for item in self.findChildren(QWidget):
# item.setParent(None)
# def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
# query = self.findChildren(QWidget)
# if object_name != None:
# query = [widget for widget in query if widget.objectName()==object_name]
# return query
# def parse_form(self) -> PydSubmission:
# logger.debug(f"Hello from form parser!")
# info = {}
# reagents = []
# if hasattr(self, 'csv'):
# info['csv'] = self.csv
# for widget in self.findChildren(QWidget):
# # logger.debug(f"Parsed widget of type {type(widget)}")
# match widget:
# case ReagentFormWidget():
# reagent, _ = widget.parse_form()
# if reagent != None:
# reagents.append(reagent)
# case self.InfoItem():
# field, value = widget.parse_form()
# if field != None:
# info[field] = value
# logger.debug(f"Info: {pformat(info)}")
# logger.debug(f"Reagents: {pformat(reagents)}")
# # app = self.parent().parent().parent().parent().parent().parent().parent().parent
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
# return submission
# class InfoItem(QWidget):
# def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|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['value'])
# self.setObjectName(key)
# try:
# self.missing:bool = value['missing']
# except (TypeError, KeyError):
# self.missing:bool = True
# if self.input != None:
# layout.addWidget(self.label)
# layout.addWidget(self.input)
# layout.setContentsMargins(0,0,0,0)
# self.setLayout(layout)
# match self.input:
# case QComboBox():
# self.input.currentTextChanged.connect(self.update_missing)
# case QDateEdit():
# self.input.dateChanged.connect(self.update_missing)
# case QLineEdit():
# self.input.textChanged.connect(self.update_missing)
# def parse_form(self):
# match self.input:
# case QLineEdit():
# value = self.input.text()
# case QComboBox():
# value = self.input.currentText()
# case QDateEdit():
# value = self.input.date().toPyDate()
# case _:
# 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) -> QWidget:
# try:
# value = value['value']
# except (TypeError, KeyError):
# pass
# obj = parent.parent().parent()
# logger.debug(f"Creating widget for: {key}")
# match key:
# case 'submitting_lab':
# add_widget = QComboBox()
# # lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
# labs = [item.__str__() for item in Organization.query()]
# # try to set closest match to top of list
# try:
# labs = difflib.get_close_matches(value, labs, len(labs), 0)
# except (TypeError, ValueError):
# pass
# # set combobox values to lookedup values
# add_widget.addItems(labs)
# case 'extraction_kit':
# # if extraction kit not available, all other values fail
# if not check_not_nan(value):
# msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
# msg.exec()
# # create combobox to hold looked up kits
# add_widget = QComboBox()
# # lookup existing kits by 'submission_type' decided on by sheetparser
# logger.debug(f"Looking up kits used for {submission_type}")
# uses = [item.__str__() for item in KitType.query(used_for=submission_type)]
# obj.uses = uses
# logger.debug(f"Kits received for {submission_type}: {uses}")
# if check_not_nan(value):
# logger.debug(f"The extraction kit in parser was: {value}")
# uses.insert(0, uses.pop(uses.index(value)))
# obj.ext_kit = value
# else:
# logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
# obj.ext_kit = uses[0]
# add_widget.addItems(uses)
# # Run reagent scraper whenever extraction kit is changed.
# # add_widget.currentTextChanged.connect(obj.scrape_reagents)
# case 'submitted_date':
# # uses base calendar
# add_widget = QDateEdit(calendarPopup=True)
# # sets submitted date based on date found in excel sheet
# try:
# add_widget.setDate(value)
# # if not found, use today
# except:
# add_widget.setDate(date.today())
# case 'submission_category':
# add_widget = QComboBox()
# cats = ['Diagnostic', "Surveillance", "Research"]
# # cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
# cats += [item.name for item in SubmissionType.query()]
# try:
# cats.insert(0, cats.pop(cats.index(value)))
# except ValueError:
# cats.insert(0, cats.pop(cats.index(submission_type)))
# add_widget.addItems(cats)
# case _:
# # 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("_", " "))
# if add_widget != None:
# add_widget.setObjectName(key)
# add_widget.setParent(parent)
# return add_widget
# def update_missing(self):
# self.missing = True
# self.label.updated(self.objectName())
# class ParsedQLabel(QLabel):
# def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
# super().__init__()
# try:
# check = not value['missing']
# except:
# check = True
# if label_name != None:
# self.setObjectName(label_name)
# else:
# self.setObjectName(f"{key}_label")
# if title:
# output = key.replace('_', ' ').title()
# else:
# output = key.replace('_', ' ')
# if check:
# self.setText(f"Parsed {output}")
# else:
# self.setText(f"MISSING {output}")
# def updated(self, key:str, title:bool=True):
# if title:
# output = key.replace('_', ' ').title()
# else:
# output = key.replace('_', ' ')
# self.setText(f"UPDATED {output}")

View File

@@ -12,7 +12,7 @@ from backend.excel.parser import SheetParser, PCRParser
from backend.validators import PydSubmission, PydReagent from backend.validators import PydSubmission, PydReagent
from backend.db import ( from backend.db import (
check_kit_integrity, KitType, Organization, SubmissionType, Reagent, check_kit_integrity, KitType, Organization, SubmissionType, Reagent,
ReagentType, KitTypeReagentTypeAssociation, BasicSubmission, update_subsampassoc_with_pcr ReagentType, KitTypeReagentTypeAssociation, BasicSubmission
) )
from pprint import pformat from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop from .pop_ups import QuestionAsker, AlertPop
@@ -22,7 +22,6 @@ import difflib
from datetime import date from datetime import date
import inspect import inspect
import json import json
import sys
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -224,7 +224,6 @@ class Settings(BaseSettings):
engine = create_engine(f"sqlite:///{database_path}")#, echo=True, future=True) engine = create_engine(f"sqlite:///{database_path}")#, echo=True, future=True)
session = Session(engine) session = Session(engine)
metadata.session = session metadata.session = session
return session return session
@field_validator('package', mode="before") @field_validator('package', mode="before")
@@ -513,4 +512,14 @@ class Report(BaseModel):
case _: case _:
pass pass
def readInChunks(fileObj, chunkSize=2048):
"""
Lazy function to read a file piece by piece.
Default chunk size: 2kB.
"""
while True:
data = fileObj.readlines(chunkSize)
if not data:
break
yield data