Moments before disaster.
This commit is contained in:
46
alembic/versions/10c47a04559d_adding_in_processes.py
Normal file
46
alembic/versions/10c47a04559d_adding_in_processes.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Adding in processes
|
||||||
|
|
||||||
|
Revision ID: 10c47a04559d
|
||||||
|
Revises: 94289d4e63e6
|
||||||
|
Create Date: 2024-01-05 13:25:02.468436
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '10c47a04559d'
|
||||||
|
down_revision = '94289d4e63e6'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('_process',
|
||||||
|
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('_equipmentroles_processes',
|
||||||
|
sa.Column('process_id', sa.INTEGER(), nullable=True),
|
||||||
|
sa.Column('equipmentroles_id', sa.INTEGER(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_equipment_roles.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
|
||||||
|
)
|
||||||
|
op.create_table('_submissiontypes_processes',
|
||||||
|
sa.Column('process_id', sa.INTEGER(), nullable=True),
|
||||||
|
sa.Column('equipmentroles_id', sa.INTEGER(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_submission_types.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('_submissiontypes_processes')
|
||||||
|
op.drop_table('_equipmentroles_processes')
|
||||||
|
op.drop_table('_process')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Adjusting process-submissionequipassoc
|
||||||
|
|
||||||
|
Revision ID: 67fa77849024
|
||||||
|
Revises: e08a69a0f381
|
||||||
|
Create Date: 2024-01-05 15:06:24.305945
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '67fa77849024'
|
||||||
|
down_revision = 'e08a69a0f381'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('process_id', sa.INTEGER(), nullable=True))
|
||||||
|
# batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key('SEA_Process_id', '_process', ['process_id'], ['id'], ondelete='SET NULL')
|
||||||
|
batch_op.drop_column('process')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('process', sa.VARCHAR(length=64), nullable=True))
|
||||||
|
batch_op.drop_constraint('SEA_Process_id', type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key(None, '_process', ['process'], ['id'], ondelete='SET NULL')
|
||||||
|
batch_op.drop_column('process_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
32
alembic/versions/e08a69a0f381_attaching_process_to_.py
Normal file
32
alembic/versions/e08a69a0f381_attaching_process_to_.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""Attaching Process to SubmissionEquipmentAssociation
|
||||||
|
|
||||||
|
Revision ID: e08a69a0f381
|
||||||
|
Revises: 10c47a04559d
|
||||||
|
Create Date: 2024-01-05 14:50:55.681167
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e08a69a0f381'
|
||||||
|
down_revision = '10c47a04559d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.create_foreign_key('SEA_Process_id', '_process', ['process'], ['id'], ondelete='SET NULL')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_equipment_submissions', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('SEA_Process_id', type_='foreignkey')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -4,9 +4,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Version of the realpython-reader package
|
# Version of the realpython-reader package
|
||||||
__project__ = "submissions"
|
__project__ = "submissions"
|
||||||
__version__ = "202312.4b"
|
__version__ = "202401.1b"
|
||||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||||
__copyright__ = "2022-2023, Government of Canada"
|
__copyright__ = "2022-2024, Government of Canada"
|
||||||
|
|
||||||
project_path = Path(__file__).parents[2].absolute()
|
project_path = Path(__file__).parents[2].absolute()
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,22 @@ equipmentroles_equipment = Table(
|
|||||||
extend_existing=True
|
extend_existing=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
equipmentroles_processes = Table(
|
||||||
|
"_equipmentroles_processes",
|
||||||
|
Base.metadata,
|
||||||
|
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||||
|
Column("equipmentroles_id", INTEGER, ForeignKey("_equipment_roles.id")),
|
||||||
|
extend_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
submissiontypes_processes = Table(
|
||||||
|
"_submissiontypes_processes",
|
||||||
|
Base.metadata,
|
||||||
|
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||||
|
Column("equipmentroles_id", INTEGER, ForeignKey("_submission_types.id")),
|
||||||
|
extend_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
class KitType(BaseClass):
|
class KitType(BaseClass):
|
||||||
"""
|
"""
|
||||||
Base of kits used in submission processing
|
Base of kits used in submission processing
|
||||||
@@ -588,6 +604,7 @@ class SubmissionType(BaseClass):
|
|||||||
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
|
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
|
||||||
# regex = Column(String(512))
|
# regex = Column(String(512))
|
||||||
template_file = Column(BLOB) #: Blank form for this type stored as binary.
|
template_file = Column(BLOB) #: Blank form for this type stored as binary.
|
||||||
|
processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes)
|
||||||
|
|
||||||
submissiontype_kit_associations = relationship(
|
submissiontype_kit_associations = relationship(
|
||||||
"SubmissionTypeKitTypeAssociation",
|
"SubmissionTypeKitTypeAssociation",
|
||||||
@@ -855,7 +872,10 @@ class Equipment(BaseClass):
|
|||||||
return f"<Equipment({self.name})>"
|
return f"<Equipment({self.name})>"
|
||||||
|
|
||||||
def get_processes(self, submission_type:SubmissionType):
|
def get_processes(self, submission_type:SubmissionType):
|
||||||
return [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
|
processes = [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
|
||||||
|
if len(processes) == 0:
|
||||||
|
processes = ['']
|
||||||
|
return processes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
@@ -888,7 +908,8 @@ class Equipment(BaseClass):
|
|||||||
|
|
||||||
def to_pydantic(self, submission_type:SubmissionType):
|
def to_pydantic(self, submission_type:SubmissionType):
|
||||||
from backend.validators.pydant import PydEquipment
|
from backend.validators.pydant import PydEquipment
|
||||||
return PydEquipment(processes=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
|
# return PydEquipment(process=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
|
||||||
|
return PydEquipment(process=None, role=None, **self.__dict__)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.__database_session__.add(self)
|
self.__database_session__.add(self)
|
||||||
@@ -911,6 +932,7 @@ class EquipmentRole(BaseClass):
|
|||||||
id = Column(INTEGER, primary_key=True)
|
id = Column(INTEGER, primary_key=True)
|
||||||
name = Column(String(32))
|
name = Column(String(32))
|
||||||
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
|
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
|
||||||
|
processes = relationship("Process", back_populates="equipment_roles", secondary=equipmentroles_processes)
|
||||||
|
|
||||||
equipmentrole_submissiontype_associations = relationship(
|
equipmentrole_submissiontype_associations = relationship(
|
||||||
"SubmissionTypeEquipmentRoleAssociation",
|
"SubmissionTypeEquipmentRoleAssociation",
|
||||||
@@ -926,7 +948,9 @@ class EquipmentRole(BaseClass):
|
|||||||
def to_pydantic(self, submission_type:SubmissionType):
|
def to_pydantic(self, submission_type:SubmissionType):
|
||||||
from backend.validators.pydant import PydEquipmentRole
|
from backend.validators.pydant import PydEquipmentRole
|
||||||
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
|
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
|
||||||
return PydEquipmentRole(equipment=equipment, **self.__dict__)
|
pyd_dict = self.__dict__
|
||||||
|
pyd_dict['processes'] = self.get_processes(submission_type=submission_type)
|
||||||
|
return PydEquipmentRole(equipment=equipment, **pyd_dict)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
@@ -946,6 +970,25 @@ class EquipmentRole(BaseClass):
|
|||||||
pass
|
pass
|
||||||
return query_return(query=query, limit=limit)
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
|
def get_processes(self, submission_type:str|SubmissionType|None) -> List[Process]:
|
||||||
|
if isinstance(submission_type, str):
|
||||||
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
|
if submission_type != None:
|
||||||
|
output = [process.name for process in self.processes if submission_type in process.submission_types]
|
||||||
|
else:
|
||||||
|
output = [process.name for process in self.processes]
|
||||||
|
if len(output) == 0:
|
||||||
|
return ['']
|
||||||
|
else:
|
||||||
|
return output
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
try:
|
||||||
|
self.__database_session__.add(self)
|
||||||
|
self.__database_session__.commit()
|
||||||
|
except:
|
||||||
|
self.__database_session__.rollback()
|
||||||
|
|
||||||
class SubmissionEquipmentAssociation(BaseClass):
|
class SubmissionEquipmentAssociation(BaseClass):
|
||||||
|
|
||||||
# Currently abstract until ready to implement
|
# Currently abstract until ready to implement
|
||||||
@@ -956,7 +999,8 @@ class SubmissionEquipmentAssociation(BaseClass):
|
|||||||
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
||||||
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
|
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
|
||||||
role = Column(String(64), primary_key=True) #: name of the role the equipment fills
|
role = Column(String(64), primary_key=True) #: name of the role the equipment fills
|
||||||
process = Column(String(64)) #: name of the process run on this equipment
|
# process = Column(String(64)) #: name of the process run on this equipment
|
||||||
|
process_id = Column(INTEGER, ForeignKey("_process.id",ondelete="SET NULL", name="SEA_Process_id"))
|
||||||
start_time = Column(TIMESTAMP)
|
start_time = Column(TIMESTAMP)
|
||||||
end_time = Column(TIMESTAMP)
|
end_time = Column(TIMESTAMP)
|
||||||
comments = Column(String(1024))
|
comments = Column(String(1024))
|
||||||
@@ -970,7 +1014,7 @@ class SubmissionEquipmentAssociation(BaseClass):
|
|||||||
self.equipment = equipment
|
self.equipment = equipment
|
||||||
|
|
||||||
def to_sub_dict(self) -> dict:
|
def to_sub_dict(self) -> dict:
|
||||||
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=[self.process], role=self.role, nickname=self.equipment.nickname)
|
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=self.process.name, role=self.role, nickname=self.equipment.nickname)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@@ -1021,3 +1065,149 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
|
|||||||
self.__database_session__.add(self)
|
self.__database_session__.add(self)
|
||||||
self.__database_session__.commit()
|
self.__database_session__.commit()
|
||||||
|
|
||||||
|
class Process(BaseClass):
|
||||||
|
|
||||||
|
__tablename__ = "_process"
|
||||||
|
|
||||||
|
id = Column(INTEGER, primary_key=True)
|
||||||
|
name = Column(String(64))
|
||||||
|
submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes)
|
||||||
|
equipment_roles = relationship("EquipmentRole", back_populates='processes', secondary=equipmentroles_processes)
|
||||||
|
submissions = relationship("SubmissionEquipmentAssociation", backref='process')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Process({self.name})"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@setup_lookup
|
||||||
|
def query(cls, name:str|None, limit:int=0):
|
||||||
|
query = cls.__database_session__.query(cls)
|
||||||
|
match name:
|
||||||
|
case str():
|
||||||
|
query = query.filter(cls.name==name)
|
||||||
|
limit = 1
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
try:
|
||||||
|
self.__database_session__.add(self)
|
||||||
|
self.__database_session__.commit()
|
||||||
|
except:
|
||||||
|
self.__database_session__.rollback()
|
||||||
|
|
||||||
|
# class KitTypeReagentTypeAssociation(BaseClass):
|
||||||
|
# """
|
||||||
|
# table containing reagenttype/kittype associations
|
||||||
|
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||||
|
# """
|
||||||
|
# __tablename__ = "_reagenttypes_kittypes"
|
||||||
|
|
||||||
|
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
|
||||||
|
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
|
||||||
|
# uses = Column(JSON) #: map to location on excel sheets of different submission types
|
||||||
|
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
|
||||||
|
# last_used = Column(String(32)) #: last used lot number of this type of reagent
|
||||||
|
|
||||||
|
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
|
||||||
|
|
||||||
|
# # reference to the "ReagentType" object
|
||||||
|
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
|
||||||
|
|
||||||
|
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
|
||||||
|
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
|
||||||
|
# self.kit_type = kit_type
|
||||||
|
# self.reagent_type = reagent_type
|
||||||
|
# self.uses = uses
|
||||||
|
# self.required = required
|
||||||
|
|
||||||
|
# def __repr__(self) -> str:
|
||||||
|
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
|
||||||
|
|
||||||
|
# @validates('required')
|
||||||
|
# def validate_age(self, key, value):
|
||||||
|
# """
|
||||||
|
# Ensures only 1 & 0 used in 'required'
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# key (str): name of attribute
|
||||||
|
# value (_type_): value of attribute
|
||||||
|
|
||||||
|
# Raises:
|
||||||
|
# ValueError: Raised if bad value given
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# _type_: value
|
||||||
|
# """
|
||||||
|
# if not 0 <= value < 2:
|
||||||
|
# raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
||||||
|
# return value
|
||||||
|
|
||||||
|
# @validates('reagenttype')
|
||||||
|
# def validate_reagenttype(self, key, value):
|
||||||
|
# """
|
||||||
|
# Ensures reagenttype is an actual ReagentType
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# key (str)): name of attribute
|
||||||
|
# value (_type_): value of attribute
|
||||||
|
|
||||||
|
# Raises:
|
||||||
|
# ValueError: raised if reagenttype is not a ReagentType
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# _type_: ReagentType
|
||||||
|
# """
|
||||||
|
# if not isinstance(value, ReagentType):
|
||||||
|
# raise ValueError(f'{value} is not a reagenttype')
|
||||||
|
# return value
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# @setup_lookup
|
||||||
|
# def query(cls,
|
||||||
|
# kit_type:KitType|str|None=None,
|
||||||
|
# reagent_type:ReagentType|str|None=None,
|
||||||
|
# limit:int=0
|
||||||
|
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
|
||||||
|
# """
|
||||||
|
# Lookup junction of ReagentType and KitType
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# kit_type (models.KitType | str | None): KitType of interest.
|
||||||
|
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
|
||||||
|
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
|
||||||
|
# """
|
||||||
|
# query: Query = cls.__database_session__.query(cls)
|
||||||
|
# match kit_type:
|
||||||
|
# case KitType():
|
||||||
|
# query = query.filter(cls.kit_type==kit_type)
|
||||||
|
# case str():
|
||||||
|
# query = query.join(KitType).filter(KitType.name==kit_type)
|
||||||
|
# case _:
|
||||||
|
# pass
|
||||||
|
# match reagent_type:
|
||||||
|
# case ReagentType():
|
||||||
|
# query = query.filter(cls.reagent_type==reagent_type)
|
||||||
|
# case str():
|
||||||
|
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
|
||||||
|
# case _:
|
||||||
|
# pass
|
||||||
|
# if kit_type != None and reagent_type != None:
|
||||||
|
# limit = 1
|
||||||
|
# return query_return(query=query, limit=limit)
|
||||||
|
|
||||||
|
# def save(self) -> Report:
|
||||||
|
# """
|
||||||
|
# Adds this instance to the database and commits.
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# Report: Result of save action
|
||||||
|
# """
|
||||||
|
# report = Report()
|
||||||
|
# self.__database_session__.add(self)
|
||||||
|
# self.__database_session__.commit()
|
||||||
|
# return report
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import pandas as pd
|
|||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from . import BaseClass
|
from . import BaseClass
|
||||||
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
|
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date, time
|
||||||
from typing import List
|
from typing import List
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from dateutil.parser._parser import ParserError
|
from dateutil.parser._parser import ParserError
|
||||||
@@ -397,7 +397,7 @@ class BasicSubmission(BaseClass):
|
|||||||
return cls.find_polymorphic_subclass(submission_type.name)
|
return cls.find_polymorphic_subclass(submission_type.name)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
if len(attrs) == 0 or attrs == None:
|
if attrs == None or len(attrs) == 0:
|
||||||
return cls
|
return cls
|
||||||
if any([not hasattr(cls, attr) for attr in attrs]):
|
if any([not hasattr(cls, attr) for attr in attrs]):
|
||||||
# looks for first model that has all included kwargs
|
# looks for first model that has all included kwargs
|
||||||
@@ -675,6 +675,7 @@ class BasicSubmission(BaseClass):
|
|||||||
logger.warning(f"End date with no start date, using Jan 1, 2023")
|
logger.warning(f"End date with no start date, using Jan 1, 2023")
|
||||||
start_date = date(2023, 1, 1)
|
start_date = date(2023, 1, 1)
|
||||||
if start_date != None:
|
if start_date != None:
|
||||||
|
logger.debug(f"Querying with start date: {start_date} and end date: {end_date}")
|
||||||
match start_date:
|
match start_date:
|
||||||
case date():
|
case date():
|
||||||
start_date = start_date.strftime("%Y-%m-%d")
|
start_date = start_date.strftime("%Y-%m-%d")
|
||||||
@@ -683,14 +684,19 @@ class BasicSubmission(BaseClass):
|
|||||||
case _:
|
case _:
|
||||||
start_date = parse(start_date).strftime("%Y-%m-%d")
|
start_date = parse(start_date).strftime("%Y-%m-%d")
|
||||||
match end_date:
|
match end_date:
|
||||||
case date():
|
case date() | datetime():
|
||||||
end_date = end_date.strftime("%Y-%m-%d")
|
end_date = end_date.strftime("%Y-%m-%d")
|
||||||
case int():
|
case int():
|
||||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
|
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
|
||||||
case _:
|
case _:
|
||||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
end_date = parse(end_date).strftime("%Y-%m-%d")
|
||||||
# logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
# logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
|
||||||
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
logger.debug(f"Start date {start_date} == End date {end_date}: {start_date==end_date}")
|
||||||
|
if start_date == end_date:
|
||||||
|
start_date = datetime.strptime(start_date, "%Y-%m-%d").strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
|
query = query.filter(cls.submitted_date==start_date)
|
||||||
|
else:
|
||||||
|
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||||
# by reagent (for some reason)
|
# by reagent (for some reason)
|
||||||
match reagent:
|
match reagent:
|
||||||
case str():
|
case str():
|
||||||
@@ -846,42 +852,55 @@ class BacterialCulture(BasicSubmission):
|
|||||||
"""
|
"""
|
||||||
Extends parent
|
Extends parent
|
||||||
"""
|
"""
|
||||||
|
from backend.validators import RSLNamer
|
||||||
|
data['abbreviation'] = "BC"
|
||||||
outstr = super().enforce_name(instr=instr, data=data)
|
outstr = super().enforce_name(instr=instr, data=data)
|
||||||
def construct(data:dict|None=None) -> str:
|
# def construct(data:dict|None=None) -> str:
|
||||||
"""
|
# """
|
||||||
Create default plate name.
|
# Create default plate name.
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
str: new RSL number
|
# str: new RSL number
|
||||||
"""
|
# """
|
||||||
# logger.debug(f"Attempting to construct RSL number from scratch...")
|
# # logger.debug(f"Attempting to construct RSL number from scratch...")
|
||||||
directory = cls.__directory_path__.joinpath("Bacteria")
|
# directory = cls.__directory_path__.joinpath("Bacteria")
|
||||||
year = str(datetime.now().year)[-2:]
|
# year = str(datetime.now().year)[-2:]
|
||||||
if directory.exists():
|
# if directory.exists():
|
||||||
logger.debug(f"Year: {year}")
|
# logger.debug(f"Year: {year}")
|
||||||
relevant_rsls = []
|
# relevant_rsls = []
|
||||||
all_xlsx = [item.stem for item in directory.rglob("*.xlsx") if bool(re.search(r"RSL-\d{2}-\d{4}", item.stem)) and year in item.stem[4:6]]
|
# all_xlsx = [item.stem for item in directory.rglob("*.xlsx") if bool(re.search(r"RSL-\d{2}-\d{4}", item.stem)) and year in item.stem[4:6]]
|
||||||
# logger.debug(f"All rsls: {all_xlsx}")
|
# # logger.debug(f"All rsls: {all_xlsx}")
|
||||||
for item in all_xlsx:
|
# for item in all_xlsx:
|
||||||
try:
|
# try:
|
||||||
relevant_rsls.append(re.match(r"RSL-\d{2}-\d{4}", item).group(0))
|
# relevant_rsls.append(re.match(r"RSL-\d{2}-\d{4}", item).group(0))
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
logger.error(f"Regex error: {e}")
|
# logger.error(f"Regex error: {e}")
|
||||||
continue
|
# continue
|
||||||
# logger.debug(f"Initial xlsx: {relevant_rsls}")
|
# # logger.debug(f"Initial xlsx: {relevant_rsls}")
|
||||||
max_number = max([int(item[-4:]) for item in relevant_rsls])
|
# max_number = max([int(item[-4:]) for item in relevant_rsls])
|
||||||
# logger.debug(f"The largest sample number is: {max_number}")
|
# # logger.debug(f"The largest sample number is: {max_number}")
|
||||||
return f"RSL-{year}-{str(max_number+1).zfill(4)}"
|
# return f"RSL-{year}-{str(max_number+1).zfill(4)}"
|
||||||
else:
|
# else:
|
||||||
# raise FileNotFoundError(f"Unable to locate the directory: {directory.__str__()}")
|
# # raise FileNotFoundError(f"Unable to locate the directory: {directory.__str__()}")
|
||||||
return f"RSL-{year}-0000"
|
# return f"RSL-{year}-0000"
|
||||||
|
# try:
|
||||||
|
# outstr = re.sub(r"RSL(\d{2})", r"RSL-\1", outstr, flags=re.IGNORECASE)
|
||||||
|
# except (AttributeError, TypeError) as e:
|
||||||
|
# outstr = construct()
|
||||||
|
# # year = datetime.now().year
|
||||||
|
# # self.parsed_name = f"RSL-{str(year)[-2:]}-0000"
|
||||||
|
# return re.sub(r"RSL-(\d{2})(\d{4})", r"RSL-\1-\2", outstr, flags=re.IGNORECASE)
|
||||||
|
# def construct():
|
||||||
|
# previous = cls.query(start_date=date.today(), end_date=date.today(), submission_type=cls.__name__)
|
||||||
|
# max = len(previous)
|
||||||
|
# return f"RSL-BC-{date.today().strftime('%Y%m%d')}-{max+1}"
|
||||||
try:
|
try:
|
||||||
outstr = re.sub(r"RSL(\d{2})", r"RSL-\1", outstr, flags=re.IGNORECASE)
|
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr)
|
||||||
|
outstr = re.sub(r"BC(\d{6})", r"BC-\1", outstr, flags=re.IGNORECASE)
|
||||||
except (AttributeError, TypeError) as e:
|
except (AttributeError, TypeError) as e:
|
||||||
outstr = construct()
|
# outstr = construct()
|
||||||
# year = datetime.now().year
|
outstr = RSLNamer.construct_new_plate_name(data=data)
|
||||||
# self.parsed_name = f"RSL-{str(year)[-2:]}-0000"
|
return outstr
|
||||||
return re.sub(r"RSL-(\d{2})(\d{4})", r"RSL-\1-\2", outstr, flags=re.IGNORECASE)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_regex(cls) -> str:
|
def get_regex(cls) -> str:
|
||||||
@@ -992,27 +1011,30 @@ class Wastewater(BasicSubmission):
|
|||||||
"""
|
"""
|
||||||
Extends parent
|
Extends parent
|
||||||
"""
|
"""
|
||||||
|
from backend.validators import RSLNamer
|
||||||
|
data['abbreviation'] = "WW"
|
||||||
outstr = super().enforce_name(instr=instr, data=data)
|
outstr = super().enforce_name(instr=instr, data=data)
|
||||||
def construct(data:dict|None=None):
|
# def construct(data:dict|None=None):
|
||||||
if "submitted_date" in data.keys():
|
# if "submitted_date" in data.keys():
|
||||||
if data['submitted_date']['value'] != None:
|
# if data['submitted_date']['value'] != None:
|
||||||
today = data['submitted_date']['value']
|
# today = data['submitted_date']['value']
|
||||||
else:
|
# else:
|
||||||
today = datetime.now()
|
# today = datetime.now()
|
||||||
else:
|
# else:
|
||||||
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr)
|
# today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr)
|
||||||
try:
|
# try:
|
||||||
today = parse(today.group())
|
# today = parse(today.group())
|
||||||
except AttributeError:
|
# except AttributeError:
|
||||||
today = datetime.now()
|
# today = datetime.now()
|
||||||
return f"RSL-WW-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
|
# return f"RSL-WW-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
|
||||||
if outstr == None:
|
# if outstr == None:
|
||||||
outstr = construct(data)
|
# outstr = construct(data)
|
||||||
try:
|
try:
|
||||||
outstr = re.sub(r"PCR(-|_)", "", outstr)
|
outstr = re.sub(r"PCR(-|_)", "", outstr)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.error(f"Problem using regex: {e}")
|
logger.error(f"Problem using regex: {e}")
|
||||||
outstr = construct(data)
|
# outstr = construct(data)
|
||||||
|
outstr = RSLNamer.construct_new_plate_name(instr=outstr)
|
||||||
outstr = outstr.replace("RSLWW", "RSL-WW")
|
outstr = outstr.replace("RSLWW", "RSL-WW")
|
||||||
outstr = re.sub(r"WW(\d{4})", r"WW-\1", outstr, flags=re.IGNORECASE)
|
outstr = re.sub(r"WW(\d{4})", r"WW-\1", outstr, flags=re.IGNORECASE)
|
||||||
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr)
|
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr)
|
||||||
@@ -1094,14 +1116,17 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
"""
|
"""
|
||||||
Extends parent
|
Extends parent
|
||||||
"""
|
"""
|
||||||
|
from backend.validators import RSLNamer
|
||||||
|
data['abbreviation'] = "AR"
|
||||||
outstr = super().enforce_name(instr=instr, data=data)
|
outstr = super().enforce_name(instr=instr, data=data)
|
||||||
def construct(data:dict|None=None):
|
# def construct(data:dict|None=None):
|
||||||
today = datetime.now()
|
# today = datetime.now()
|
||||||
return f"RSL-AR-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
|
# return f"RSL-AR-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
|
||||||
try:
|
try:
|
||||||
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"RSL-AR-\1\2\3", outstr, flags=re.IGNORECASE)
|
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"RSL-AR-\1\2\3", outstr, flags=re.IGNORECASE)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
outstr = construct()
|
# outstr = construct()
|
||||||
|
outstr = RSLNamer.construct_new_plate_name(instr=outstr, data=data)
|
||||||
try:
|
try:
|
||||||
plate_number = int(re.search(r"_|-\d?_", outstr).group().strip("_").strip("-"))
|
plate_number = int(re.search(r"_|-\d?_", outstr).group().strip("_").strip("-"))
|
||||||
except (AttributeError, ValueError) as e:
|
except (AttributeError, ValueError) as e:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging, re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
from backend.db.models import BasicSubmission, SubmissionType
|
from backend.db.models import BasicSubmission, SubmissionType
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -17,6 +18,10 @@ class RSLNamer(object):
|
|||||||
if self.submission_type != None:
|
if self.submission_type != None:
|
||||||
enforcer = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
enforcer = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
||||||
self.parsed_name = self.retrieve_rsl_number(instr=instr, regex=enforcer.get_regex())
|
self.parsed_name = self.retrieve_rsl_number(instr=instr, regex=enforcer.get_regex())
|
||||||
|
if data == None:
|
||||||
|
data = dict(submission_type=self.submission_type)
|
||||||
|
if "submission_type" not in data.keys():
|
||||||
|
data['submission_type'] = self.submission_type
|
||||||
self.parsed_name = enforcer.enforce_name(instr=self.parsed_name, data=data)
|
self.parsed_name = enforcer.enforce_name(instr=self.parsed_name, data=data)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -105,4 +110,22 @@ class RSLNamer(object):
|
|||||||
logger.debug(f"Got parsed submission name: {parsed_name}")
|
logger.debug(f"Got parsed submission name: {parsed_name}")
|
||||||
return parsed_name
|
return parsed_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def construct_new_plate_name(cls, data:dict) -> str:
|
||||||
|
if "submitted_date" in data.keys():
|
||||||
|
if data['submitted_date']['value'] != None:
|
||||||
|
today = data['submitted_date']['value']
|
||||||
|
else:
|
||||||
|
today = datetime.now()
|
||||||
|
else:
|
||||||
|
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr)
|
||||||
|
try:
|
||||||
|
today = parse(today.group())
|
||||||
|
except AttributeError:
|
||||||
|
today = datetime.now()
|
||||||
|
previous = BasicSubmission.query(start_date=today, end_date=today, submission_type=data['submission_type'])
|
||||||
|
plate_number = len(previous) + 1
|
||||||
|
return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
|
||||||
|
|
||||||
|
|
||||||
from .pydant import *
|
from .pydant import *
|
||||||
@@ -355,7 +355,8 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
value = value['value'].title()
|
value = value['value'].title()
|
||||||
return dict(value=value, missing=False)
|
return dict(value=value, missing=False)
|
||||||
else:
|
else:
|
||||||
return dict(value=RSLNamer(instr=values.data['filepath'].__str__()).submission_type.title(), missing=True)
|
# return dict(value=RSLNamer(instr=values.data['filepath'].__str__()).submission_type.title(), missing=True)
|
||||||
|
return dict(value=RSLNamer.retrieve_submission_type(instr=values.data['filepath']).title(), missing=True)
|
||||||
|
|
||||||
@field_validator("submission_category", mode="before")
|
@field_validator("submission_category", mode="before")
|
||||||
def create_category(cls, value):
|
def create_category(cls, value):
|
||||||
@@ -444,6 +445,11 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
instance.submission_sample_associations.append(assoc)
|
instance.submission_sample_associations.append(assoc)
|
||||||
case "equipment":
|
case "equipment":
|
||||||
logger.debug(f"Equipment: {pformat(self.equipment)}")
|
logger.debug(f"Equipment: {pformat(self.equipment)}")
|
||||||
|
try:
|
||||||
|
if equip == None:
|
||||||
|
continue
|
||||||
|
except UnboundLocalError:
|
||||||
|
continue
|
||||||
for equip in self.equipment:
|
for equip in self.equipment:
|
||||||
equip, association = equip.toSQL(submission=instance)
|
equip, association = equip.toSQL(submission=instance)
|
||||||
if association != None:
|
if association != None:
|
||||||
@@ -773,20 +779,20 @@ class PydEquipment(BaseModel, extra='ignore'):
|
|||||||
asset_number: str
|
asset_number: str
|
||||||
name: str
|
name: str
|
||||||
nickname: str|None
|
nickname: str|None
|
||||||
process: List[str]|None
|
process: str|None
|
||||||
role: str|None
|
role: str|None
|
||||||
|
|
||||||
@field_validator('process')
|
# @field_validator('process')
|
||||||
@classmethod
|
# @classmethod
|
||||||
def remove_dupes(cls, value):
|
# def remove_dupes(cls, value):
|
||||||
if isinstance(value, list):
|
# if isinstance(value, list):
|
||||||
return list(set(value))
|
# return list(set(value))
|
||||||
else:
|
# else:
|
||||||
return value
|
# return value
|
||||||
|
|
||||||
def toForm(self, parent):
|
# def toForm(self, parent):
|
||||||
from frontend.widgets.equipment_usage import EquipmentCheckBox
|
# from frontend.widgets.equipment_usage import EquipmentCheckBox
|
||||||
return EquipmentCheckBox(parent=parent, equipment=self)
|
# return EquipmentCheckBox(parent=parent, equipment=self)
|
||||||
|
|
||||||
def toSQL(self, submission:BasicSubmission|str=None):
|
def toSQL(self, submission:BasicSubmission|str=None):
|
||||||
if isinstance(submission, str):
|
if isinstance(submission, str):
|
||||||
@@ -796,7 +802,7 @@ class PydEquipment(BaseModel, extra='ignore'):
|
|||||||
return
|
return
|
||||||
if submission != None:
|
if submission != None:
|
||||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
|
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
|
||||||
assoc.process = self.process[0]
|
assoc.process = self.process
|
||||||
assoc.role = self.role
|
assoc.role = self.role
|
||||||
# equipment.equipment_submission_associations.append(assoc)
|
# equipment.equipment_submission_associations.append(assoc)
|
||||||
equipment.equipment_submission_associations.append(assoc)
|
equipment.equipment_submission_associations.append(assoc)
|
||||||
@@ -808,6 +814,7 @@ class PydEquipmentRole(BaseModel):
|
|||||||
|
|
||||||
name: str
|
name: str
|
||||||
equipment: List[PydEquipment]
|
equipment: List[PydEquipment]
|
||||||
|
processes: List[str]|None
|
||||||
|
|
||||||
def toForm(self, parent, submission_type, used):
|
def toForm(self, parent, submission_type, used):
|
||||||
from frontend.widgets.equipment_usage import RoleComboBox
|
from frontend.widgets.equipment_usage import RoleComboBox
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ class RoleComboBox(QWidget):
|
|||||||
self.process.setMaximumWidth(125)
|
self.process.setMaximumWidth(125)
|
||||||
self.process.setMinimumWidth(125)
|
self.process.setMinimumWidth(125)
|
||||||
self.process.setEditable(True)
|
self.process.setEditable(True)
|
||||||
self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
|
# self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
|
||||||
|
self.process.addItems(role.processes)
|
||||||
self.layout.addWidget(self.check)
|
self.layout.addWidget(self.check)
|
||||||
self.layout.addWidget(QLabel(f"{role.name}:"))
|
self.layout.addWidget(QLabel(f"{role.name}:"))
|
||||||
self.layout.addWidget(self.box)
|
self.layout.addWidget(self.box)
|
||||||
@@ -107,7 +108,7 @@ class RoleComboBox(QWidget):
|
|||||||
def parse_form(self) -> str|None:
|
def parse_form(self) -> str|None:
|
||||||
eq = Equipment.query(name=self.box.currentText())
|
eq = Equipment.query(name=self.box.currentText())
|
||||||
if self.check:
|
if self.check:
|
||||||
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
|
return PydEquipment(name=eq.name, process=self.process.currentText(), role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
|
|||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||||
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
||||||
from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation
|
from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation, Process
|
||||||
from backend.excel import make_report_html, make_report_xlsx
|
from backend.excel import make_report_html, make_report_xlsx
|
||||||
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
|
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
|
||||||
from xhtml2pdf import pisa
|
from xhtml2pdf import pisa
|
||||||
@@ -194,12 +194,13 @@ class SubmissionsSheet(QTableView):
|
|||||||
for equip in equipment:
|
for equip in equipment:
|
||||||
e = Equipment.query(name=equip.name)
|
e = Equipment.query(name=equip.name)
|
||||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
||||||
assoc.process = equip.processes[0]
|
process = Process.query(name=equip.process)
|
||||||
|
assoc.process = process
|
||||||
assoc.role = equip.role
|
assoc.role = equip.role
|
||||||
# submission.submission_equipment_associations.append(assoc)
|
# submission.submission_equipment_associations.append(assoc)
|
||||||
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
||||||
# submission.save()
|
# submission.save()
|
||||||
# assoc.save()
|
assoc.save()
|
||||||
|
|
||||||
def delete_item(self, event):
|
def delete_item(self, event):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user