Post code-cleanup
This commit is contained in:
@@ -180,9 +180,9 @@ class KitType(BaseClass):
|
|||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
|
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
|
||||||
logger.debug(f"Submission type: {submission_type}, Kit: {self}")
|
# logger.debug(f"Submission type: {submission_type}, Kit: {self}")
|
||||||
assocs = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type]
|
assocs = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type]
|
||||||
logger.debug(f"Associations: {assocs}")
|
# logger.debug(f"Associations: {assocs}")
|
||||||
# NOTE: rescue with submission type's default kit.
|
# NOTE: rescue with submission type's default kit.
|
||||||
if not assocs:
|
if not assocs:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -211,7 +211,7 @@ class KitType(BaseClass):
|
|||||||
# except TypeError:
|
# except TypeError:
|
||||||
# continue
|
# continue
|
||||||
output = {assoc.reagent_role.name: assoc.uses for assoc in assocs}
|
output = {assoc.reagent_role.name: assoc.uses for assoc in assocs}
|
||||||
logger.debug(f"Output: {output}")
|
# logger.debug(f"Output: {output}")
|
||||||
return output, new_kit
|
return output, new_kit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1718,7 +1718,7 @@ class SubmissionEquipmentAssociation(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) \
|
def query(cls, equipment_id: int|None=None, submission_id: int|None=None, role: str | None = None, limit: int = 0, **kwargs) \
|
||||||
-> Any | List[Any]:
|
-> Any | List[Any]:
|
||||||
query: Query = cls.__database_session__.query(cls)
|
query: Query = cls.__database_session__.query(cls)
|
||||||
query = query.filter(cls.equipment_id == equipment_id)
|
query = query.filter(cls.equipment_id == equipment_id)
|
||||||
@@ -2013,7 +2013,8 @@ class SubmissionTipsAssociation(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def query_or_create(cls, tips, submission, role: str, **kwargs):
|
def query_or_create(cls, tips, submission, role: str, **kwargs):
|
||||||
instance = cls.query(tip_id=tips.id, role=role, submission_id=submission.id, limit=1, **kwargs)
|
kwargs['limit'] = 1
|
||||||
|
instance = cls.query(tip_id=tips.id, role=role, submission_id=submission.id, **kwargs)
|
||||||
if instance is None:
|
if instance is None:
|
||||||
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
|
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
|
||||||
return instance
|
return instance
|
||||||
|
|||||||
@@ -185,3 +185,6 @@ class Contact(BaseClass):
|
|||||||
pass
|
pass
|
||||||
return cls.execute_query(query=query, limit=limit)
|
return cls.execute_query(query=query, limit=limit)
|
||||||
|
|
||||||
|
def to_pydantic(self) -> "PydContact":
|
||||||
|
from backend.validators import PydContact
|
||||||
|
return PydContact(name=self.name, email=self.email, phone=self.phone)
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ class ReagentParser(object):
|
|||||||
# submission_type = submission_type['value']
|
# submission_type = submission_type['value']
|
||||||
# if isinstance(submission_type, str):
|
# if isinstance(submission_type, str):
|
||||||
# submission_type = SubmissionType.query(name=submission_type)
|
# submission_type = SubmissionType.query(name=submission_type)
|
||||||
logger.debug("Running kit map")
|
# logger.debug("Running kit map")
|
||||||
associations, self.kit_object = self.kit_object.construct_xl_map_for_use(submission_type=self.submission_type_obj)
|
associations, self.kit_object = self.kit_object.construct_xl_map_for_use(submission_type=self.submission_type_obj)
|
||||||
reagent_map = {k: v for k, v in associations.items() if k != 'info'}
|
reagent_map = {k: v for k, v in associations.items() if k != 'info'}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -158,12 +158,15 @@ class InfoWriter(object):
|
|||||||
match k:
|
match k:
|
||||||
case "custom":
|
case "custom":
|
||||||
continue
|
continue
|
||||||
# case "comment":
|
case "comment":
|
||||||
|
|
||||||
# NOTE: merge all comments to fit in single cell.
|
# NOTE: merge all comments to fit in single cell.
|
||||||
if k == "comment" and isinstance(v['value'], list):
|
if isinstance(v['value'], list):
|
||||||
json_join = [item['text'] for item in v['value'] if 'text' in item.keys()]
|
json_join = [item['text'] for item in v['value'] if 'text' in item.keys()]
|
||||||
v['value'] = "\n".join(json_join)
|
v['value'] = "\n".join(json_join)
|
||||||
|
case thing if thing in self.sub_object.timestamps:
|
||||||
|
v['value'] = v['value'].date()
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
final_info[k] = v
|
final_info[k] = v
|
||||||
try:
|
try:
|
||||||
locations = v['locations']
|
locations = v['locations']
|
||||||
@@ -252,6 +255,11 @@ class ReagentWriter(object):
|
|||||||
for v in reagent.values():
|
for v in reagent.values():
|
||||||
if not isinstance(v, dict):
|
if not isinstance(v, dict):
|
||||||
continue
|
continue
|
||||||
|
match v['value']:
|
||||||
|
case datetime():
|
||||||
|
v['value'] = v['value'].date()
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
sheet.cell(row=v['row'], column=v['column'], value=v['value'])
|
sheet.cell(row=v['row'], column=v['column'], value=v['value'])
|
||||||
return self.xl
|
return self.xl
|
||||||
|
|
||||||
|
|||||||
@@ -641,7 +641,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
@field_validator("contact")
|
@field_validator("contact")
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_contact_from_org(cls, value, values):
|
def get_contact_from_org(cls, value, values):
|
||||||
logger.debug(f"Value coming in: {value}")
|
# logger.debug(f"Value coming in: {value}")
|
||||||
match value:
|
match value:
|
||||||
case dict():
|
case dict():
|
||||||
if isinstance(value['value'], tuple):
|
if isinstance(value['value'], tuple):
|
||||||
@@ -650,12 +650,12 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
value = dict(value=value[0], missing=False)
|
value = dict(value=value[0], missing=False)
|
||||||
case _:
|
case _:
|
||||||
value = dict(value=value, missing=False)
|
value = dict(value=value, missing=False)
|
||||||
logger.debug(f"Value after match: {value}")
|
# logger.debug(f"Value after match: {value}")
|
||||||
check = Contact.query(name=value['value'])
|
check = Contact.query(name=value['value'])
|
||||||
logger.debug(f"Check came back with {check}")
|
# logger.debug(f"Check came back with {check}")
|
||||||
if not isinstance(check, Contact):
|
if not isinstance(check, Contact):
|
||||||
org = values.data['submitting_lab']['value']
|
org = values.data['submitting_lab']['value']
|
||||||
logger.debug(f"Checking organization: {org}")
|
# logger.debug(f"Checking organization: {org}")
|
||||||
if isinstance(org, str):
|
if isinstance(org, str):
|
||||||
org = Organization.query(name=values.data['submitting_lab']['value'], limit=1)
|
org = Organization.query(name=values.data['submitting_lab']['value'], limit=1)
|
||||||
if isinstance(org, Organization):
|
if isinstance(org, Organization):
|
||||||
@@ -666,10 +666,10 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
if isinstance(contact, tuple):
|
if isinstance(contact, tuple):
|
||||||
contact = contact[0]
|
contact = contact[0]
|
||||||
value = dict(value=f"Defaulted to: {contact}", missing=False)
|
value = dict(value=f"Defaulted to: {contact}", missing=False)
|
||||||
logger.debug(f"Value after query: {value}")
|
# logger.debug(f"Value after query: {value}")
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Value after bypass check: {value}")
|
# logger.debug(f"Value after bypass check: {value}")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __init__(self, run_custom: bool = False, **data):
|
def __init__(self, run_custom: bool = False, **data):
|
||||||
@@ -879,6 +879,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
|
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
disable (list, optional): a list of widgets to be disabled in the form. Defaults to None.
|
||||||
parent (QWidget): parent widget of the constructed object
|
parent (QWidget): parent widget of the constructed object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -911,7 +912,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
|
|
||||||
# @report_result
|
# @report_result
|
||||||
def check_kit_integrity(self, extraction_kit: str | dict | None = None, exempt: List[PydReagent] = []) -> Tuple[
|
def check_kit_integrity(self, extraction_kit: str | dict | None = None, exempt: List[PydReagent] = []) -> Tuple[
|
||||||
List[PydReagent], Report]:
|
List[PydReagent], Report, List[PydReagent]]:
|
||||||
"""
|
"""
|
||||||
Ensures all reagents expected in kit are listed in Submission
|
Ensures all reagents expected in kit are listed in Submission
|
||||||
|
|
||||||
@@ -933,13 +934,11 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])]
|
ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])]
|
||||||
# NOTE: Exclude any reagenttype found in this pyd not expected in kit.
|
# NOTE: Exclude any reagenttype found in this pyd not expected in kit.
|
||||||
expected_check = [item.role for item in ext_kit_rtypes]
|
expected_check = [item.role for item in ext_kit_rtypes]
|
||||||
logger.debug(self.reagents)
|
|
||||||
output_reagents = [rt for rt in self.reagents if rt.role in expected_check]
|
output_reagents = [rt for rt in self.reagents if rt.role in expected_check]
|
||||||
missing_check = [item.role for item in output_reagents]
|
missing_check = [item.role for item in output_reagents]
|
||||||
missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check and rt.role not in exempt]
|
missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check and rt.role not in exempt]
|
||||||
# logger.debug(f"Missing reagents: {missing_reagents}")
|
# logger.debug(f"Missing reagents: {missing_reagents}")
|
||||||
missing_reagents += [rt for rt in output_reagents if rt.missing]
|
missing_reagents += [rt for rt in output_reagents if rt.missing]
|
||||||
logger.debug(pformat(missing_reagents))
|
|
||||||
output_reagents += [rt for rt in missing_reagents if rt not in output_reagents]
|
output_reagents += [rt for rt in missing_reagents if rt not in output_reagents]
|
||||||
# NOTE: if lists are equal return no problem
|
# NOTE: if lists are equal return no problem
|
||||||
if len(missing_reagents) == 0:
|
if len(missing_reagents) == 0:
|
||||||
@@ -956,13 +955,13 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
expired = []
|
expired = []
|
||||||
for reagent in self.reagents:
|
for reagent in self.reagents:
|
||||||
if reagent not in exempt:
|
if reagent not in exempt:
|
||||||
role_expiry = ReagentRole.query(name=reagent.role).eol_ext
|
role_eol = ReagentRole.query(name=reagent.role).eol_ext
|
||||||
try:
|
try:
|
||||||
dt = datetime.combine(reagent.expiry, datetime.max.time())
|
dt = datetime.combine(reagent.expiry, datetime.max.time())
|
||||||
except TypeError:
|
except TypeError:
|
||||||
continue
|
continue
|
||||||
if datetime.now() > dt + role_expiry:
|
if datetime.now() > dt + role_eol:
|
||||||
expired.append(f"{reagent.role}, {reagent.lot}: {reagent.expiry} + {role_expiry.days}")
|
expired.append(f"{reagent.role}, {reagent.lot}: {reagent.expiry.date()} + {role_eol.days}")
|
||||||
if expired:
|
if expired:
|
||||||
output = '\n'.join(expired)
|
output = '\n'.join(expired)
|
||||||
result = Result(status="Warning",
|
result = Result(status="Warning",
|
||||||
@@ -996,11 +995,12 @@ class PydContact(BaseModel):
|
|||||||
area_regex = re.compile(r"^\(?(\d{3})\)?(-| )?")
|
area_regex = re.compile(r"^\(?(\d{3})\)?(-| )?")
|
||||||
if len(value) > 8:
|
if len(value) > 8:
|
||||||
match = area_regex.match(value)
|
match = area_regex.match(value)
|
||||||
logger.debug(f"Match: {match.group(1)}")
|
# logger.debug(f"Match: {match.group(1)}")
|
||||||
value = area_regex.sub(f"({match.group(1).strip()}) ", value)
|
value = area_regex.sub(f"({match.group(1).strip()}) ", value)
|
||||||
logger.debug(f"Output phone: {value}")
|
# logger.debug(f"Output phone: {value}")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@report_result
|
||||||
def to_sql(self) -> Tuple[Contact, Report]:
|
def to_sql(self) -> Tuple[Contact, Report]:
|
||||||
"""
|
"""
|
||||||
Converts this instance into a backend.db.models.organization. Contact instance.
|
Converts this instance into a backend.db.models.organization. Contact instance.
|
||||||
@@ -1036,6 +1036,18 @@ class PydOrganization(BaseModel):
|
|||||||
cost_centre: str
|
cost_centre: str
|
||||||
contacts: List[PydContact] | None
|
contacts: List[PydContact] | None
|
||||||
|
|
||||||
|
@field_validator("contacts", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def string_to_list(cls, value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = Contact.query(name=value)
|
||||||
|
try:
|
||||||
|
value = [value.to_pydantic()]
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def to_sql(self) -> Organization:
|
def to_sql(self) -> Organization:
|
||||||
"""
|
"""
|
||||||
Converts this instance into a backend.db.models.organization.Organization instance.
|
Converts this instance into a backend.db.models.organization.Organization instance.
|
||||||
@@ -1047,10 +1059,14 @@ class PydOrganization(BaseModel):
|
|||||||
for field in self.model_fields:
|
for field in self.model_fields:
|
||||||
match field:
|
match field:
|
||||||
case "contacts":
|
case "contacts":
|
||||||
value = [item.to_sql() for item in getattr(self, field)]
|
value = getattr(self, field)
|
||||||
|
if value:
|
||||||
|
value = [item.to_sql() for item in value if item]
|
||||||
case _:
|
case _:
|
||||||
value = getattr(self, field)
|
value = getattr(self, field)
|
||||||
instance.__setattr__(name=field, value=value)
|
logger.debug(f"Setting {field} to {value}")
|
||||||
|
if value:
|
||||||
|
setattr(instance, field, value)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
@@ -1105,7 +1121,8 @@ class PydKit(BaseModel):
|
|||||||
instance = KitType.query(name=self.name)
|
instance = KitType.query(name=self.name)
|
||||||
if instance is None:
|
if instance is None:
|
||||||
instance = KitType(name=self.name)
|
instance = KitType(name=self.name)
|
||||||
[item.to_sql(instance) for item in self.reagent_roles]
|
for role in self.reagent_roles:
|
||||||
|
role.to_sql(instance)
|
||||||
return instance, report
|
return instance, report
|
||||||
|
|
||||||
|
|
||||||
@@ -1162,7 +1179,8 @@ class PydIridaControl(BaseModel, extra='ignore'):
|
|||||||
contains: list | dict #: unstructured hashes in contains.tsv for each organism
|
contains: list | dict #: unstructured hashes in contains.tsv for each organism
|
||||||
matches: list | dict #: unstructured hashes in matches.tsv for each organism
|
matches: list | dict #: unstructured hashes in matches.tsv for each organism
|
||||||
kraken: list | dict #: unstructured output from kraken_report
|
kraken: list | dict #: unstructured output from kraken_report
|
||||||
subtype: str #: EN-NOS, MCS-NOS, etc
|
# subtype: str #: EN-NOS, MCS-NOS, etc
|
||||||
|
subtype: Literal["ATCC49226", "ATCC49619", "EN-NOS", "EN-SSTI", "MCS-NOS", "MCS-SSTI", "SN-NOS", "SN-SSTI"]
|
||||||
refseq_version: str #: version of refseq used in fastq parsing
|
refseq_version: str #: version of refseq used in fastq parsing
|
||||||
kraken2_version: str
|
kraken2_version: str
|
||||||
kraken2_db_version: str
|
kraken2_db_version: str
|
||||||
@@ -1171,6 +1189,13 @@ class PydIridaControl(BaseModel, extra='ignore'):
|
|||||||
submission_id: int
|
submission_id: int
|
||||||
controltype_name: str
|
controltype_name: str
|
||||||
|
|
||||||
|
@field_validator("refseq_version", "kraken2_version", "kraken2_db_version", mode='before')
|
||||||
|
@classmethod
|
||||||
|
def enforce_string(cls, value):
|
||||||
|
if not value:
|
||||||
|
value = ""
|
||||||
|
return value
|
||||||
|
|
||||||
def to_sql(self):
|
def to_sql(self):
|
||||||
instance = IridaControl.query(name=self.name)
|
instance = IridaControl.query(name=self.name)
|
||||||
if not instance:
|
if not instance:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from PyQt6.QtWidgets import QWidget
|
|||||||
import plotly, logging
|
import plotly, logging
|
||||||
from plotly.graph_objects import Figure
|
from plotly.graph_objects import Figure
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from frontend.widgets.functions import select_save_file
|
|
||||||
from tools import divide_chunks
|
from tools import divide_chunks
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
@@ -65,7 +64,8 @@ class CustomFigure(Figure):
|
|||||||
)
|
)
|
||||||
assert isinstance(self, CustomFigure)
|
assert isinstance(self, CustomFigure)
|
||||||
|
|
||||||
def make_plotly_buttons(self, months: int = 6) -> Generator[dict, None, None]:
|
@classmethod
|
||||||
|
def make_plotly_buttons(cls, months: int = 6) -> Generator[dict, None, None]:
|
||||||
"""
|
"""
|
||||||
Creates html buttons to zoom in on date areas
|
Creates html buttons to zoom in on date areas
|
||||||
|
|
||||||
@@ -115,7 +115,8 @@ class CustomFigure(Figure):
|
|||||||
{"yaxis.title.text": mode},
|
{"yaxis.title.text": mode},
|
||||||
])
|
])
|
||||||
|
|
||||||
def to_html(self) -> str:
|
@property
|
||||||
|
def html(self) -> str:
|
||||||
"""
|
"""
|
||||||
Creates final html code from plotly
|
Creates final html code from plotly
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class IridaFigure(CustomFigure):
|
|||||||
|
|
||||||
super().__init__(df=df, modes=modes, settings=settings)
|
super().__init__(df=df, modes=modes, settings=settings)
|
||||||
self.df = df
|
self.df = df
|
||||||
try:
|
# try:
|
||||||
months = int(settings['months'])
|
# months = int(settings['months'])
|
||||||
except KeyError:
|
# except KeyError:
|
||||||
months = 6
|
# months = 6
|
||||||
self.construct_chart(df=df, modes=modes, start_date=settings['start_date'], end_date=settings['end_date'])
|
self.construct_chart(df=df, modes=modes, start_date=settings['start_date'], end_date=settings['end_date'])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ class PCRFigure(CustomFigure):
|
|||||||
months: int = 6):
|
months: int = 6):
|
||||||
super().__init__(df=df, modes=modes, settings=settings)
|
super().__init__(df=df, modes=modes, settings=settings)
|
||||||
self.df = df
|
self.df = df
|
||||||
try:
|
# try:
|
||||||
months = int(settings['months'])
|
# months = int(settings['months'])
|
||||||
except KeyError:
|
# except KeyError:
|
||||||
months = 6
|
# months = 6
|
||||||
self.construct_chart(df=df)
|
self.construct_chart(df=df)
|
||||||
|
|
||||||
def construct_chart(self, df: pd.DataFrame):
|
def construct_chart(self, df: pd.DataFrame):
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class TurnaroundChart(CustomFigure):
|
|||||||
months: int = 6):
|
months: int = 6):
|
||||||
super().__init__(df=df, modes=modes, settings=settings)
|
super().__init__(df=df, modes=modes, settings=settings)
|
||||||
self.df = df
|
self.df = df
|
||||||
try:
|
# try:
|
||||||
months = int(settings['months'])
|
# months = int(settings['months'])
|
||||||
except KeyError:
|
# except KeyError:
|
||||||
months = 6
|
# months = 6
|
||||||
self.construct_chart()
|
self.construct_chart()
|
||||||
if threshold:
|
if threshold:
|
||||||
self.add_hline(y=threshold)
|
self.add_hline(y=threshold)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Constructs main application.
|
Constructs main application.
|
||||||
"""
|
"""
|
||||||
|
import getpass
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from PyQt6.QtCore import qInstallMessageHandler
|
from PyQt6.QtCore import qInstallMessageHandler
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
@@ -13,9 +14,10 @@ from pathlib import Path
|
|||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from __init__ import project_path
|
from __init__ import project_path
|
||||||
from backend import SubmissionType, Reagent, BasicSample, Organization, KitType
|
from backend import SubmissionType, Reagent, BasicSample, Organization, KitType
|
||||||
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user
|
from tools import (
|
||||||
|
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, under_development
|
||||||
|
)
|
||||||
from .functions import select_save_file, select_open_file
|
from .functions import select_save_file, select_open_file
|
||||||
# from datetime import date
|
|
||||||
from .pop_ups import HTMLPop, AlertPop
|
from .pop_ups import HTMLPop, AlertPop
|
||||||
from .misc import Pagifier
|
from .misc import Pagifier
|
||||||
import logging, webbrowser, sys, shutil
|
import logging, webbrowser, sys, shutil
|
||||||
@@ -84,7 +86,8 @@ class App(QMainWindow):
|
|||||||
maintenanceMenu.addAction(self.joinPCRAction)
|
maintenanceMenu.addAction(self.joinPCRAction)
|
||||||
editMenu.addAction(self.editReagentAction)
|
editMenu.addAction(self.editReagentAction)
|
||||||
editMenu.addAction(self.manageOrgsAction)
|
editMenu.addAction(self.manageOrgsAction)
|
||||||
# editMenu.addAction(self.manageKitsAction)
|
if getpass.getuser() == "lwark":
|
||||||
|
editMenu.addAction(self.manageKitsAction)
|
||||||
if not is_power_user():
|
if not is_power_user():
|
||||||
editMenu.setEnabled(False)
|
editMenu.setEnabled(False)
|
||||||
|
|
||||||
@@ -119,7 +122,7 @@ class App(QMainWindow):
|
|||||||
connect menu and tool bar item to functions
|
connect menu and tool bar item to functions
|
||||||
"""
|
"""
|
||||||
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
|
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
|
||||||
self.addReagentAction.triggered.connect(self.table_widget.formwidget.new_add_reagent)
|
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
|
||||||
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
|
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
|
||||||
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
|
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
|
||||||
self.helpAction.triggered.connect(self.showAbout)
|
self.helpAction.triggered.connect(self.showAbout)
|
||||||
@@ -177,6 +180,11 @@ class App(QMainWindow):
|
|||||||
dlg = SearchBox(self, object_type=BasicSample, extras=[])
|
dlg = SearchBox(self, object_type=BasicSample, extras=[])
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
|
@check_authorization
|
||||||
|
def edit_reagent(self, *args, **kwargs):
|
||||||
|
dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="role")])
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
def export_ST_yaml(self):
|
def export_ST_yaml(self):
|
||||||
"""
|
"""
|
||||||
Copies submission type yaml to file system for editing and remport
|
Copies submission type yaml to file system for editing and remport
|
||||||
@@ -191,13 +199,18 @@ class App(QMainWindow):
|
|||||||
fname = select_save_file(obj=self, default_name="Submission Type Template.yml", extension="yml")
|
fname = select_save_file(obj=self, default_name="Submission Type Template.yml", extension="yml")
|
||||||
shutil.copyfile(yaml_path, fname)
|
shutil.copyfile(yaml_path, fname)
|
||||||
|
|
||||||
@check_authorization
|
|
||||||
def edit_reagent(self, *args, **kwargs):
|
|
||||||
dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="role")])
|
|
||||||
dlg.exec()
|
|
||||||
|
|
||||||
@check_authorization
|
@check_authorization
|
||||||
def import_ST_yaml(self, *args, **kwargs):
|
def import_ST_yaml(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Imports a yml form into a submission type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args ():
|
||||||
|
**kwargs ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
fname = select_open_file(obj=self, file_extension="yml")
|
fname = select_open_file(obj=self, file_extension="yml")
|
||||||
if not fname:
|
if not fname:
|
||||||
logger.info(f"Import cancelled.")
|
logger.info(f"Import cancelled.")
|
||||||
@@ -220,9 +233,11 @@ class App(QMainWindow):
|
|||||||
dlg = ManagerWindow(parent=self, object_type=Organization, extras=[])
|
dlg = ManagerWindow(parent=self, object_type=Organization, extras=[])
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
new_org = dlg.parse_form()
|
new_org = dlg.parse_form()
|
||||||
|
new_org.save()
|
||||||
# logger.debug(new_org.__dict__)
|
# logger.debug(new_org.__dict__)
|
||||||
|
|
||||||
def manage_kits(self):
|
@under_development
|
||||||
|
def manage_kits(self, *args, **kwargs):
|
||||||
dlg = ManagerWindow(parent=self, object_type=KitType, extras=[])
|
dlg = ManagerWindow(parent=self, object_type=KitType, extras=[])
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
print(dlg.parse_form())
|
print(dlg.parse_form())
|
||||||
|
|||||||
@@ -99,18 +99,24 @@ class ControlsViewer(InfoPane):
|
|||||||
self.mode_sub_type = self.mode_sub_typer.currentText()
|
self.mode_sub_type = self.mode_sub_typer.currentText()
|
||||||
months = self.diff_month(self.start_date, self.end_date)
|
months = self.diff_month(self.start_date, self.end_date)
|
||||||
# NOTE: query all controls using the type/start and end dates from the gui
|
# NOTE: query all controls using the type/start and end dates from the gui
|
||||||
chart_settings = dict(sub_type=self.con_sub_type, start_date=self.start_date, end_date=self.end_date,
|
chart_settings = dict(
|
||||||
|
sub_type=self.con_sub_type,
|
||||||
|
start_date=self.start_date,
|
||||||
|
end_date=self.end_date,
|
||||||
mode=self.mode,
|
mode=self.mode,
|
||||||
sub_mode=self.mode_sub_type, parent=self, months=months)
|
sub_mode=self.mode_sub_type,
|
||||||
|
parent=self,
|
||||||
|
months=months
|
||||||
|
)
|
||||||
self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
|
self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
|
||||||
self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name)
|
self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name)
|
||||||
if issubclass(self.fig.__class__, CustomFigure):
|
if issubclass(self.fig.__class__, CustomFigure):
|
||||||
self.save_button.setEnabled(True)
|
self.save_button.setEnabled(True)
|
||||||
# NOTE: construct html for webview
|
# NOTE: construct html for webview
|
||||||
try:
|
# try:
|
||||||
html = self.fig.to_html()
|
# html = self.fig.html
|
||||||
except AttributeError:
|
# except AttributeError:
|
||||||
html = ""
|
# html = ""
|
||||||
self.webview.setHtml(html)
|
self.webview.setHtml(self.fig.html)
|
||||||
self.webview.update()
|
self.webview.update()
|
||||||
return report
|
return report
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ Creates forms that the user can enter equipment info into.
|
|||||||
'''
|
'''
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from PyQt6.QtCore import Qt, QSignalBlocker
|
from PyQt6.QtCore import Qt, QSignalBlocker
|
||||||
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout)
|
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
|
||||||
|
)
|
||||||
from backend.db.models import Equipment, BasicSubmission, Process
|
from backend.db.models import Equipment, BasicSubmission, Process
|
||||||
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
|
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
|
||||||
import logging
|
import logging
|
||||||
@@ -36,8 +37,8 @@ class EquipmentUsage(QDialog):
|
|||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
label = self.LabelRow(parent=self)
|
label = self.LabelRow(parent=self)
|
||||||
self.layout.addWidget(label)
|
self.layout.addWidget(label)
|
||||||
for eq in self.opt_equipment:
|
for equipment in self.opt_equipment:
|
||||||
widg = eq.to_form(parent=self, used=self.used_equipment)
|
widg = equipment.to_form(parent=self, used=self.used_equipment)
|
||||||
self.layout.addWidget(widg)
|
self.layout.addWidget(widg)
|
||||||
widg.update_processes()
|
widg.update_processes()
|
||||||
self.layout.addWidget(self.buttonBox)
|
self.layout.addWidget(self.buttonBox)
|
||||||
@@ -64,6 +65,7 @@ class EquipmentUsage(QDialog):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
class LabelRow(QWidget):
|
class LabelRow(QWidget):
|
||||||
|
"""Provides column headers"""
|
||||||
|
|
||||||
def __init__(self, parent) -> None:
|
def __init__(self, parent) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'''
|
"""
|
||||||
functions used by all windows in the application's frontend
|
functions used by all windows in the application's frontend
|
||||||
'''
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
|
from PyQt6.QtCore import QMarginsF
|
||||||
|
from PyQt6.QtGui import QPageLayout, QPageSize
|
||||||
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWidgets import QMainWindow, QFileDialog
|
from PyQt6.QtWidgets import QMainWindow, QFileDialog
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
@@ -60,3 +63,21 @@ def select_save_file(obj: QMainWindow, default_name: str, extension: str) -> Pat
|
|||||||
fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter=f"{extension}(*.{extension})")[0])
|
fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter=f"{extension}(*.{extension})")[0])
|
||||||
obj.last_dir = fname.parent
|
obj.last_dir = fname.parent
|
||||||
return fname
|
return fname
|
||||||
|
|
||||||
|
|
||||||
|
def save_pdf(obj: QWebEngineView, filename: Path):
|
||||||
|
"""
|
||||||
|
Handles printing to PDF
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (): Parent object
|
||||||
|
filename (): Where to save pdf.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
page_layout = QPageLayout()
|
||||||
|
page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
|
||||||
|
page_layout.setOrientation(QPageLayout.Orientation.Portrait)
|
||||||
|
page_layout.setMargins(QMarginsF(25, 25, 25, 25))
|
||||||
|
obj.page().printToPdf(filename.absolute().__str__(), page_layout)
|
||||||
|
|||||||
@@ -2,15 +2,13 @@
|
|||||||
Gel box for artic quality control
|
Gel box for artic quality control
|
||||||
"""
|
"""
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QLineEdit, QDialogButtonBox,
|
QWidget, QDialog, QGridLayout, QLabel, QLineEdit, QDialogButtonBox, QTextEdit, QComboBox
|
||||||
QTextEdit, QComboBox
|
|
||||||
)
|
)
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import numpy as np
|
import logging, numpy as np
|
||||||
import logging
|
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Tuple, List
|
from typing import Tuple, List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -103,7 +101,8 @@ class ControlsForm(QWidget):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
tt_text = None
|
tt_text = None
|
||||||
for iii, item in enumerate(
|
for iii, item in enumerate(
|
||||||
["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
|
["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]
|
||||||
|
):
|
||||||
label = QLabel(item)
|
label = QLabel(item)
|
||||||
self.layout.addWidget(label, 0, iii, 1, 1)
|
self.layout.addWidget(label, 0, iii, 1, 1)
|
||||||
if iii > 1:
|
if iii > 1:
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from PyQt6.QtCore import QSignalBlocker
|
|||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWidgets import QWidget, QGridLayout
|
from PyQt6.QtWidgets import QWidget, QGridLayout
|
||||||
from tools import Report, report_result, Result
|
from tools import Report, report_result, Result
|
||||||
from .misc import StartEndDatePicker, save_pdf
|
from .misc import StartEndDatePicker
|
||||||
from .functions import select_save_file
|
from .functions import select_save_file, save_pdf
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
@@ -38,8 +38,7 @@ class InfoPane(QWidget):
|
|||||||
lastmonth = self.datepicker.end_date.date().addDays(-31)
|
lastmonth = self.datepicker.end_date.date().addDays(-31)
|
||||||
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
|
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
# NOTE: block signal that will rerun controls getter and set start date
|
# NOTE: block signal that will rerun controls getter and set start date without triggering this function again
|
||||||
# Without triggering this function again
|
|
||||||
with QSignalBlocker(self.datepicker.start_date) as blocker:
|
with QSignalBlocker(self.datepicker.start_date) as blocker:
|
||||||
self.datepicker.start_date.setDate(lastmonth)
|
self.datepicker.start_date.setDate(lastmonth)
|
||||||
self.update_data()
|
self.update_data()
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ Contains miscellaneous widgets for frontend functions
|
|||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QIcon
|
from PyQt6.QtGui import QStandardItem, QIcon
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QVBoxLayout,
|
QLabel, QLineEdit, QComboBox, QDateEdit, QPushButton, QWidget,
|
||||||
QLineEdit, QComboBox, QDialog,
|
QHBoxLayout, QSizePolicy
|
||||||
QDialogButtonBox, QDateEdit, QPushButton, QWidget, QHBoxLayout, QSizePolicy
|
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF
|
from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF
|
||||||
from tools import jinja_template_loading
|
from tools import jinja_template_loading
|
||||||
@@ -20,96 +18,98 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
|||||||
env = jinja_template_loading()
|
env = jinja_template_loading()
|
||||||
|
|
||||||
|
|
||||||
class AddReagentForm(QDialog):
|
# class AddReagentForm(QDialog):
|
||||||
"""
|
# """
|
||||||
dialog to add gather info about new reagent
|
# dialog to add gather info about new reagent (Defunct)
|
||||||
"""
|
# """
|
||||||
|
#
|
||||||
def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
|
# def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
|
||||||
reagent_name: str | None = None, kit: str | KitType | None = None) -> None:
|
# reagent_name: str | None = None, kit: str | KitType | None = None) -> None:
|
||||||
super().__init__()
|
# super().__init__()
|
||||||
if reagent_name is None:
|
# if reagent_name is None:
|
||||||
reagent_name = reagent_role
|
# reagent_name = reagent_role
|
||||||
self.setWindowTitle("Add Reagent")
|
# self.setWindowTitle("Add Reagent")
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
# QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
# self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
# self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
# self.buttonBox.rejected.connect(self.reject)
|
||||||
# NOTE: widget to get lot info
|
# # NOTE: widget to get lot info
|
||||||
self.name_input = QComboBox()
|
# self.name_input = QComboBox()
|
||||||
self.name_input.setObjectName("name")
|
# self.name_input.setObjectName("name")
|
||||||
self.name_input.setEditable(True)
|
# self.name_input.setEditable(True)
|
||||||
self.name_input.setCurrentText(reagent_name)
|
# self.name_input.setCurrentText(reagent_name)
|
||||||
self.lot_input = QLineEdit()
|
# self.lot_input = QLineEdit()
|
||||||
self.lot_input.setObjectName("lot")
|
# self.lot_input.setObjectName("lot")
|
||||||
self.lot_input.setText(reagent_lot)
|
# self.lot_input.setText(reagent_lot)
|
||||||
# NOTE: widget to get expiry info
|
# # NOTE: widget to get expiry info
|
||||||
self.exp_input = QDateEdit(calendarPopup=True)
|
# self.expiry_input = QDateEdit(calendarPopup=True)
|
||||||
self.exp_input.setObjectName('expiry')
|
# self.expiry_input.setObjectName('expiry')
|
||||||
# NOTE: if expiry is not passed in from gui, use today
|
# # NOTE: if expiry is not passed in from gui, use today
|
||||||
if expiry is None:
|
# if expiry is None:
|
||||||
self.exp_input.setDate(QDate(1970, 1, 1))
|
# logger.warning(f"Did not receive expiry, setting to 1970, 1, 1")
|
||||||
else:
|
# self.expiry_input.setDate(QDate(1970, 1, 1))
|
||||||
try:
|
# else:
|
||||||
self.exp_input.setDate(expiry)
|
# try:
|
||||||
except TypeError:
|
# self.expiry_input.setDate(expiry)
|
||||||
self.exp_input.setDate(QDate(1970, 1, 1))
|
# except TypeError:
|
||||||
# NOTE: widget to get reagent type info
|
# self.expiry_input.setDate(QDate(1970, 1, 1))
|
||||||
self.type_input = QComboBox()
|
# # NOTE: widget to get reagent type info
|
||||||
self.type_input.setObjectName('role')
|
# self.role_input = QComboBox()
|
||||||
if kit:
|
# self.role_input.setObjectName('role')
|
||||||
match kit:
|
# if kit:
|
||||||
case str():
|
# match kit:
|
||||||
kit = KitType.query(name=kit)
|
# case str():
|
||||||
case _:
|
# kit = KitType.query(name=kit)
|
||||||
pass
|
# case _:
|
||||||
self.type_input.addItems([item.name for item in ReagentRole.query() if kit in item.kit_types])
|
# pass
|
||||||
else:
|
# self.role_input.addItems([item.name for item in ReagentRole.query() if kit in item.kit_types])
|
||||||
self.type_input.addItems([item.name for item in ReagentRole.query()])
|
# else:
|
||||||
# NOTE: convert input to user-friendly string?
|
# self.role_input.addItems([item.name for item in ReagentRole.query()])
|
||||||
try:
|
# # NOTE: convert input to user-friendly string?
|
||||||
reagent_role = reagent_role.replace("_", " ").title()
|
# try:
|
||||||
except AttributeError:
|
# reagent_role = reagent_role.replace("_", " ").title()
|
||||||
reagent_role = None
|
# except AttributeError:
|
||||||
# NOTE: set parsed reagent type to top of list
|
# reagent_role = None
|
||||||
index = self.type_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith)
|
# # NOTE: set parsed reagent type to top of list
|
||||||
if index >= 0:
|
# index = self.role_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith)
|
||||||
self.type_input.setCurrentIndex(index)
|
# if index >= 0:
|
||||||
self.layout = QVBoxLayout()
|
# self.role_input.setCurrentIndex(index)
|
||||||
self.layout.addWidget(QLabel("Name:"))
|
# self.layout = QVBoxLayout()
|
||||||
self.layout.addWidget(self.name_input)
|
# self.layout.addWidget(QLabel("Name:"))
|
||||||
self.layout.addWidget(QLabel("Lot:"))
|
# self.layout.addWidget(self.name_input)
|
||||||
self.layout.addWidget(self.lot_input)
|
# self.layout.addWidget(QLabel("Lot:"))
|
||||||
self.layout.addWidget(
|
# self.layout.addWidget(self.lot_input)
|
||||||
QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
|
# self.layout.addWidget(
|
||||||
self.layout.addWidget(self.exp_input)
|
# QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)")
|
||||||
self.layout.addWidget(QLabel("Type:"))
|
# )
|
||||||
self.layout.addWidget(self.type_input)
|
# self.layout.addWidget(self.expiry_input)
|
||||||
self.layout.addWidget(self.buttonBox)
|
# self.layout.addWidget(QLabel("Type:"))
|
||||||
self.setLayout(self.layout)
|
# self.layout.addWidget(self.role_input)
|
||||||
self.type_input.currentTextChanged.connect(self.update_names)
|
# self.layout.addWidget(self.buttonBox)
|
||||||
|
# self.setLayout(self.layout)
|
||||||
def parse_form(self) -> dict:
|
# self.role_input.currentTextChanged.connect(self.update_names)
|
||||||
"""
|
#
|
||||||
Converts information in form to dict.
|
# def parse_form(self) -> dict:
|
||||||
|
# """
|
||||||
Returns:
|
# Converts information in form to dict.
|
||||||
dict: Output info
|
#
|
||||||
"""
|
# Returns:
|
||||||
return dict(name=self.name_input.currentText().strip(),
|
# dict: Output info
|
||||||
lot=self.lot_input.text().strip(),
|
# """
|
||||||
expiry=self.exp_input.date().toPyDate(),
|
# return dict(name=self.name_input.currentText().strip(),
|
||||||
role=self.type_input.currentText().strip())
|
# lot=self.lot_input.text().strip(),
|
||||||
|
# expiry=self.expiry_input.date().toPyDate(),
|
||||||
def update_names(self):
|
# role=self.role_input.currentText().strip())
|
||||||
"""
|
#
|
||||||
Updates reagent names form field with examples from reagent type
|
# def update_names(self):
|
||||||
"""
|
# """
|
||||||
self.name_input.clear()
|
# Updates reagent names form field with examples from reagent type
|
||||||
lookup = Reagent.query(role=self.type_input.currentText())
|
# """
|
||||||
self.name_input.addItems(list(set([item.name for item in lookup])))
|
# self.name_input.clear()
|
||||||
|
# lookup = Reagent.query(role=self.role_input.currentText())
|
||||||
|
# self.name_input.addItems(list(set([item.name for item in lookup])))
|
||||||
|
#
|
||||||
|
#
|
||||||
class StartEndDatePicker(QWidget):
|
class StartEndDatePicker(QWidget):
|
||||||
"""
|
"""
|
||||||
custom widget to pick start and end dates for controls graphs
|
custom widget to pick start and end dates for controls graphs
|
||||||
@@ -135,12 +135,12 @@ class StartEndDatePicker(QWidget):
|
|||||||
return QSize(80, 20)
|
return QSize(80, 20)
|
||||||
|
|
||||||
|
|
||||||
def save_pdf(obj: QWebEngineView, filename: Path):
|
# def save_pdf(obj: QWebEngineView, filename: Path):
|
||||||
page_layout = QPageLayout()
|
# page_layout = QPageLayout()
|
||||||
page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
|
# page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
|
||||||
page_layout.setOrientation(QPageLayout.Orientation.Portrait)
|
# page_layout.setOrientation(QPageLayout.Orientation.Portrait)
|
||||||
page_layout.setMargins(QMarginsF(25, 25, 25, 25))
|
# page_layout.setMargins(QMarginsF(25, 25, 25, 25))
|
||||||
obj.page().printToPdf(filename.absolute().__str__(), page_layout)
|
# obj.page().printToPdf(filename.absolute().__str__(), page_layout)
|
||||||
|
|
||||||
|
|
||||||
# NOTE: subclass
|
# NOTE: subclass
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
|
"""
|
||||||
|
A widget to handle adding/updating any database object.
|
||||||
|
"""
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Any, List, Tuple
|
from typing import Any, Tuple
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QDialog, QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit
|
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox
|
||||||
)
|
)
|
||||||
from sqlalchemy import String, TIMESTAMP
|
from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT
|
||||||
from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
|
from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
||||||
|
from tools import Report, report_result
|
||||||
from tools import Report, Result, report_result
|
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ class AddEdit(QDialog):
|
|||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.object_type = instance.__class__
|
self.object_type = instance.__class__
|
||||||
self.layout = QGridLayout(self)
|
self.layout = QGridLayout(self)
|
||||||
# logger.debug(f"Manager: {manager}")
|
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
@@ -36,7 +36,6 @@ class AddEdit(QDialog):
|
|||||||
fields = {'name': fields.pop('name'), **fields}
|
fields = {'name': fields.pop('name'), **fields}
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
# logger.debug(pformat(fields, indent=4))
|
|
||||||
height_counter = 0
|
height_counter = 0
|
||||||
for key, field in fields.items():
|
for key, field in fields.items():
|
||||||
try:
|
try:
|
||||||
@@ -45,9 +44,6 @@ class AddEdit(QDialog):
|
|||||||
value = None
|
value = None
|
||||||
try:
|
try:
|
||||||
logger.debug(f"{key} property: {type(field['class_attr'].property)}")
|
logger.debug(f"{key} property: {type(field['class_attr'].property)}")
|
||||||
# widget = EditProperty(self, key=key, column_type=field.property.expression.type,
|
|
||||||
# value=getattr(self.instance, key))
|
|
||||||
# logger.debug(f"Column type: {field}, Value: {value}")
|
|
||||||
widget = EditProperty(self, key=key, column_type=field, value=value)
|
widget = EditProperty(self, key=key, column_type=field, value=value)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.error(f"Problem setting widget {key}: {e}")
|
logger.error(f"Problem setting widget {key}: {e}")
|
||||||
@@ -64,15 +60,11 @@ class AddEdit(QDialog):
|
|||||||
def parse_form(self) -> Tuple[BaseModel, Report]:
|
def parse_form(self) -> Tuple[BaseModel, Report]:
|
||||||
report = Report()
|
report = Report()
|
||||||
parsed = {result[0].strip(":"): result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
|
parsed = {result[0].strip(":"): result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
|
||||||
logger.debug(parsed)
|
# logger.debug(parsed)
|
||||||
model = self.object_type.pydantic_model
|
model = self.object_type.pydantic_model
|
||||||
# NOTE: Hand-off to pydantic model for validation.
|
# NOTE: Hand-off to pydantic model for validation.
|
||||||
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
|
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
|
||||||
model = model(**parsed)
|
model = model(**parsed)
|
||||||
# output, result = model.to_sql()
|
|
||||||
# report.add_result(result)
|
|
||||||
# if len(report.results) < 1:
|
|
||||||
# report.add_result(Result(msg="Added new regeant.", icon="Information", owner=__name__))
|
|
||||||
return model, report
|
return model, report
|
||||||
|
|
||||||
|
|
||||||
@@ -84,7 +76,7 @@ class EditProperty(QWidget):
|
|||||||
self.label = QLabel(key.title().replace("_", " "))
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
self.layout = QGridLayout()
|
self.layout = QGridLayout()
|
||||||
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
self.setObjectName(f"{key}:")
|
self.setObjectName(key)
|
||||||
match column_type['class_attr'].property:
|
match column_type['class_attr'].property:
|
||||||
case ColumnProperty():
|
case ColumnProperty():
|
||||||
self.column_property_set(column_type, value=value)
|
self.column_property_set(column_type, value=value)
|
||||||
@@ -97,15 +89,15 @@ class EditProperty(QWidget):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def relationship_property_set(self, relationship_property, value=None):
|
def relationship_property_set(self, relationship_property, value=None):
|
||||||
# print(relationship_property)
|
|
||||||
self.property_class = relationship_property['class_attr'].property.entity.class_
|
self.property_class = relationship_property['class_attr'].property.entity.class_
|
||||||
self.is_list = relationship_property['class_attr'].property.uselist
|
self.is_list = relationship_property['class_attr'].property.uselist
|
||||||
choices = [item.name for item in self.property_class.query()]
|
choices = [""] + [item.name for item in self.property_class.query()]
|
||||||
try:
|
try:
|
||||||
instance_value = getattr(self.parent().instance, self.name)
|
instance_value = getattr(self.parent().instance, self.objectName())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.error(f"Unable to get instance {self.parent().instance} attribute: {self.name}")
|
logger.error(f"Unable to get instance {self.parent().instance} attribute: {self.objectName()}")
|
||||||
instance_value = None
|
instance_value = None
|
||||||
|
# NOTE: get the value for the current instance and move it to the front.
|
||||||
if isinstance(instance_value, list):
|
if isinstance(instance_value, list):
|
||||||
instance_value = next((item.name for item in instance_value), None)
|
instance_value = next((item.name for item in instance_value), None)
|
||||||
if instance_value:
|
if instance_value:
|
||||||
@@ -120,6 +112,16 @@ class EditProperty(QWidget):
|
|||||||
value = ""
|
value = ""
|
||||||
self.widget = QLineEdit(self)
|
self.widget = QLineEdit(self)
|
||||||
self.widget.setText(value)
|
self.widget.setText(value)
|
||||||
|
case INTEGER():
|
||||||
|
if not value:
|
||||||
|
value = 1
|
||||||
|
self.widget = QSpinBox()
|
||||||
|
self.widget.setValue(value)
|
||||||
|
case FLOAT():
|
||||||
|
if not value:
|
||||||
|
value = 1.0
|
||||||
|
self.widget = QDoubleSpinBox()
|
||||||
|
self.widget.setValue(value)
|
||||||
case TIMESTAMP():
|
case TIMESTAMP():
|
||||||
self.widget = QDateEdit(self, calendarPopup=True)
|
self.widget = QDateEdit(self, calendarPopup=True)
|
||||||
if not value:
|
if not value:
|
||||||
@@ -129,12 +131,13 @@ class EditProperty(QWidget):
|
|||||||
logger.error(f"{column_property} not a supported property.")
|
logger.error(f"{column_property} not a supported property.")
|
||||||
self.widget = None
|
self.widget = None
|
||||||
try:
|
try:
|
||||||
tooltip_text = self.parent().object_type.add_edit_tooltips[self.name]
|
tooltip_text = self.parent().object_type.add_edit_tooltips[self.objectName()]
|
||||||
self.widget.setToolTip(tooltip_text)
|
self.widget.setToolTip(tooltip_text)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def parse_form(self):
|
def parse_form(self):
|
||||||
|
# NOTE: Make sure there's a widget.
|
||||||
try:
|
try:
|
||||||
check = self.widget
|
check = self.widget
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -146,10 +149,12 @@ class EditProperty(QWidget):
|
|||||||
value = self.widget.date().toPyDate()
|
value = self.widget.date().toPyDate()
|
||||||
case QComboBox():
|
case QComboBox():
|
||||||
value = self.widget.currentText()
|
value = self.widget.currentText()
|
||||||
|
case QSpinBox() | QDoubleSpinBox():
|
||||||
|
value = self.widget.value()
|
||||||
# if self.is_list:
|
# if self.is_list:
|
||||||
# value = [self.property_class.query(name=prelim)]
|
# value = [self.property_class.query(name=prelim)]
|
||||||
# else:
|
# else:
|
||||||
# value = self.property_class.query(name=prelim)
|
# value = self.property_class.query(name=prelim)
|
||||||
case _:
|
case _:
|
||||||
value = None
|
value = None
|
||||||
return self.name, value
|
return self.objectName(), value
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from operator import itemgetter
|
"""
|
||||||
|
Provides a screen for managing all attributes of a database object.
|
||||||
|
"""
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
from PyQt6.QtCore import QSortFilterProxyModel, Qt
|
from PyQt6.QtCore import QSortFilterProxyModel, Qt
|
||||||
from PyQt6.QtGui import QAction, QCursor
|
from PyQt6.QtGui import QAction, QCursor
|
||||||
@@ -49,21 +51,22 @@ class ManagerWindow(QDialog):
|
|||||||
self.layout.addWidget(self.sub_class, 0, 0)
|
self.layout.addWidget(self.sub_class, 0, 0)
|
||||||
else:
|
else:
|
||||||
self.sub_class = None
|
self.sub_class = None
|
||||||
# self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0)
|
|
||||||
self.options = QComboBox(self)
|
self.options = QComboBox(self)
|
||||||
self.options.setObjectName("options")
|
self.options.setObjectName("options")
|
||||||
self.update_options()
|
self.update_options()
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
self.setWindowTitle(f"Manage {self.object_type.__name__}")
|
self.setWindowTitle(f"Manage {self.object_type.__name__}")
|
||||||
|
|
||||||
def update_options(self):
|
def update_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
Changes form inputs based on sample type
|
Changes form inputs based on sample type
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.sub_class:
|
if self.sub_class:
|
||||||
self.object_type = getattr(db, self.sub_class.currentText())
|
self.object_type = getattr(db, self.sub_class.currentText())
|
||||||
options = [item.name for item in self.object_type.query()]
|
options = [item.name for item in self.object_type.query()]
|
||||||
|
logger.debug(f"self.instance: {self.instance}")
|
||||||
|
if self.instance:
|
||||||
|
options.insert(0, options.pop(options.index(self.instance.name)))
|
||||||
self.options.clear()
|
self.options.clear()
|
||||||
self.options.addItems(options)
|
self.options.addItems(options)
|
||||||
self.options.setEditable(False)
|
self.options.setEditable(False)
|
||||||
@@ -75,23 +78,34 @@ class ManagerWindow(QDialog):
|
|||||||
self.add_button.clicked.connect(self.add_new)
|
self.add_button.clicked.connect(self.add_new)
|
||||||
self.update_data()
|
self.update_data()
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self) -> None:
|
||||||
|
"""
|
||||||
|
Performs updating of widgets on first run and after options change.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
# NOTE: Remove all old widgets.
|
||||||
deletes = [item for item in self.findChildren(EditProperty)] + \
|
deletes = [item for item in self.findChildren(EditProperty)] + \
|
||||||
[item for item in self.findChildren(EditRelationship)] + \
|
[item for item in self.findChildren(EditRelationship)] + \
|
||||||
[item for item in self.findChildren(QDialogButtonBox)]
|
[item for item in self.findChildren(QDialogButtonBox)]
|
||||||
for item in deletes:
|
for item in deletes:
|
||||||
item.setParent(None)
|
item.setParent(None)
|
||||||
|
# NOTE: Find the instance this manager will update
|
||||||
self.instance = self.object_type.query(name=self.options.currentText())
|
self.instance = self.object_type.query(name=self.options.currentText())
|
||||||
fields = {k: v for k, v in self.object_type.__dict__.items() if
|
fields = {k: v for k, v in self.object_type.__dict__.items() if
|
||||||
isinstance(v, InstrumentedAttribute) and k != "id"}
|
isinstance(v, InstrumentedAttribute) and k != "id"}
|
||||||
for key, field in fields.items():
|
for key, field in fields.items():
|
||||||
# logger.debug(f"Key: {key}, Value: {field}")
|
|
||||||
match field.property:
|
match field.property:
|
||||||
|
# NOTE: ColumnProperties will be directly edited.
|
||||||
case ColumnProperty():
|
case ColumnProperty():
|
||||||
|
# NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP
|
||||||
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
|
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
|
||||||
value=getattr(self.instance, key))
|
value=getattr(self.instance, key))
|
||||||
|
# NOTE: RelationshipDeclareds will be given a list of existing related objects.
|
||||||
case _RelationshipDeclared():
|
case _RelationshipDeclared():
|
||||||
if key != "submissions":
|
if key != "submissions":
|
||||||
|
# NOTE: field.comparator.entity.class_ gives the relationship class
|
||||||
widget = EditRelationship(self, key=key, entity=field.comparator.entity.class_,
|
widget = EditRelationship(self, key=key, entity=field.comparator.entity.class_,
|
||||||
value=getattr(self.instance, key))
|
value=getattr(self.instance, key))
|
||||||
else:
|
else:
|
||||||
@@ -100,11 +114,18 @@ class ManagerWindow(QDialog):
|
|||||||
continue
|
continue
|
||||||
if widget:
|
if widget:
|
||||||
self.layout.addWidget(widget, self.layout.rowCount(), 0, 1, 2)
|
self.layout.addWidget(widget, self.layout.rowCount(), 0, 1, 2)
|
||||||
|
# NOTE: Add OK|Cancel to bottom of dialog.
|
||||||
self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2)
|
self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2)
|
||||||
|
|
||||||
def parse_form(self):
|
def parse_form(self) -> Any:
|
||||||
|
"""
|
||||||
|
Returns the instance associated with this window.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The instance with updated fields.
|
||||||
|
"""
|
||||||
|
# TODO: Need Relationship property here too?
|
||||||
results = [item.parse_form() for item in self.findChildren(EditProperty)]
|
results = [item.parse_form() for item in self.findChildren(EditProperty)]
|
||||||
# logger.debug(results)
|
|
||||||
for result in results:
|
for result in results:
|
||||||
# logger.debug(result)
|
# logger.debug(result)
|
||||||
self.instance.__setattr__(result[0], result[1])
|
self.instance.__setattr__(result[0], result[1])
|
||||||
@@ -113,9 +134,10 @@ class ManagerWindow(QDialog):
|
|||||||
def add_new(self):
|
def add_new(self):
|
||||||
dlg = AddEdit(parent=self, instance=self.object_type(), manager=self.object_type.__name__.lower())
|
dlg = AddEdit(parent=self, instance=self.object_type(), manager=self.object_type.__name__.lower())
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
new_instance = dlg.parse_form()
|
new_pyd = dlg.parse_form()
|
||||||
# logger.debug(new_instance.__dict__)
|
new_instance = new_pyd.to_sql()
|
||||||
new_instance.save()
|
new_instance.save()
|
||||||
|
self.instance = new_instance
|
||||||
self.update_options()
|
self.update_options()
|
||||||
|
|
||||||
|
|
||||||
@@ -222,10 +244,13 @@ class EditRelationship(QWidget):
|
|||||||
self.data['id'] = self.data['id'].apply(str)
|
self.data['id'] = self.data['id'].apply(str)
|
||||||
self.data['id'] = self.data['id'].str.zfill(4)
|
self.data['id'] = self.data['id'].str.zfill(4)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.error(f"Could not alter id to string due to {e}")
|
logger.error(f"Could not alter id to string due to KeyError: {e}")
|
||||||
proxy_model = QSortFilterProxyModel()
|
proxy_model = QSortFilterProxyModel()
|
||||||
proxy_model.setSourceModel(pandasModel(self.data))
|
proxy_model.setSourceModel(pandasModel(self.data))
|
||||||
self.table.setModel(proxy_model)
|
self.table.setModel(proxy_model)
|
||||||
|
self.table.resizeColumnsToContents()
|
||||||
|
self.table.resizeRowsToContents()
|
||||||
|
self.table.setSortingEnabled(True)
|
||||||
self.table.doubleClicked.connect(self.parse_row)
|
self.table.doubleClicked.connect(self.parse_row)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
Search box that performs fuzzy search for various object types
|
Search box that performs fuzzy search for various object types
|
||||||
"""
|
"""
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Tuple, Any, List
|
from typing import Tuple, Any, List, Generator
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from PyQt6.QtCore import QSortFilterProxyModel
|
from PyQt6.QtCore import QSortFilterProxyModel, QModelIndex
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QVBoxLayout, QDialog,
|
QLabel, QVBoxLayout, QDialog,
|
||||||
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox
|
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox
|
||||||
@@ -74,7 +74,6 @@ class SearchBox(QDialog):
|
|||||||
search_fields = []
|
search_fields = []
|
||||||
for iii, searchable in enumerate(search_fields):
|
for iii, searchable in enumerate(search_fields):
|
||||||
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field'])
|
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field'])
|
||||||
# widget = FieldSearch(parent=self, label=k, field_name=v)
|
|
||||||
widget.setObjectName(searchable['field'])
|
widget.setObjectName(searchable['field'])
|
||||||
self.layout.addWidget(widget, 1 + iii, 0)
|
self.layout.addWidget(widget, 1 + iii, 0)
|
||||||
widget.search_widget.textChanged.connect(self.update_data)
|
widget.search_widget.textChanged.connect(self.update_data)
|
||||||
@@ -100,11 +99,17 @@ class SearchBox(QDialog):
|
|||||||
# NOTE: Setting results moved to here from __init__ 202411118
|
# NOTE: Setting results moved to here from __init__ 202411118
|
||||||
self.results.setData(df=data)
|
self.results.setData(df=data)
|
||||||
|
|
||||||
def return_selected_rows(self):
|
def return_selected_rows(self) -> Generator[dict, None, None]:
|
||||||
rows = sorted(set(index.row() for index in
|
"""
|
||||||
self.results.selectedIndexes()))
|
Yields data from selected rows
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary of column name: data
|
||||||
|
"""
|
||||||
|
rows = sorted(set(index.row() for index in self.results.selectedIndexes()))
|
||||||
for index in rows:
|
for index in rows:
|
||||||
output = {column:self.results.model().data(self.results.model().index(index, ii)) for ii, column in enumerate(self.results.data.columns)}
|
output = {column: self.results.model().data(self.results.model().index(index, ii)) for ii, column in
|
||||||
|
enumerate(self.results.data.columns)}
|
||||||
yield output
|
yield output
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +135,13 @@ class FieldSearch(QWidget):
|
|||||||
"""
|
"""
|
||||||
self.parent().update_data()
|
self.parent().update_data()
|
||||||
|
|
||||||
def parse_form(self) -> Tuple:
|
def parse_form(self) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Gets object name and widget value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple(str, str): Key, value to be used in constructing a dictionary.
|
||||||
|
"""
|
||||||
field_value = self.search_widget.text()
|
field_value = self.search_widget.text()
|
||||||
if field_value == "":
|
if field_value == "":
|
||||||
field_value = None
|
field_value = None
|
||||||
@@ -147,6 +158,7 @@ class SearchResults(QTableView):
|
|||||||
self.context = kwargs
|
self.context = kwargs
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.object_type = object_type
|
self.object_type = object_type
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.extras = extras + self.object_type.searchables
|
self.extras = extras + self.object_type.searchables
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -160,7 +172,8 @@ class SearchResults(QTableView):
|
|||||||
|
|
||||||
self.data = df
|
self.data = df
|
||||||
try:
|
try:
|
||||||
self.columns_of_interest = [dict(name=item['field'], column=self.data.columns.get_loc(item['field'])) for item in self.extras]
|
self.columns_of_interest = [dict(name=item['field'], column=self.data.columns.get_loc(item['field'])) for
|
||||||
|
item in self.extras]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.columns_of_interest = []
|
self.columns_of_interest = []
|
||||||
try:
|
try:
|
||||||
@@ -171,9 +184,21 @@ class SearchResults(QTableView):
|
|||||||
proxy_model = QSortFilterProxyModel()
|
proxy_model = QSortFilterProxyModel()
|
||||||
proxy_model.setSourceModel(pandasModel(self.data))
|
proxy_model.setSourceModel(pandasModel(self.data))
|
||||||
self.setModel(proxy_model)
|
self.setModel(proxy_model)
|
||||||
|
self.resizeColumnsToContents()
|
||||||
|
self.resizeRowsToContents()
|
||||||
|
self.setSortingEnabled(True)
|
||||||
self.doubleClicked.connect(self.parse_row)
|
self.doubleClicked.connect(self.parse_row)
|
||||||
|
|
||||||
def parse_row(self, x):
|
def parse_row(self, x: QModelIndex) -> None:
|
||||||
|
"""
|
||||||
|
Runs the self.object_type edit from search method for row X.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (QModelIndex): Row to be parsed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest}
|
context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest}
|
||||||
try:
|
try:
|
||||||
object = self.object_type.query(**context)
|
object = self.object_type.query(**context)
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ from PyQt6.QtWebChannel import QWebChannel
|
|||||||
from PyQt6.QtCore import Qt, pyqtSlot
|
from PyQt6.QtCore import Qt, pyqtSlot
|
||||||
from jinja2 import TemplateNotFound
|
from jinja2 import TemplateNotFound
|
||||||
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
|
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
|
||||||
from tools import is_power_user, jinja_template_loading, timezone
|
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
|
||||||
from .functions import select_save_file
|
from .functions import select_save_file, save_pdf
|
||||||
from .misc import save_pdf
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
@@ -30,13 +29,11 @@ class SubmissionDetails(QDialog):
|
|||||||
def __init__(self, parent, sub: BasicSubmission | BasicSample | Reagent) -> None:
|
def __init__(self, parent, sub: BasicSubmission | BasicSample | Reagent) -> None:
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
try:
|
self.app = get_application_from_parent(parent)
|
||||||
self.app = parent.parent().parent().parent().parent().parent().parent()
|
|
||||||
except AttributeError:
|
|
||||||
self.app = None
|
|
||||||
self.webview = QWebEngineView(parent=self)
|
self.webview = QWebEngineView(parent=self)
|
||||||
self.webview.setMinimumSize(900, 500)
|
self.webview.setMinimumSize(900, 500)
|
||||||
self.webview.setMaximumWidth(900)
|
self.webview.setMaximumWidth(900)
|
||||||
|
# NOTE: Decide if exporting should be allowed.
|
||||||
self.webview.loadFinished.connect(self.activate_export)
|
self.webview.loadFinished.connect(self.activate_export)
|
||||||
self.layout = QGridLayout()
|
self.layout = QGridLayout()
|
||||||
# NOTE: button to export a pdf version
|
# NOTE: button to export a pdf version
|
||||||
@@ -61,9 +58,16 @@ class SubmissionDetails(QDialog):
|
|||||||
self.sample_details(sample=sub)
|
self.sample_details(sample=sub)
|
||||||
case Reagent():
|
case Reagent():
|
||||||
self.reagent_details(reagent=sub)
|
self.reagent_details(reagent=sub)
|
||||||
|
# NOTE: Used to maintain javascript functions.
|
||||||
self.webview.page().setWebChannel(self.channel)
|
self.webview.page().setWebChannel(self.channel)
|
||||||
|
|
||||||
def activate_export(self):
|
def activate_export(self) -> None:
|
||||||
|
"""
|
||||||
|
Determines if export pdf should be active.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
title = self.webview.title()
|
title = self.webview.title()
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
if "Submission" in title:
|
if "Submission" in title:
|
||||||
@@ -103,6 +107,13 @@ class SubmissionDetails(QDialog):
|
|||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
|
def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
|
||||||
|
"""
|
||||||
|
Changes details view to summary of Reagent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kit (str | KitType): Name of kit.
|
||||||
|
reagent (str | Reagent): Lot number of the reagent
|
||||||
|
"""
|
||||||
if isinstance(reagent, str):
|
if isinstance(reagent, str):
|
||||||
reagent = Reagent.query(lot=reagent)
|
reagent = Reagent.query(lot=reagent)
|
||||||
if isinstance(kit, str):
|
if isinstance(kit, str):
|
||||||
@@ -124,6 +135,17 @@ class SubmissionDetails(QDialog):
|
|||||||
|
|
||||||
@pyqtSlot(str, str, str)
|
@pyqtSlot(str, str, str)
|
||||||
def update_reagent(self, old_lot: str, new_lot: str, expiry: str):
|
def update_reagent(self, old_lot: str, new_lot: str, expiry: str):
|
||||||
|
"""
|
||||||
|
Designed to allow editing reagent in details view (depreciated)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
old_lot ():
|
||||||
|
new_lot ():
|
||||||
|
expiry ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
expiry = datetime.strptime(expiry, "%Y-%m-%d")
|
expiry = datetime.strptime(expiry, "%Y-%m-%d")
|
||||||
reagent = Reagent.query(lot=old_lot)
|
reagent = Reagent.query(lot=old_lot)
|
||||||
if reagent:
|
if reagent:
|
||||||
@@ -157,7 +179,16 @@ class SubmissionDetails(QDialog):
|
|||||||
self.webview.setHtml(self.html)
|
self.webview.setHtml(self.html)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def sign_off(self, submission: str | BasicSubmission):
|
def sign_off(self, submission: str | BasicSubmission) -> None:
|
||||||
|
"""
|
||||||
|
Allows power user to signify a submission is complete.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
submission (str | BasicSubmission): Submission to be completed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
logger.info(f"Signing off on {submission} - ({getuser()})")
|
logger.info(f"Signing off on {submission} - ({getuser()})")
|
||||||
if isinstance(submission, str):
|
if isinstance(submission, str):
|
||||||
submission = BasicSubmission.query(rsl_plate_num=submission)
|
submission = BasicSubmission.query(rsl_plate_num=submission)
|
||||||
@@ -183,10 +214,7 @@ class SubmissionComment(QDialog):
|
|||||||
def __init__(self, parent, submission: BasicSubmission) -> None:
|
def __init__(self, parent, submission: BasicSubmission) -> None:
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
try:
|
self.app = get_application_from_parent(parent)
|
||||||
self.app = parent.parent().parent().parent().parent().parent().parent
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
self.submission = submission
|
self.submission = submission
|
||||||
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
|
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
|
||||||
# NOTE: create text field
|
# NOTE: create text field
|
||||||
|
|||||||
@@ -65,12 +65,6 @@ class SubmissionsSheet(QTableView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent) -> None:
|
def __init__(self, parent) -> None:
|
||||||
"""
|
|
||||||
initialize
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ctx (dict): settings passed from gui
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.app = self.parent()
|
self.app = self.parent()
|
||||||
self.report = Report()
|
self.report = Report()
|
||||||
@@ -107,7 +101,9 @@ class SubmissionsSheet(QTableView):
|
|||||||
Args:
|
Args:
|
||||||
event (_type_): the item of interest
|
event (_type_): the item of interest
|
||||||
"""
|
"""
|
||||||
|
# NOTE: Get current row index
|
||||||
id = self.selectionModel().currentIndex()
|
id = self.selectionModel().currentIndex()
|
||||||
|
# NOTE: Convert to data in id column (i.e. column 0)
|
||||||
id = id.sibling(id.row(), 0).data()
|
id = id.sibling(id.row(), 0).data()
|
||||||
submission = BasicSubmission.query(id=id)
|
submission = BasicSubmission.query(id=id)
|
||||||
self.menu = QMenu(self)
|
self.menu = QMenu(self)
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker
|
|||||||
from .functions import select_open_file, select_save_file
|
from .functions import select_open_file, select_save_file
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools import Report, Result, check_not_nan, main_form_style, report_result
|
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
|
||||||
from backend.excel.parser import SheetParser
|
from backend.excel.parser import SheetParser
|
||||||
from backend.validators import PydSubmission, PydReagent
|
from backend.validators import PydSubmission, PydReagent
|
||||||
from backend.db import (
|
from backend.db import (
|
||||||
KitType, Organization, SubmissionType, Reagent,
|
Organization, SubmissionType, Reagent,
|
||||||
ReagentRole, KitTypeReagentRoleAssociation, BasicSubmission
|
ReagentRole, KitTypeReagentRoleAssociation, BasicSubmission
|
||||||
)
|
)
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from .pop_ups import QuestionAsker, AlertPop
|
from .pop_ups import QuestionAsker, AlertPop
|
||||||
from .misc import AddReagentForm
|
|
||||||
from .omni_add_edit import AddEdit
|
from .omni_add_edit import AddEdit
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -67,7 +66,6 @@ class SubmissionFormContainer(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()
|
||||||
self.report = Report()
|
|
||||||
self.setStyleSheet('background-color: light grey;')
|
self.setStyleSheet('background-color: light grey;')
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
# NOTE: if import_drag is emitted, importSubmission will fire
|
# NOTE: if import_drag is emitted, importSubmission will fire
|
||||||
@@ -97,12 +95,12 @@ class SubmissionFormContainer(QWidget):
|
|||||||
"""
|
"""
|
||||||
self.app.raise_()
|
self.app.raise_()
|
||||||
self.app.activateWindow()
|
self.app.activateWindow()
|
||||||
self.report = Report()
|
report = Report()
|
||||||
self.import_submission_function(fname)
|
self.import_submission_function(fname)
|
||||||
return self.report
|
return report
|
||||||
|
|
||||||
@report_result
|
@report_result
|
||||||
def import_submission_function(self, fname: Path | None = None):
|
def import_submission_function(self, fname: Path | None = None) -> Report:
|
||||||
"""
|
"""
|
||||||
Import a new submission to the app window
|
Import a new submission to the app window
|
||||||
|
|
||||||
@@ -110,10 +108,11 @@ class SubmissionFormContainer(QWidget):
|
|||||||
obj (QMainWindow): original app window
|
obj (QMainWindow): original app window
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[QMainWindow, dict|None]: Collection of new main app window and result dict
|
Report: Object to give results of import.
|
||||||
"""
|
"""
|
||||||
logger.info(f"\n\nStarting Import...\n\n")
|
logger.info(f"\n\nStarting Import...\n\n")
|
||||||
report = Report()
|
report = Report()
|
||||||
|
# NOTE: Clear any previous forms.
|
||||||
try:
|
try:
|
||||||
self.form.setParent(None)
|
self.form.setParent(None)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -141,7 +140,16 @@ class SubmissionFormContainer(QWidget):
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
@report_result
|
@report_result
|
||||||
def new_add_reagent(self, instance: Reagent | None = None):
|
def add_reagent(self, instance: Reagent | None = None):
|
||||||
|
"""
|
||||||
|
Action to create new reagent in DB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance (Reagent | None): Blank reagent instance to be edited and then added.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
models.Reagent: the constructed reagent object to add to submission
|
||||||
|
"""
|
||||||
report = Report()
|
report = Report()
|
||||||
if not instance:
|
if not instance:
|
||||||
instance = Reagent()
|
instance = Reagent()
|
||||||
@@ -149,48 +157,12 @@ class SubmissionFormContainer(QWidget):
|
|||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
reagent = dlg.parse_form()
|
reagent = dlg.parse_form()
|
||||||
reagent.missing = False
|
reagent.missing = False
|
||||||
# logger.debug(f"Reagent: {reagent}, result: {result}")
|
|
||||||
# report.add_result(result)
|
|
||||||
# NOTE: send reagent to db
|
# NOTE: send reagent to db
|
||||||
sqlobj = reagent.to_sql()
|
sqlobj = reagent.to_sql()
|
||||||
sqlobj.save()
|
sqlobj.save()
|
||||||
logger.debug(f"Reagent added!")
|
|
||||||
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information"))
|
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information"))
|
||||||
# report.add_result(result)
|
|
||||||
return reagent, report
|
return reagent, report
|
||||||
|
|
||||||
@report_result
|
|
||||||
def add_reagent(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
|
|
||||||
name: str | None = None, kit: str | KitType | None = None) -> Tuple[PydReagent, Report]:
|
|
||||||
"""
|
|
||||||
Action to create new reagent in DB.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
reagent_lot (str | None, optional): Parsed reagent from import form. Defaults to None.
|
|
||||||
reagent_role (str | None, optional): Parsed reagent type from import form. Defaults to None.
|
|
||||||
expiry (date | None, optional): Parsed reagent expiry data. Defaults to None.
|
|
||||||
name (str | None, optional): Parsed reagent name. Defaults to None.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
models.Reagent: the constructed reagent object to add to submission
|
|
||||||
"""
|
|
||||||
report = Report()
|
|
||||||
if isinstance(reagent_lot, bool):
|
|
||||||
reagent_lot = ""
|
|
||||||
# NOTE: create form
|
|
||||||
dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_role=reagent_role, expiry=expiry, reagent_name=name,
|
|
||||||
kit=kit)
|
|
||||||
if dlg.exec():
|
|
||||||
# NOTE: extract form info
|
|
||||||
info = dlg.parse_form()
|
|
||||||
# NOTE: create reagent object
|
|
||||||
reagent = PydReagent(ctx=self.app.ctx, **info, missing=False)
|
|
||||||
# NOTE: send reagent to db
|
|
||||||
sqlobj = reagent.to_sql()
|
|
||||||
sqlobj.save()
|
|
||||||
# report.add_result(result)
|
|
||||||
return reagent
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionFormWidget(QWidget):
|
class SubmissionFormWidget(QWidget):
|
||||||
update_reagent_fields = ['extraction_kit']
|
update_reagent_fields = ['extraction_kit']
|
||||||
@@ -199,12 +171,12 @@ class SubmissionFormWidget(QWidget):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
if disable is None:
|
if disable is None:
|
||||||
disable = []
|
disable = []
|
||||||
self.app = parent.app
|
self.app = get_application_from_parent(parent)
|
||||||
self.pyd = submission
|
self.pyd = submission
|
||||||
self.missing_info = []
|
self.missing_info = []
|
||||||
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
|
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
|
||||||
st = self.submission_type.submission_class
|
basic_submission_class = self.submission_type.submission_class
|
||||||
defaults = st.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
|
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
|
||||||
self.recover = defaults['form_recover']
|
self.recover = defaults['form_recover']
|
||||||
self.ignore = defaults['form_ignore']
|
self.ignore = defaults['form_ignore']
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
@@ -225,7 +197,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
value = dict(value=None, missing=True)
|
value = dict(value=None, missing=True)
|
||||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
|
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
|
||||||
sub_obj=st, disable=check)
|
sub_obj=basic_submission_class, disable=check)
|
||||||
if add_widget is not None:
|
if add_widget is not None:
|
||||||
self.layout.addWidget(add_widget)
|
self.layout.addWidget(add_widget)
|
||||||
if k in self.__class__.update_reagent_fields:
|
if k in self.__class__.update_reagent_fields:
|
||||||
@@ -302,10 +274,9 @@ class SubmissionFormWidget(QWidget):
|
|||||||
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
|
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
|
||||||
reagent.setParent(None)
|
reagent.setParent(None)
|
||||||
reagents, integrity_report, missing_reagents = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit)
|
reagents, integrity_report, missing_reagents = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit)
|
||||||
logger.debug(f"Reagents: {reagents}")
|
# logger.debug(f"Reagents: {reagents}")
|
||||||
expiry_report = self.pyd.check_reagent_expiries(exempt=missing_reagents)
|
expiry_report = self.pyd.check_reagent_expiries(exempt=missing_reagents)
|
||||||
for reagent in reagents:
|
for reagent in reagents:
|
||||||
|
|
||||||
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit)
|
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit)
|
||||||
self.layout.addWidget(add_widget)
|
self.layout.addWidget(add_widget)
|
||||||
report.add_result(integrity_report)
|
report.add_result(integrity_report)
|
||||||
@@ -340,9 +311,12 @@ class SubmissionFormWidget(QWidget):
|
|||||||
Returns:
|
Returns:
|
||||||
List[QWidget]: Widgets matching filter
|
List[QWidget]: Widgets matching filter
|
||||||
"""
|
"""
|
||||||
|
if object_name:
|
||||||
|
query = self.findChildren(QWidget, name=object_name)
|
||||||
|
else:
|
||||||
query = self.findChildren(QWidget)
|
query = self.findChildren(QWidget)
|
||||||
if object_name is not None:
|
# if object_name is not None:
|
||||||
query = [widget for widget in query if widget.objectName() == object_name]
|
# query = [widget for widget in query if widget.objectName() == object_name]
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@report_result
|
@report_result
|
||||||
@@ -581,13 +555,13 @@ class SubmissionFormWidget(QWidget):
|
|||||||
parent.extraction_kit = add_widget.currentText()
|
parent.extraction_kit = add_widget.currentText()
|
||||||
case 'submission_category':
|
case 'submission_category':
|
||||||
add_widget = MyQComboBox(scrollWidget=parent)
|
add_widget = MyQComboBox(scrollWidget=parent)
|
||||||
cats = ['Diagnostic', "Surveillance", "Research"]
|
categories = ['Diagnostic', "Surveillance", "Research"]
|
||||||
cats += [item.name for item in SubmissionType.query()]
|
categories += [item.name for item in SubmissionType.query()]
|
||||||
try:
|
try:
|
||||||
cats.insert(0, cats.pop(cats.index(value)))
|
categories.insert(0, categories.pop(categories.index(value)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
cats.insert(0, cats.pop(cats.index(submission_type)))
|
categories.insert(0, categories.pop(categories.index(submission_type)))
|
||||||
add_widget.addItems(cats)
|
add_widget.addItems(categories)
|
||||||
add_widget.setToolTip("Enter submission category or select from list.")
|
add_widget.setToolTip("Enter submission category or select from list.")
|
||||||
case _:
|
case _:
|
||||||
if key in sub_obj.timestamps:
|
if key in sub_obj.timestamps:
|
||||||
@@ -655,7 +629,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent: QWidget, reagent: PydReagent, extraction_kit: str):
|
def __init__(self, parent: QWidget, reagent: PydReagent, extraction_kit: str):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.app = self.parent().parent().parent().parent().parent().parent().parent().parent()
|
self.parent = parent
|
||||||
|
self.app = get_application_from_parent(parent)
|
||||||
self.reagent = reagent
|
self.reagent = reagent
|
||||||
self.extraction_kit = extraction_kit
|
self.extraction_kit = extraction_kit
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
@@ -684,10 +659,11 @@ class SubmissionFormWidget(QWidget):
|
|||||||
def disable(self):
|
def disable(self):
|
||||||
self.lot.setEnabled(self.check.isChecked())
|
self.lot.setEnabled(self.check.isChecked())
|
||||||
self.label.setEnabled(self.check.isChecked())
|
self.label.setEnabled(self.check.isChecked())
|
||||||
if not any([item.lot.isEnabled() for item in self.parent().findChildren(self.__class__)]):
|
with QSignalBlocker(self.parent.disabler.checkbox) as blocker:
|
||||||
self.parent().disabler.checkbox.setChecked(False)
|
if any([item.lot.isEnabled() for item in self.parent.findChildren(self.__class__)]):
|
||||||
|
self.parent.disabler.checkbox.setChecked(True)
|
||||||
else:
|
else:
|
||||||
self.parent().disabler.checkbox.setChecked(True)
|
self.parent.disabler.checkbox.setChecked(False)
|
||||||
|
|
||||||
@report_result
|
@report_result
|
||||||
def parse_form(self) -> Tuple[PydReagent | None, Report]:
|
def parse_form(self) -> Tuple[PydReagent | None, Report]:
|
||||||
@@ -703,31 +679,23 @@ class SubmissionFormWidget(QWidget):
|
|||||||
lot = self.lot.currentText()
|
lot = self.lot.currentText()
|
||||||
wanted_reagent, new = Reagent.query_or_create(lot=lot, role=self.reagent.role, expiry=self.reagent.expiry)
|
wanted_reagent, new = Reagent.query_or_create(lot=lot, role=self.reagent.role, expiry=self.reagent.expiry)
|
||||||
# NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent)
|
# NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent)
|
||||||
logger.debug(f"Wanted reagent: {wanted_reagent}, New: {new}")
|
|
||||||
# if wanted_reagent is None:
|
|
||||||
if new:
|
if new:
|
||||||
dlg = QuestionAsker(title=f"Add {lot}?",
|
dlg = QuestionAsker(title=f"Add {lot}?",
|
||||||
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
|
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
|
||||||
|
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
wanted_reagent = self.parent().parent().new_add_reagent(instance=wanted_reagent)
|
wanted_reagent = self.parent().parent().new_add_reagent(instance=wanted_reagent)
|
||||||
logger.debug(f"Reagent added!")
|
|
||||||
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information"))
|
|
||||||
return wanted_reagent, report
|
return wanted_reagent, report
|
||||||
else:
|
else:
|
||||||
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||||
|
|
||||||
return None, report
|
return None, report
|
||||||
else:
|
else:
|
||||||
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
||||||
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
|
||||||
rt = ReagentRole.query(name=self.reagent.role)
|
rt = ReagentRole.query(name=self.reagent.role)
|
||||||
logger.debug(f"Reagent role: {rt}")
|
|
||||||
if rt is None:
|
if rt is None:
|
||||||
rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||||
final = PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name,
|
final = PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name,
|
||||||
expiry=wanted_reagent.expiry.date(), missing=False)
|
expiry=wanted_reagent.expiry.date(), missing=False)
|
||||||
logger.debug(f"Final Reagent: {final}")
|
|
||||||
return final, report
|
return final, report
|
||||||
|
|
||||||
def updated(self):
|
def updated(self):
|
||||||
@@ -781,10 +749,11 @@ class SubmissionFormWidget(QWidget):
|
|||||||
looked_up_reg = None
|
looked_up_reg = None
|
||||||
if looked_up_reg:
|
if looked_up_reg:
|
||||||
try:
|
try:
|
||||||
relevant_reagents.remove(str(looked_up_reg.lot))
|
# relevant_reagents.remove(str(looked_up_reg.lot))
|
||||||
|
relevant_reagents.insert(0, relevant_reagents.pop(relevant_reagents.index(looked_up_reg.lot)))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Error reordering relevant reagents: {e}")
|
logger.error(f"Error reordering relevant reagents: {e}")
|
||||||
relevant_reagents.insert(0, str(looked_up_reg.lot))
|
# relevant_reagents.insert(0, str(looked_up_reg.lot))
|
||||||
else:
|
else:
|
||||||
if len(relevant_reagents) > 1:
|
if len(relevant_reagents) > 1:
|
||||||
idx = relevant_reagents.index(str(reagent.lot))
|
idx = relevant_reagents.index(str(reagent.lot))
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ class Summary(InfoPane):
|
|||||||
self.update_data()
|
self.update_data()
|
||||||
|
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self) -> None:
|
||||||
|
"""
|
||||||
|
Sets data in the info pane
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
super().update_data()
|
super().update_data()
|
||||||
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
|
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
|
||||||
self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
|
self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
|
||||||
|
|||||||
@@ -31,7 +31,13 @@ class TurnaroundTime(InfoPane):
|
|||||||
self.submission_typer.currentTextChanged.connect(self.update_data)
|
self.submission_typer.currentTextChanged.connect(self.update_data)
|
||||||
self.update_data()
|
self.update_data()
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self) -> None:
|
||||||
|
"""
|
||||||
|
Sets data in the info pane
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
super().update_data()
|
super().update_data()
|
||||||
months = self.diff_month(self.start_date, self.end_date)
|
months = self.diff_month(self.start_date, self.end_date)
|
||||||
chart_settings = dict(start_date=self.start_date, end_date=self.end_date)
|
chart_settings = dict(start_date=self.start_date, end_date=self.end_date)
|
||||||
@@ -47,4 +53,4 @@ class TurnaroundTime(InfoPane):
|
|||||||
else:
|
else:
|
||||||
threshold = None
|
threshold = None
|
||||||
self.fig = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold, months=months)
|
self.fig = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold, months=months)
|
||||||
self.webview.setHtml(self.fig.to_html())
|
self.webview.setHtml(self.fig.html)
|
||||||
|
|||||||
@@ -10,11 +10,12 @@
|
|||||||
<h2><u>Reagent Details for {{ reagent['name'] }} - {{ reagent['lot'] }}</u></h2>
|
<h2><u>Reagent Details for {{ reagent['name'] }} - {{ reagent['lot'] }}</u></h2>
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<p>{% for key, value in reagent.items() if key not in reagent['excluded'] %}
|
<p>{% for key, value in reagent.items() if key not in reagent['excluded'] %}
|
||||||
<b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>
|
<!-- <b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
|
||||||
|
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% if permission %}
|
<!-- {% if permission %}-->
|
||||||
<button type="button" id="save_btn">Save</button>
|
<!-- <button type="button" id="save_btn">Save</button>-->
|
||||||
{% endif %}
|
<!-- {% endif %}-->
|
||||||
{% if reagent['submissions'] %}<h2>Submissions:</h2>
|
{% if reagent['submissions'] %}<h2>Submissions:</h2>
|
||||||
{% for submission in reagent['submissions'] %}
|
{% for submission in reagent['submissions'] %}
|
||||||
<p><b><a class="data-link" id="{{ submission }}">{{ submission }}:</a></b> {{ reagent['role'] }}</p>
|
<p><b><a class="data-link" id="{{ submission }}">{{ submission }}:</a></b> {{ reagent['role'] }}</p>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from json import JSONDecodeError
|
|||||||
import logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd
|
import logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from inspect import getmembers, isfunction, stack
|
from inspect import getmembers, isfunction, stack
|
||||||
|
from types import GeneratorType
|
||||||
|
|
||||||
from dateutil.easter import easter
|
from dateutil.easter import easter
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
@@ -18,7 +20,7 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import create_engine, text, MetaData
|
from sqlalchemy import create_engine, text, MetaData
|
||||||
from pydantic import field_validator, BaseModel, Field
|
from pydantic import field_validator, BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing import Any, Tuple, Literal, List
|
from typing import Any, Tuple, Literal, List, Generator
|
||||||
from __init__ import project_path
|
from __init__ import project_path
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from tkinter import Tk # NOTE: This is for choosing database path before app is created.
|
from tkinter import Tk # NOTE: This is for choosing database path before app is created.
|
||||||
@@ -38,7 +40,7 @@ if platform.system() == "Windows":
|
|||||||
logger.info(f"Got platform Windows, config_dir: {os_config_dir}")
|
logger.info(f"Got platform Windows, config_dir: {os_config_dir}")
|
||||||
else:
|
else:
|
||||||
os_config_dir = ".config"
|
os_config_dir = ".config"
|
||||||
logger.info(f"Got platform other, config_dir: {os_config_dir}")
|
logger.info(f"Got platform {platform.system()}, config_dir: {os_config_dir}")
|
||||||
|
|
||||||
main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions")
|
main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions")
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ main_form_style = '''
|
|||||||
page_size = 250
|
page_size = 250
|
||||||
|
|
||||||
|
|
||||||
def divide_chunks(input_list: list, chunk_count: int):
|
def divide_chunks(input_list: list, chunk_count: int) -> Generator[Any, Any, None]:
|
||||||
"""
|
"""
|
||||||
Divides a list into {chunk_count} equal parts
|
Divides a list into {chunk_count} equal parts
|
||||||
|
|
||||||
@@ -179,7 +181,7 @@ def check_not_nan(cell_contents) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def convert_nans_to_nones(input_str) -> str | None:
|
def convert_nans_to_nones(input_str:str) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get rid of various "nan", "NAN", "NaN", etc/
|
Get rid of various "nan", "NAN", "NaN", etc/
|
||||||
|
|
||||||
@@ -289,12 +291,10 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def set_schema(cls, value):
|
def set_schema(cls, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
# print("No value for dir path")
|
|
||||||
if check_if_app():
|
if check_if_app():
|
||||||
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
else:
|
else:
|
||||||
alembic_path = project_path.joinpath("alembic.ini")
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
# print(f"Getting alembic path: {alembic_path}")
|
|
||||||
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='schema')
|
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='schema')
|
||||||
if value is None:
|
if value is None:
|
||||||
value = "sqlite"
|
value = "sqlite"
|
||||||
@@ -321,14 +321,11 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
if value is None:
|
if value is None:
|
||||||
match values.data['database_schema']:
|
match values.data['database_schema']:
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
# print("No value for dir path")
|
|
||||||
if check_if_app():
|
if check_if_app():
|
||||||
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
else:
|
else:
|
||||||
alembic_path = project_path.joinpath("alembic.ini")
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
# print(f"Getting alembic path: {alembic_path}")
|
|
||||||
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='path').parent
|
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='path').parent
|
||||||
# print(f"Using {value}")
|
|
||||||
case _:
|
case _:
|
||||||
Tk().withdraw() # we don't want a full GUI, so keep the root window from appearing
|
Tk().withdraw() # we don't want a full GUI, so keep the root window from appearing
|
||||||
value = Path(askdirectory(
|
value = Path(askdirectory(
|
||||||
@@ -340,9 +337,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
check = False
|
check = False
|
||||||
if not check:
|
if not check:
|
||||||
# print(f"No directory found, using Documents/submissions")
|
|
||||||
value.mkdir(exist_ok=True)
|
value.mkdir(exist_ok=True)
|
||||||
# print(f"Final return of directory_path: {value}")
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator('database_path', mode="before")
|
@field_validator('database_path', mode="before")
|
||||||
@@ -360,7 +355,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
else:
|
else:
|
||||||
alembic_path = project_path.joinpath("alembic.ini")
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
# print(f"Getting alembic path: {alembic_path}")
|
|
||||||
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='path').parent
|
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='path').parent
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -372,7 +366,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
else:
|
else:
|
||||||
alembic_path = project_path.joinpath("alembic.ini")
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
# print(f"Getting alembic path: {alembic_path}")
|
|
||||||
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='path').stem
|
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='path').stem
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -384,9 +377,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
else:
|
else:
|
||||||
alembic_path = project_path.joinpath("alembic.ini")
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
# print(f"Getting alembic path: {alembic_path}")
|
|
||||||
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='user')
|
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='user')
|
||||||
# print(f"Got {value} for user")
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator("database_password", mode='before')
|
@field_validator("database_password", mode='before')
|
||||||
@@ -397,9 +388,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
else:
|
else:
|
||||||
alembic_path = project_path.joinpath("alembic.ini")
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
# print(f"Getting alembic path: {alembic_path}")
|
|
||||||
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='pass')
|
value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='pass')
|
||||||
# print(f"Got {value} for pass")
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator('database_session', mode="before")
|
@field_validator('database_session', mode="before")
|
||||||
@@ -421,7 +410,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Trusted_Connection=yes"
|
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Trusted_Connection=yes"
|
||||||
)
|
)
|
||||||
case _:
|
case _:
|
||||||
# print(pprint.pprint(values.data))
|
|
||||||
tmp = jinja_template_loading().from_string(
|
tmp = jinja_template_loading().from_string(
|
||||||
"{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}")
|
"{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}")
|
||||||
value = tmp.render(values=values.data)
|
value = tmp.render(values=values.data)
|
||||||
@@ -444,7 +432,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.set_from_db()
|
self.set_from_db()
|
||||||
self.set_scripts()
|
self.set_scripts()
|
||||||
# pprint(f"User settings:\n{self.__dict__}")
|
|
||||||
|
|
||||||
def set_from_db(self):
|
def set_from_db(self):
|
||||||
if 'pytest' in sys.modules:
|
if 'pytest' in sys.modules:
|
||||||
@@ -453,11 +440,8 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
teardown_scripts=dict(goodbye=None)
|
teardown_scripts=dict(goodbye=None)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# print(f"Hello from database settings getter.")
|
|
||||||
# print(self.__dict__)
|
|
||||||
session = self.database_session
|
session = self.database_session
|
||||||
metadata = MetaData()
|
metadata = MetaData()
|
||||||
# print(self.database_session.get_bind())
|
|
||||||
try:
|
try:
|
||||||
metadata.reflect(bind=session.get_bind())
|
metadata.reflect(bind=session.get_bind())
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
@@ -467,7 +451,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
print(f"Couldn't find _configitems in {metadata.tables.keys()}.")
|
print(f"Couldn't find _configitems in {metadata.tables.keys()}.")
|
||||||
return
|
return
|
||||||
config_items = session.execute(text("SELECT * FROM _configitem")).all()
|
config_items = session.execute(text("SELECT * FROM _configitem")).all()
|
||||||
# print(f"Config: {pprint.pprint(config_items)}")
|
|
||||||
output = {}
|
output = {}
|
||||||
for item in config_items:
|
for item in config_items:
|
||||||
try:
|
try:
|
||||||
@@ -488,6 +471,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
p = Path(__file__).parents[2].joinpath("scripts").absolute()
|
p = Path(__file__).parents[2].joinpath("scripts").absolute()
|
||||||
if p.__str__() not in sys.path:
|
if p.__str__() not in sys.path:
|
||||||
sys.path.append(p.__str__())
|
sys.path.append(p.__str__())
|
||||||
|
# NOTE: Get all .py files that don't have __ in them.
|
||||||
modules = p.glob("[!__]*.py")
|
modules = p.glob("[!__]*.py")
|
||||||
for module in modules:
|
for module in modules:
|
||||||
mod = importlib.import_module(module.stem)
|
mod = importlib.import_module(module.stem)
|
||||||
@@ -495,6 +479,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
name = function[0]
|
name = function[0]
|
||||||
func = function[1]
|
func = function[1]
|
||||||
# NOTE: assign function based on its name being in config: startup/teardown
|
# NOTE: assign function based on its name being in config: startup/teardown
|
||||||
|
# NOTE: scripts must be registered using {name: Null} in the database
|
||||||
if name in self.startup_scripts.keys():
|
if name in self.startup_scripts.keys():
|
||||||
self.startup_scripts[name] = func
|
self.startup_scripts[name] = func
|
||||||
if name in self.teardown_scripts.keys():
|
if name in self.teardown_scripts.keys():
|
||||||
@@ -543,14 +528,12 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
try:
|
try:
|
||||||
return url[:url.index("@")].split(":")[0]
|
return url[:url.index("@")].split(":")[0]
|
||||||
except (IndexError, ValueError) as e:
|
except (IndexError, ValueError) as e:
|
||||||
# print(f"Error on user: {e}")
|
|
||||||
return None
|
return None
|
||||||
case "pass":
|
case "pass":
|
||||||
url = re.sub(r"^.*//", "", url)
|
url = re.sub(r"^.*//", "", url)
|
||||||
try:
|
try:
|
||||||
return url[:url.index("@")].split(":")[1]
|
return url[:url.index("@")].split(":")[1]
|
||||||
except (IndexError, ValueError) as e:
|
except (IndexError, ValueError) as e:
|
||||||
# print(f"Error on user: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save(self, settings_path: Path):
|
def save(self, settings_path: Path):
|
||||||
@@ -592,7 +575,6 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
|
|||||||
def join(loader, node):
|
def join(loader, node):
|
||||||
seq = loader.construct_sequence(node)
|
seq = loader.construct_sequence(node)
|
||||||
return ''.join([str(i) for i in seq])
|
return ''.join([str(i) for i in seq])
|
||||||
|
|
||||||
# NOTE: register the tag handler
|
# NOTE: register the tag handler
|
||||||
yaml.add_constructor('!join', join)
|
yaml.add_constructor('!join', join)
|
||||||
# NOTE: make directories
|
# NOTE: make directories
|
||||||
@@ -624,7 +606,6 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
|
|||||||
# NOTE: copy settings to config directory
|
# NOTE: copy settings to config directory
|
||||||
settings = Settings(**default_settings)
|
settings = Settings(**default_settings)
|
||||||
settings.save(settings_path=CONFIGDIR.joinpath("config.yml"))
|
settings.save(settings_path=CONFIGDIR.joinpath("config.yml"))
|
||||||
# print(f"Default settings: {pprint.pprint(settings.__dict__)}")
|
|
||||||
return settings
|
return settings
|
||||||
else:
|
else:
|
||||||
# NOTE: check if user defined path is directory
|
# NOTE: check if user defined path is directory
|
||||||
@@ -829,10 +810,23 @@ def setup_lookup(func):
|
|||||||
elif v is not None:
|
elif v is not None:
|
||||||
sanitized_kwargs[k] = v
|
sanitized_kwargs[k] = v
|
||||||
return func(*args, **sanitized_kwargs)
|
return func(*args, **sanitized_kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def get_application_from_parent(widget):
|
||||||
|
try:
|
||||||
|
return widget.app
|
||||||
|
except AttributeError:
|
||||||
|
logger.info("Using recursion to get application object.")
|
||||||
|
from frontend.widgets.app import App
|
||||||
|
while not isinstance(widget, App):
|
||||||
|
try:
|
||||||
|
widget = widget.parent()
|
||||||
|
except AttributeError:
|
||||||
|
return widget
|
||||||
|
return widget
|
||||||
|
|
||||||
|
|
||||||
class Result(BaseModel, arbitrary_types_allowed=True):
|
class Result(BaseModel, arbitrary_types_allowed=True):
|
||||||
owner: str = Field(default="", validate_default=True)
|
owner: str = Field(default="", validate_default=True)
|
||||||
code: int = Field(default=0)
|
code: int = Field(default=0)
|
||||||
@@ -937,20 +931,20 @@ def rreplace(s: str, old: str, new: str) -> str:
|
|||||||
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
||||||
|
|
||||||
|
|
||||||
def remove_key_from_list_of_dicts(input: list, key: str) -> list:
|
def remove_key_from_list_of_dicts(input_list: list, key: str) -> list:
|
||||||
"""
|
"""
|
||||||
Removes a key from all dictionaries in a list of dictionaries
|
Removes a key from all dictionaries in a list of dictionaries
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input (list): Input list of dicts
|
input_list (list): Input list of dicts
|
||||||
key (str): Name of key to remove.
|
key (str): Name of key to remove.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: List of updated dictionaries
|
list: List of updated dictionaries
|
||||||
"""
|
"""
|
||||||
for item in input:
|
for item in input_list:
|
||||||
del item[key]
|
del item[key]
|
||||||
return input
|
return input_list
|
||||||
|
|
||||||
|
|
||||||
def yaml_regex_creator(loader, node):
|
def yaml_regex_creator(loader, node):
|
||||||
@@ -963,6 +957,7 @@ def yaml_regex_creator(loader, node):
|
|||||||
|
|
||||||
def super_splitter(ins_str: str, substring: str, idx: int) -> str:
|
def super_splitter(ins_str: str, substring: str, idx: int) -> str:
|
||||||
"""
|
"""
|
||||||
|
Splits string on substring at index
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ins_str (str): input string
|
ins_str (str): input string
|
||||||
@@ -978,6 +973,20 @@ def super_splitter(ins_str: str, substring: str, idx: int) -> str:
|
|||||||
return ins_str
|
return ins_str
|
||||||
|
|
||||||
|
|
||||||
|
def is_developer() -> bool:
|
||||||
|
"""
|
||||||
|
Checks if user is in list of super users
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if yes, False if no.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
check = getpass.getuser() in ctx.super_users
|
||||||
|
except:
|
||||||
|
check = False
|
||||||
|
return check
|
||||||
|
|
||||||
|
|
||||||
def is_power_user() -> bool:
|
def is_power_user() -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if user is in list of power users
|
Checks if user is in list of power users
|
||||||
@@ -1000,21 +1009,49 @@ def check_authorization(func):
|
|||||||
func (function): Function to be used.
|
func (function): Function to be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
@report_result
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
logger.info(f"Checking authorization")
|
logger.info(f"Checking authorization")
|
||||||
if is_power_user():
|
error_msg = f"User {getpass.getuser()} is not authorized for this function."
|
||||||
|
auth_func = is_power_user
|
||||||
|
if auth_func():
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
logger.error(f"User {getpass.getuser()} is not authorized for this function.")
|
logger.error(error_msg)
|
||||||
report = Report()
|
report = Report()
|
||||||
report.add_result(
|
report.add_result(
|
||||||
Result(owner=func.__str__(), code=1, msg="This user does not have permission for this function.",
|
Result(owner=func.__str__(), code=1, msg=error_msg, status="warning"))
|
||||||
status="warning"))
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def under_development(func):
|
||||||
|
"""
|
||||||
|
Decorator to check if user is authorized to access function
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (function): Function to be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
@report_result
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
logger.warning(f"This feature is under development")
|
||||||
|
if is_developer():
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
error_msg = f"User {getpass.getuser()} is not authorized for this function."
|
||||||
|
logger.error(error_msg)
|
||||||
|
report = Report()
|
||||||
|
report.add_result(
|
||||||
|
Result(owner=func.__str__(), code=1, msg=error_msg,
|
||||||
|
status="warning"))
|
||||||
|
return report
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def report_result(func):
|
def report_result(func):
|
||||||
"""
|
"""
|
||||||
Decorator to display any reports returned from a function.
|
Decorator to display any reports returned from a function.
|
||||||
@@ -1036,14 +1073,9 @@ def report_result(func):
|
|||||||
case Report():
|
case Report():
|
||||||
report = output
|
report = output
|
||||||
case tuple():
|
case tuple():
|
||||||
# try:
|
|
||||||
report = next((item for item in output if isinstance(item, Report)), None)
|
report = next((item for item in output if isinstance(item, Report)), None)
|
||||||
# except IndexError:
|
|
||||||
# report = None
|
|
||||||
case _:
|
case _:
|
||||||
report = Report()
|
report = Report()
|
||||||
# return report
|
|
||||||
# logger.info(f"Got report: {report}")
|
|
||||||
try:
|
try:
|
||||||
results = report.results
|
results = report.results
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -1058,13 +1090,11 @@ def report_result(func):
|
|||||||
logger.error(result.msg)
|
logger.error(result.msg)
|
||||||
if output:
|
if output:
|
||||||
true_output = tuple(item for item in output if not isinstance(item, Report))
|
true_output = tuple(item for item in output if not isinstance(item, Report))
|
||||||
# logger.debug(f"True output: {true_output}")
|
|
||||||
if len(true_output) == 1:
|
if len(true_output) == 1:
|
||||||
true_output = true_output[0]
|
true_output = true_output[0]
|
||||||
else:
|
else:
|
||||||
true_output = None
|
true_output = None
|
||||||
return true_output
|
return true_output
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@@ -1084,20 +1114,19 @@ def create_holidays_for_year(year: int | None = None) -> List[date]:
|
|||||||
offset = -d.weekday() # weekday == 0 means Monday
|
offset = -d.weekday() # weekday == 0 means Monday
|
||||||
output = d + timedelta(offset)
|
output = d + timedelta(offset)
|
||||||
return output.date()
|
return output.date()
|
||||||
|
|
||||||
if not year:
|
if not year:
|
||||||
year = date.today().year
|
year = date.today().year
|
||||||
# Includes New Year's day for next year.
|
# NOTE: Includes New Year's day for next year.
|
||||||
holidays = [date(year, 1, 1), date(year, 7, 1), date(year, 9, 30),
|
holidays = [date(year, 1, 1), date(year, 7, 1), date(year, 9, 30),
|
||||||
date(year, 11, 11), date(year, 12, 25), date(year, 12, 26),
|
date(year, 11, 11), date(year, 12, 25), date(year, 12, 26),
|
||||||
date(year + 1, 1, 1)]
|
date(year + 1, 1, 1)]
|
||||||
# Labour Day
|
# NOTE: Labour Day
|
||||||
holidays.append(find_nth_monday(year, 9))
|
holidays.append(find_nth_monday(year, 9))
|
||||||
# Thanksgiving
|
# NOTE: Thanksgiving
|
||||||
holidays.append(find_nth_monday(year, 10, occurence=2))
|
holidays.append(find_nth_monday(year, 10, occurence=2))
|
||||||
# Victoria Day
|
# NOTE: Victoria Day
|
||||||
holidays.append(find_nth_monday(year, 5, day=25))
|
holidays.append(find_nth_monday(year, 5, day=25))
|
||||||
# Easter, etc
|
# NOTE: Easter, etc
|
||||||
holidays.append(easter(year) - timedelta(days=2))
|
holidays.append(easter(year) - timedelta(days=2))
|
||||||
holidays.append(easter(year) + timedelta(days=1))
|
holidays.append(easter(year) + timedelta(days=1))
|
||||||
return sorted(holidays)
|
return sorted(holidays)
|
||||||
@@ -1107,8 +1136,7 @@ class classproperty(property):
|
|||||||
def __get__(self, owner_self, owner_cls):
|
def __get__(self, owner_self, owner_cls):
|
||||||
return self.fget(owner_cls)
|
return self.fget(owner_cls)
|
||||||
|
|
||||||
|
# NOTE: Monkey patching... hooray!
|
||||||
builtins.classproperty = classproperty
|
builtins.classproperty = classproperty
|
||||||
|
|
||||||
|
|
||||||
ctx = get_config(None)
|
ctx = get_config(None)
|
||||||
|
|||||||
Reference in New Issue
Block a user