Created omni-manager, omni-addit
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
# 202412.06
|
||||||
|
|
||||||
|
- Switched startup/teardown scripts to importlib/getattr addition to ctx.
|
||||||
|
|
||||||
# 202412.05
|
# 202412.05
|
||||||
|
|
||||||
- Switched startup/teardown scripts to decorator registration.
|
- Switched startup/teardown scripts to decorator registration.
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import logging, shutil, pyodbc
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools import Settings
|
from tools import Settings
|
||||||
# from .. import register_script
|
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
# @register_script
|
|
||||||
def backup_database(ctx: Settings):
|
def backup_database(ctx: Settings):
|
||||||
"""
|
"""
|
||||||
Copies the database into the backup directory the first time it is opened every month.
|
Copies the database into the backup directory the first time it is opened every month.
|
||||||
22
src/scripts/goodbye.py
Normal file
22
src/scripts/goodbye.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Test script for teardown_scripts
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def goodbye(ctx):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
ctx (Settings): All scripts must take ctx as an argument to maintain interoperability.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: Scripts are currently unable to return results to the program.
|
||||||
|
"""
|
||||||
|
print("\n\nGoodbye. Thank you for using Robotics Submission Tracker.\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
For scripts to be run, they must be added to the _configitem.startup_scripts or _configitem.teardown_scripts
|
||||||
|
rows as a key: value (name: null) entry in the JSON.
|
||||||
|
ex: {"goodbye": null, "backup_database": null}
|
||||||
|
The program will overwrite null with the actual function upon startup.
|
||||||
|
"""
|
||||||
22
src/scripts/hello.py
Normal file
22
src/scripts/hello.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Test script for startup_scripts
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def hello(ctx) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
ctx (Settings): All scripts must take ctx as an argument to maintain interoperability.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: Scripts are currently unable to return results to the program.
|
||||||
|
"""
|
||||||
|
print("\n\nHello! Welcome to Robotics Submission Tracker.\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
For scripts to be run, they must be added to the _configitem.startup_scripts or _configitem.teardown_scripts
|
||||||
|
rows as a key: value (name: null) entry in the JSON.
|
||||||
|
ex: {"hello": null, "import_irida": null}
|
||||||
|
The program will overwrite null with the actual function upon startup.
|
||||||
|
"""
|
||||||
@@ -4,11 +4,9 @@ from datetime import datetime
|
|||||||
from tools import Settings
|
from tools import Settings
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
# from .. import register_script
|
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
# @register_script
|
|
||||||
def import_irida(ctx: Settings):
|
def import_irida(ctx: Settings):
|
||||||
"""
|
"""
|
||||||
Grabs Irida controls from secondary database.
|
Grabs Irida controls from secondary database.
|
||||||
@@ -38,7 +36,6 @@ def import_irida(ctx: Settings):
|
|||||||
subtype=row[6], refseq_version=row[7], kraken2_version=row[8], kraken2_db_version=row[9],
|
subtype=row[6], refseq_version=row[7], kraken2_version=row[8], kraken2_db_version=row[9],
|
||||||
sample_id=row[10]) for row in cursor]
|
sample_id=row[10]) for row in cursor]
|
||||||
for record in records:
|
for record in records:
|
||||||
# instance = IridaControl.query(name=record['name'])
|
|
||||||
instance = new_session.query(IridaControl).filter(IridaControl.name == record['name']).first()
|
instance = new_session.query(IridaControl).filter(IridaControl.name == record['name']).first()
|
||||||
if instance:
|
if instance:
|
||||||
logger.warning(f"Irida Control {instance.name} already exists, skipping.")
|
logger.warning(f"Irida Control {instance.name} already exists, skipping.")
|
||||||
@@ -49,19 +46,13 @@ def import_irida(ctx: Settings):
|
|||||||
assert isinstance(record[thing], dict)
|
assert isinstance(record[thing], dict)
|
||||||
else:
|
else:
|
||||||
record[thing] = {}
|
record[thing] = {}
|
||||||
# record['matches'] = json.loads(record['matches'])
|
|
||||||
# assert isinstance(record['matches'], dict)
|
|
||||||
# record['kraken'] = json.loads(record['kraken'])
|
|
||||||
# assert isinstance(record['kraken'], dict)
|
|
||||||
record['submitted_date'] = datetime.strptime(record['submitted_date'], "%Y-%m-%d %H:%M:%S.%f")
|
record['submitted_date'] = datetime.strptime(record['submitted_date'], "%Y-%m-%d %H:%M:%S.%f")
|
||||||
assert isinstance(record['submitted_date'], datetime)
|
assert isinstance(record['submitted_date'], datetime)
|
||||||
instance = IridaControl(controltype=ct, **record)
|
instance = IridaControl(controltype=ct, **record)
|
||||||
# sample = BasicSample.query(submitter_id=instance.name)
|
|
||||||
sample = new_session.query(BasicSample).filter(BasicSample.submitter_id == instance.name).first()
|
sample = new_session.query(BasicSample).filter(BasicSample.submitter_id == instance.name).first()
|
||||||
if sample:
|
if sample:
|
||||||
instance.sample = sample
|
instance.sample = sample
|
||||||
instance.submission = sample.submissions[0]
|
instance.submission = sample.submissions[0]
|
||||||
# instance.save()
|
|
||||||
new_session.add(instance)
|
new_session.add(instance)
|
||||||
new_session.commit()
|
new_session.commit()
|
||||||
new_session.close()
|
new_session.close()
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import sys, os
|
import sys, os
|
||||||
from tools import ctx, setup_logger, check_if_app
|
from tools import ctx, setup_logger, check_if_app
|
||||||
|
|
||||||
# environment variable must be set to enable qtwebengine in network path
|
# NOTE: environment variable must be set to enable qtwebengine in network path
|
||||||
if check_if_app():
|
if check_if_app():
|
||||||
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
|
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
|
||||||
|
|
||||||
# setup custom logger
|
# NOTE: setup custom logger
|
||||||
logger = setup_logger(verbosity=3)
|
logger = setup_logger(verbosity=3)
|
||||||
|
|
||||||
# from backend import scripts
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from frontend.widgets.app import App
|
from frontend.widgets.app import App
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Any, List
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools import report_result
|
from tools import report_result
|
||||||
|
|
||||||
|
|
||||||
# NOTE: Load testing environment
|
# NOTE: Load testing environment
|
||||||
if 'pytest' in sys.modules:
|
if 'pytest' in sys.modules:
|
||||||
sys.path.append(Path(__file__).parents[4].absolute().joinpath("tests").__str__())
|
sys.path.append(Path(__file__).parents[4].absolute().joinpath("tests").__str__())
|
||||||
@@ -167,7 +168,10 @@ class BaseClass(Base):
|
|||||||
Returns:
|
Returns:
|
||||||
Dataframe
|
Dataframe
|
||||||
"""
|
"""
|
||||||
records = [obj.to_sub_dict(**kwargs) for obj in objects]
|
try:
|
||||||
|
records = [obj.to_sub_dict(**kwargs) for obj in objects]
|
||||||
|
except AttributeError:
|
||||||
|
records = [obj.to_dict() for obj in objects]
|
||||||
return DataFrame.from_records(records)
|
return DataFrame.from_records(records)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -233,6 +237,15 @@ class BaseClass(Base):
|
|||||||
report.add_result(Result(msg=e, status="Critical"))
|
report.add_result(Result(msg=e, status="Critical"))
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state", "id"]}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_pydantic_model(cls):
|
||||||
|
from backend.validators import pydant
|
||||||
|
model = getattr(pydant, f"Pyd{cls.__name__}")
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
class ConfigItem(BaseClass):
|
class ConfigItem(BaseClass):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ class Contact(BaseClass):
|
|||||||
Base of Contact
|
Base of Contact
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
searchables =[]
|
||||||
|
|
||||||
id = Column(INTEGER, primary_key=True) #: primary key
|
id = Column(INTEGER, primary_key=True) #: primary key
|
||||||
name = Column(String(64)) #: contact name
|
name = Column(String(64)) #: contact name
|
||||||
email = Column(String(64)) #: contact email
|
email = Column(String(64)) #: contact email
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from collections import OrderedDict
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
import logging, uuid, tempfile, re, base64, numpy as np, pandas as pd, types, sys
|
import logging, uuid, tempfile, re, base64, numpy as np, pandas as pd, types, sys
|
||||||
|
from inspect import isclass
|
||||||
from zipfile import ZipFile, BadZipfile
|
from zipfile import ZipFile, BadZipfile
|
||||||
from tempfile import TemporaryDirectory, TemporaryFile
|
from tempfile import TemporaryDirectory, TemporaryFile
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
@@ -175,7 +176,7 @@ class BasicSubmission(BaseClass, LogMixin):
|
|||||||
# NOTE: Fields not placed in ui form
|
# NOTE: Fields not placed in ui form
|
||||||
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
|
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
|
||||||
'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre', 'completed_date',
|
'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre', 'completed_date',
|
||||||
'controls'] + recover,
|
'controls', "origin_plate"] + recover,
|
||||||
# NOTE: Fields not placed in ui form to be moved to pydantic
|
# NOTE: Fields not placed in ui form to be moved to pydantic
|
||||||
form_recover=recover
|
form_recover=recover
|
||||||
))
|
))
|
||||||
@@ -352,7 +353,10 @@ class BasicSubmission(BaseClass, LogMixin):
|
|||||||
try:
|
try:
|
||||||
contact = self.contact.name
|
contact = self.contact.name
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
contact = "NA"
|
try:
|
||||||
|
contact = f"Defaulted to: {self.submitting_lab.contacts[0].name}"
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
contact = "NA"
|
||||||
try:
|
try:
|
||||||
contact_phone = self.contact.phone
|
contact_phone = self.contact.phone
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -627,8 +631,14 @@ class BasicSubmission(BaseClass, LogMixin):
|
|||||||
continue
|
continue
|
||||||
case "tips":
|
case "tips":
|
||||||
field_value = [item.to_pydantic() for item in self.submission_tips_associations]
|
field_value = [item.to_pydantic() for item in self.submission_tips_associations]
|
||||||
case "submission_type" | "contact":
|
case "submission_type":
|
||||||
field_value = dict(value=self.__getattribute__(key).name, missing=missing)
|
field_value = dict(value=self.__getattribute__(key).name, missing=missing)
|
||||||
|
# case "contact":
|
||||||
|
# try:
|
||||||
|
# field_value = dict(value=self.__getattribute__(key).name, missing=missing)
|
||||||
|
# except AttributeError:
|
||||||
|
# contact = self.submitting_lab.contacts[0]
|
||||||
|
# field_value = dict(value=contact.name, missing=True)
|
||||||
case "plate_number":
|
case "plate_number":
|
||||||
key = 'rsl_plate_num'
|
key = 'rsl_plate_num'
|
||||||
field_value = dict(value=self.rsl_plate_num, missing=missing)
|
field_value = dict(value=self.rsl_plate_num, missing=missing)
|
||||||
@@ -640,10 +650,13 @@ class BasicSubmission(BaseClass, LogMixin):
|
|||||||
case _:
|
case _:
|
||||||
try:
|
try:
|
||||||
key = key.lower().replace(" ", "_")
|
key = key.lower().replace(" ", "_")
|
||||||
field_value = dict(value=self.__getattribute__(key), missing=missing)
|
if isclass(value):
|
||||||
|
field_value = dict(value=self.__getattribute__(key).name, missing=missing)
|
||||||
|
else:
|
||||||
|
field_value = dict(value=self.__getattribute__(key), missing=missing)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.error(f"{key} is not available in {self}")
|
logger.error(f"{key} is not available in {self}")
|
||||||
continue
|
field_value = dict(value="NA", missing=True)
|
||||||
new_dict[key] = field_value
|
new_dict[key] = field_value
|
||||||
new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
|
new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
|
||||||
dicto.update(new_dict)
|
dicto.update(new_dict)
|
||||||
@@ -1505,6 +1518,7 @@ class Wastewater(BasicSubmission):
|
|||||||
# NOTE: Due to having to run through samples in for loop we need to convert to list.
|
# NOTE: Due to having to run through samples in for loop we need to convert to list.
|
||||||
output = []
|
output = []
|
||||||
for sample in samples:
|
for sample in samples:
|
||||||
|
logger.debug(sample)
|
||||||
# NOTE: remove '-{target}' from controls
|
# NOTE: remove '-{target}' from controls
|
||||||
sample['sample'] = re.sub('-N\\d*$', '', sample['sample'])
|
sample['sample'] = re.sub('-N\\d*$', '', sample['sample'])
|
||||||
# NOTE: if sample is already in output skip
|
# NOTE: if sample is already in output skip
|
||||||
@@ -1512,14 +1526,16 @@ class Wastewater(BasicSubmission):
|
|||||||
logger.warning(f"Already have {sample['sample']}")
|
logger.warning(f"Already have {sample['sample']}")
|
||||||
continue
|
continue
|
||||||
# NOTE: Set ct values
|
# NOTE: Set ct values
|
||||||
|
logger.debug(f"Sample ct: {sample['ct']}")
|
||||||
sample[f"ct_{sample['target'].lower()}"] = sample['ct'] if isinstance(sample['ct'], float) else 0.0
|
sample[f"ct_{sample['target'].lower()}"] = sample['ct'] if isinstance(sample['ct'], float) else 0.0
|
||||||
# NOTE: Set assessment
|
# NOTE: Set assessment
|
||||||
sample[f"{sample['target'].lower()}_status"] = sample['assessment']
|
logger.debug(f"Sample assessemnt: {sample['assessment']}")
|
||||||
|
# sample[f"{sample['target'].lower()}_status"] = sample['assessment']
|
||||||
# NOTE: Get sample having other target
|
# NOTE: Get sample having other target
|
||||||
other_targets = [s for s in samples if re.sub('-N\\d*$', '', s['sample']) == sample['sample']]
|
other_targets = [s for s in samples if re.sub('-N\\d*$', '', s['sample']) == sample['sample']]
|
||||||
for s in other_targets:
|
for s in other_targets:
|
||||||
sample[f"ct_{s['target'].lower()}"] = s['ct'] if isinstance(s['ct'], float) else 0.0
|
sample[f"ct_{s['target'].lower()}"] = s['ct'] if isinstance(s['ct'], float) else 0.0
|
||||||
sample[f"{s['target'].lower()}_status"] = s['assessment']
|
# sample[f"{s['target'].lower()}_status"] = s['assessment']
|
||||||
try:
|
try:
|
||||||
del sample['ct']
|
del sample['ct']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -2915,7 +2931,8 @@ class WastewaterAssociation(SubmissionSampleAssociation):
|
|||||||
sample['background_color'] = f"rgb({red}, {grn}, {blu})"
|
sample['background_color'] = f"rgb({red}, {grn}, {blu})"
|
||||||
try:
|
try:
|
||||||
sample[
|
sample[
|
||||||
'tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})<br>- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})"
|
# 'tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})<br>- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})"
|
||||||
|
'tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)}<br>- ct N2: {'{:.2f}'.format(self.ct_n2)}"
|
||||||
except (TypeError, AttributeError) as e:
|
except (TypeError, AttributeError) as e:
|
||||||
logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.")
|
logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.")
|
||||||
return sample
|
return sample
|
||||||
|
|||||||
@@ -171,9 +171,9 @@ class InfoWriter(object):
|
|||||||
try:
|
try:
|
||||||
sheet.cell(row=loc['row'], column=loc['column'], value=v['value'])
|
sheet.cell(row=loc['row'], column=loc['column'], value=v['value'])
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.error(f"Can't write {k} to that cell due to {e}")
|
logger.error(f"Can't write {k} to that cell due to AttributeError: {e}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Can't write {v} to that cell due to {e}")
|
logger.error(f"Can't write {v} to that cell due to ValueError: {e}")
|
||||||
sheet.cell(row=loc['row'], column=loc['column'], value=v['value'].name)
|
sheet.cell(row=loc['row'], column=loc['column'], value=v['value'].name)
|
||||||
return self.sub_object.custom_info_writer(self.xl, info=final_info, custom_fields=self.info_map['custom'])
|
return self.sub_object.custom_info_writer(self.xl, info=final_info, custom_fields=self.info_map['custom'])
|
||||||
|
|
||||||
|
|||||||
@@ -645,6 +645,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}")
|
||||||
match value:
|
match value:
|
||||||
case dict():
|
case dict():
|
||||||
if isinstance(value['value'], tuple):
|
if isinstance(value['value'], tuple):
|
||||||
@@ -653,14 +654,26 @@ 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}")
|
||||||
check = Contact.query(name=value['value'])
|
check = Contact.query(name=value['value'])
|
||||||
if check is None:
|
logger.debug(f"Check came back with {check}")
|
||||||
org = Organization.query(name=values.data['submitting_lab']['value'])
|
if not isinstance(check, Contact):
|
||||||
contact = org.contacts[0].name
|
org = values.data['submitting_lab']['value']
|
||||||
|
logger.debug(f"Checking organization: {org}")
|
||||||
|
if isinstance(org, str):
|
||||||
|
org = Organization.query(name=values.data['submitting_lab']['value'], limit=1)
|
||||||
|
if isinstance(org, Organization):
|
||||||
|
contact = org.contacts[0].name
|
||||||
|
else:
|
||||||
|
logger.warning(f"All attempts at defaulting Contact failed, returning: {value}")
|
||||||
|
return value
|
||||||
if isinstance(contact, tuple):
|
if isinstance(contact, tuple):
|
||||||
contact = contact[0]
|
contact = contact[0]
|
||||||
return dict(value=contact, missing=True)
|
value = dict(value=f"Defaulted to: {contact}", missing=True)
|
||||||
|
logger.debug(f"Value after query: {value}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
|
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):
|
||||||
@@ -983,6 +996,17 @@ class PydContact(BaseModel):
|
|||||||
phone: str | None
|
phone: str | None
|
||||||
email: str | None
|
email: str | None
|
||||||
|
|
||||||
|
@field_validator("phone")
|
||||||
|
@classmethod
|
||||||
|
def enforce_phone_number(cls, value):
|
||||||
|
area_regex = re.compile(r"^\(?(\d{3})\)?(-| )?")
|
||||||
|
if len(value) > 8:
|
||||||
|
match = area_regex.match(value)
|
||||||
|
logger.debug(f"Match: {match.group(1)}")
|
||||||
|
value = area_regex.sub(f"({match.group(1).strip()}) ", value)
|
||||||
|
logger.debug(f"Output phone: {value}")
|
||||||
|
return value
|
||||||
|
|
||||||
def toSQL(self) -> Contact:
|
def toSQL(self) -> Contact:
|
||||||
"""
|
"""
|
||||||
Converts this instance into a backend.db.models.organization.Contact instance
|
Converts this instance into a backend.db.models.organization.Contact instance
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from PyQt6.QtGui import QAction
|
|||||||
from pathlib import Path
|
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
|
from backend import SubmissionType, Reagent, BasicSample, Organization
|
||||||
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
|
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user
|
||||||
from .functions import select_save_file, select_open_file
|
from .functions import select_save_file, select_open_file
|
||||||
# from datetime import date
|
# from datetime import date
|
||||||
from .pop_ups import HTMLPop, AlertPop
|
from .pop_ups import HTMLPop, AlertPop
|
||||||
@@ -25,6 +25,7 @@ from .controls_chart import ControlsViewer
|
|||||||
from .summary import Summary
|
from .summary import Summary
|
||||||
from .turnaround import TurnaroundTime
|
from .turnaround import TurnaroundTime
|
||||||
from .omni_search import SearchBox
|
from .omni_search import SearchBox
|
||||||
|
from .omni_manager import ManagerWindow
|
||||||
|
|
||||||
logger = logging.getLogger(f'submissions.{__name__}')
|
logger = logging.getLogger(f'submissions.{__name__}')
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ class App(QMainWindow):
|
|||||||
fileMenu = menuBar.addMenu("&File")
|
fileMenu = menuBar.addMenu("&File")
|
||||||
editMenu = menuBar.addMenu("&Edit")
|
editMenu = menuBar.addMenu("&Edit")
|
||||||
# NOTE: Creating menus using a title
|
# NOTE: Creating menus using a title
|
||||||
methodsMenu = menuBar.addMenu("&Methods")
|
methodsMenu = menuBar.addMenu("&Search")
|
||||||
maintenanceMenu = menuBar.addMenu("&Monthly")
|
maintenanceMenu = menuBar.addMenu("&Monthly")
|
||||||
helpMenu = menuBar.addMenu("&Help")
|
helpMenu = menuBar.addMenu("&Help")
|
||||||
helpMenu.addAction(self.helpAction)
|
helpMenu.addAction(self.helpAction)
|
||||||
@@ -82,6 +83,9 @@ class App(QMainWindow):
|
|||||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||||
maintenanceMenu.addAction(self.joinPCRAction)
|
maintenanceMenu.addAction(self.joinPCRAction)
|
||||||
editMenu.addAction(self.editReagentAction)
|
editMenu.addAction(self.editReagentAction)
|
||||||
|
editMenu.addAction(self.manageOrgsAction)
|
||||||
|
if not is_power_user():
|
||||||
|
editMenu.setEnabled(False)
|
||||||
|
|
||||||
def _createToolBar(self):
|
def _createToolBar(self):
|
||||||
"""
|
"""
|
||||||
@@ -106,6 +110,7 @@ class App(QMainWindow):
|
|||||||
self.yamlExportAction = QAction("Export Type Example", self)
|
self.yamlExportAction = QAction("Export Type Example", self)
|
||||||
self.yamlImportAction = QAction("Import Type Template", self)
|
self.yamlImportAction = QAction("Import Type Template", self)
|
||||||
self.editReagentAction = QAction("Edit Reagent", self)
|
self.editReagentAction = QAction("Edit Reagent", self)
|
||||||
|
self.manageOrgsAction = QAction("Manage Clients", self)
|
||||||
|
|
||||||
def _connectActions(self):
|
def _connectActions(self):
|
||||||
"""
|
"""
|
||||||
@@ -123,6 +128,7 @@ class App(QMainWindow):
|
|||||||
self.yamlImportAction.triggered.connect(self.import_ST_yaml)
|
self.yamlImportAction.triggered.connect(self.import_ST_yaml)
|
||||||
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
|
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
|
||||||
self.editReagentAction.triggered.connect(self.edit_reagent)
|
self.editReagentAction.triggered.connect(self.edit_reagent)
|
||||||
|
self.manageOrgsAction.triggered.connect(self.manage_orgs)
|
||||||
|
|
||||||
def showAbout(self):
|
def showAbout(self):
|
||||||
"""
|
"""
|
||||||
@@ -207,6 +213,11 @@ class App(QMainWindow):
|
|||||||
def update_data(self):
|
def update_data(self):
|
||||||
self.table_widget.sub_wid.setData(page=self.table_widget.pager.page_anchor, page_size=page_size)
|
self.table_widget.sub_wid.setData(page=self.table_widget.pager.page_anchor, page_size=page_size)
|
||||||
|
|
||||||
|
def manage_orgs(self):
|
||||||
|
dlg = ManagerWindow(parent=self, object_type=Organization, extras=[])
|
||||||
|
if dlg.exec():
|
||||||
|
new_org = dlg.parse_form()
|
||||||
|
logger.debug(new_org.__dict__)
|
||||||
|
|
||||||
class AddSubForm(QWidget):
|
class AddSubForm(QWidget):
|
||||||
|
|
||||||
|
|||||||
88
src/submissions/frontend/widgets/omni_add_edit.py
Normal file
88
src/submissions/frontend/widgets/omni_add_edit.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QLabel, QDialog, QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit
|
||||||
|
)
|
||||||
|
from sqlalchemy import String, TIMESTAMP
|
||||||
|
from sqlalchemy.orm import InstrumentedAttribute
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
class AddEdit(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, instance: Any):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.instance = instance
|
||||||
|
self.object_type = instance.__class__
|
||||||
|
self.layout = QGridLayout(self)
|
||||||
|
|
||||||
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
fields = {k: v for k, v in self.object_type.__dict__.items() if
|
||||||
|
isinstance(v, InstrumentedAttribute) and k != "id"}
|
||||||
|
for key, field in fields.items():
|
||||||
|
try:
|
||||||
|
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
|
||||||
|
value=getattr(self.instance, key))
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
self.layout.addWidget(widget, self.layout.rowCount(), 0)
|
||||||
|
self.layout.addWidget(self.buttonBox)
|
||||||
|
self.setWindowTitle(f"Add/Edit {self.object_type.__name__}")
|
||||||
|
self.setMinimumSize(600, 50 * len(fields))
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
results = {result[0]:result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)]}
|
||||||
|
# logger.debug(results)
|
||||||
|
model = self.object_type.get_pydantic_model()
|
||||||
|
model = model(**results)
|
||||||
|
try:
|
||||||
|
extras = list(model.model_extra.keys())
|
||||||
|
except AttributeError:
|
||||||
|
extras = []
|
||||||
|
fields = list(model.model_fields.keys()) + extras
|
||||||
|
for field in fields:
|
||||||
|
# logger.debug(result)
|
||||||
|
self.instance.__setattr__(field, model.__getattribute__(field))
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
|
||||||
|
class EditProperty(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent: AddEdit, key: str, column_type: Any, value):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
|
self.layout = QGridLayout()
|
||||||
|
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
|
self.setObjectName(key)
|
||||||
|
match column_type:
|
||||||
|
case String():
|
||||||
|
self.widget = QLineEdit(self)
|
||||||
|
self.widget.setText(value)
|
||||||
|
case TIMESTAMP():
|
||||||
|
self.widget = QDateEdit(self)
|
||||||
|
self.widget.setDate(value)
|
||||||
|
case _:
|
||||||
|
logger.error(f"{column_type} not a supported type.")
|
||||||
|
self.widget = None
|
||||||
|
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
match self.widget:
|
||||||
|
case QLineEdit():
|
||||||
|
value = self.widget.text()
|
||||||
|
case QDateEdit():
|
||||||
|
value = self.widget.date()
|
||||||
|
case _:
|
||||||
|
value = None
|
||||||
|
return self.objectName(), value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
227
src/submissions/frontend/widgets/omni_manager.py
Normal file
227
src/submissions/frontend/widgets/omni_manager.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from PyQt6.QtCore import QSortFilterProxyModel, Qt
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QLabel, QDialog,
|
||||||
|
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit
|
||||||
|
)
|
||||||
|
from sqlalchemy import String, TIMESTAMP
|
||||||
|
from sqlalchemy.orm import InstrumentedAttribute
|
||||||
|
from sqlalchemy.orm.collections import InstrumentedList
|
||||||
|
from sqlalchemy.orm.properties import ColumnProperty
|
||||||
|
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
||||||
|
from pandas import DataFrame
|
||||||
|
from backend import db
|
||||||
|
import logging
|
||||||
|
from .omni_add_edit import AddEdit
|
||||||
|
from .omni_search import SearchBox
|
||||||
|
|
||||||
|
from frontend.widgets.submission_table import pandasModel
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerWindow(QDialog):
|
||||||
|
"""
|
||||||
|
Initially this is a window to manage Organization Contacts, but hope to abstract it more later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.object_type = self.original_type = object_type
|
||||||
|
self.instance = None
|
||||||
|
self.extras = extras
|
||||||
|
self.context = kwargs
|
||||||
|
self.layout = QGridLayout(self)
|
||||||
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
self.setMinimumSize(600, 600)
|
||||||
|
sub_classes = ["Any"] + [cls.__name__ for cls in self.object_type.__subclasses__()]
|
||||||
|
if len(sub_classes) > 1:
|
||||||
|
self.sub_class = QComboBox(self)
|
||||||
|
self.sub_class.setObjectName("sub_class")
|
||||||
|
self.sub_class.addItems(sub_classes)
|
||||||
|
self.sub_class.currentTextChanged.connect(self.update_options)
|
||||||
|
self.sub_class.setEditable(False)
|
||||||
|
self.sub_class.setMinimumWidth(self.minimumWidth())
|
||||||
|
self.layout.addWidget(self.sub_class, 0, 0)
|
||||||
|
else:
|
||||||
|
self.sub_class = None
|
||||||
|
# self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0)
|
||||||
|
self.options = QComboBox(self)
|
||||||
|
self.options.setObjectName("options")
|
||||||
|
self.update_options()
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.setWindowTitle(f"Manage {self.object_type.__name__}")
|
||||||
|
|
||||||
|
def update_options(self):
|
||||||
|
"""
|
||||||
|
Changes form inputs based on sample type
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.sub_class:
|
||||||
|
self.object_type = getattr(db, self.sub_class.currentText())
|
||||||
|
options = [item.name for item in self.object_type.query()]
|
||||||
|
self.options.clear()
|
||||||
|
self.options.addItems(options)
|
||||||
|
self.options.setEditable(False)
|
||||||
|
self.options.setMinimumWidth(self.minimumWidth())
|
||||||
|
self.layout.addWidget(self.options, 1, 0, 1, 1)
|
||||||
|
self.add_button = QPushButton("Add New")
|
||||||
|
self.layout.addWidget(self.add_button, 1, 1, 1, 1)
|
||||||
|
self.options.currentTextChanged.connect(self.update_data)
|
||||||
|
self.add_button.clicked.connect(self.add_new)
|
||||||
|
self.update_data()
|
||||||
|
|
||||||
|
def update_data(self):
|
||||||
|
deletes = [item for item in self.findChildren(EditProperty)] + \
|
||||||
|
[item for item in self.findChildren(EditRelationship)] + \
|
||||||
|
[item for item in self.findChildren(QDialogButtonBox)]
|
||||||
|
for item in deletes:
|
||||||
|
item.setParent(None)
|
||||||
|
self.instance = self.object_type.query(name=self.options.currentText())
|
||||||
|
fields = {k: v for k, v in self.object_type.__dict__.items() if
|
||||||
|
isinstance(v, InstrumentedAttribute) and k != "id"}
|
||||||
|
for key, field in fields.items():
|
||||||
|
# logger.debug(f"Key: {key}, Value: {field}")
|
||||||
|
match field.property:
|
||||||
|
case ColumnProperty():
|
||||||
|
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
|
||||||
|
value=getattr(self.instance, key))
|
||||||
|
case _RelationshipDeclared():
|
||||||
|
if key != "submissions":
|
||||||
|
widget = EditRelationship(self, key=key, entity=field.comparator.entity.class_,
|
||||||
|
value=getattr(self.instance, key))
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
case _:
|
||||||
|
continue
|
||||||
|
if widget:
|
||||||
|
self.layout.addWidget(widget, self.layout.rowCount(), 0, 1, 2)
|
||||||
|
self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
results = [item.parse_form() for item in self.findChildren(EditProperty)]
|
||||||
|
# logger.debug(results)
|
||||||
|
for result in results:
|
||||||
|
# logger.debug(result)
|
||||||
|
self.instance.__setattr__(result[0], result[1])
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
def add_new(self):
|
||||||
|
dlg = AddEdit(parent=self, instance=self.object_type())
|
||||||
|
if dlg.exec():
|
||||||
|
new_instance = dlg.parse_form()
|
||||||
|
# logger.debug(new_instance.__dict__)
|
||||||
|
new_instance.save()
|
||||||
|
self.update_options()
|
||||||
|
|
||||||
|
|
||||||
|
class EditProperty(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent: ManagerWindow, key: str, column_type: Any, value):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
|
self.layout = QGridLayout()
|
||||||
|
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
|
match column_type:
|
||||||
|
case String():
|
||||||
|
self.widget = QLineEdit(self)
|
||||||
|
self.widget.setText(value)
|
||||||
|
case TIMESTAMP():
|
||||||
|
self.widget = QDateEdit(self)
|
||||||
|
self.widget.setDate(value)
|
||||||
|
case _:
|
||||||
|
self.widget = None
|
||||||
|
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
match self.widget:
|
||||||
|
case QLineEdit():
|
||||||
|
value = self.widget.text()
|
||||||
|
case QDateEdit():
|
||||||
|
value = self.widget.date()
|
||||||
|
case _:
|
||||||
|
value = None
|
||||||
|
return self.objectName(), value
|
||||||
|
|
||||||
|
|
||||||
|
class EditRelationship(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent, key: str, entity: Any, value):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.entity = entity
|
||||||
|
self.data = value
|
||||||
|
self.label = QLabel(key.title().replace("_", " "))
|
||||||
|
self.setObjectName(key)
|
||||||
|
self.table = QTableView()
|
||||||
|
self.add_button = QPushButton("Add New")
|
||||||
|
self.add_button.clicked.connect(self.add_new)
|
||||||
|
self.existing_button = QPushButton("Add Existing")
|
||||||
|
self.existing_button.clicked.connect(self.add_existing)
|
||||||
|
self.layout = QGridLayout()
|
||||||
|
self.layout.addWidget(self.label, 0, 0, 1, 5)
|
||||||
|
self.layout.addWidget(self.table, 1, 0, 1, 8)
|
||||||
|
self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.set_data()
|
||||||
|
|
||||||
|
def parse_row(self, x):
|
||||||
|
context = {item: x.sibling(x.row(), self.data.columns.get_loc(item)).data() for item in self.data.columns}
|
||||||
|
try:
|
||||||
|
object = self.entity.query(**context)
|
||||||
|
except KeyError:
|
||||||
|
object = None
|
||||||
|
# logger.debug(object)
|
||||||
|
self.table.doubleClicked.disconnect()
|
||||||
|
self.add_edit(instance=object)
|
||||||
|
|
||||||
|
def add_new(self, instance: Any = None):
|
||||||
|
if not instance:
|
||||||
|
instance = self.entity()
|
||||||
|
dlg = AddEdit(self, instance=instance)
|
||||||
|
if dlg.exec():
|
||||||
|
new_instance = dlg.parse_form()
|
||||||
|
# logger.debug(new_instance.__dict__)
|
||||||
|
addition = getattr(self.parent().instance, self.objectName())
|
||||||
|
if isinstance(addition, InstrumentedList):
|
||||||
|
addition.append(new_instance)
|
||||||
|
self.parent().instance.save()
|
||||||
|
self.parent().update_data()
|
||||||
|
|
||||||
|
def add_existing(self):
|
||||||
|
dlg = SearchBox(self, object_type=self.entity, returnable=True, extras=[])
|
||||||
|
if dlg.exec():
|
||||||
|
rows = dlg.return_selected_rows()
|
||||||
|
# print(f"Rows selected: {[row for row in rows]}")
|
||||||
|
for row in rows:
|
||||||
|
instance = self.entity.query(**row)
|
||||||
|
# logger.debug(instance)
|
||||||
|
addition = getattr(self.parent().instance, self.objectName())
|
||||||
|
if isinstance(addition, InstrumentedList):
|
||||||
|
addition.append(instance)
|
||||||
|
self.parent().instance.save()
|
||||||
|
self.parent().update_data()
|
||||||
|
|
||||||
|
def set_data(self) -> None:
|
||||||
|
"""
|
||||||
|
sets data in model
|
||||||
|
"""
|
||||||
|
# logger.debug(self.data)
|
||||||
|
self.data = DataFrame.from_records([item.to_dict() for item in self.data])
|
||||||
|
try:
|
||||||
|
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
self.columns_of_interest = []
|
||||||
|
# try:
|
||||||
|
# self.data['id'] = self.data['id'].apply(str)
|
||||||
|
# self.data['id'] = self.data['id'].str.zfill(3)
|
||||||
|
# except (TypeError, KeyError) as e:
|
||||||
|
# logger.error(f"Couldn't format id string: {e}")
|
||||||
|
proxy_model = QSortFilterProxyModel()
|
||||||
|
proxy_model.setSourceModel(pandasModel(self.data))
|
||||||
|
self.table.setModel(proxy_model)
|
||||||
|
self.table.doubleClicked.connect(self.parse_row)
|
||||||
@@ -7,7 +7,7 @@ from pandas import DataFrame
|
|||||||
from PyQt6.QtCore import QSortFilterProxyModel
|
from PyQt6.QtCore import QSortFilterProxyModel
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QLabel, QVBoxLayout, QDialog,
|
QLabel, QVBoxLayout, QDialog,
|
||||||
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox
|
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox
|
||||||
)
|
)
|
||||||
from .submission_table import pandasModel
|
from .submission_table import pandasModel
|
||||||
import logging
|
import logging
|
||||||
@@ -20,7 +20,7 @@ class SearchBox(QDialog):
|
|||||||
The full search widget.
|
The full search widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
|
def __init__(self, parent, object_type: Any, extras: List[str], returnable: bool = False, **kwargs):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.object_type = self.original_type = object_type
|
self.object_type = self.original_type = object_type
|
||||||
self.extras = extras
|
self.extras = extras
|
||||||
@@ -44,6 +44,14 @@ class SearchBox(QDialog):
|
|||||||
self.setWindowTitle(f"Search {self.object_type.__name__}")
|
self.setWindowTitle(f"Search {self.object_type.__name__}")
|
||||||
self.update_widgets()
|
self.update_widgets()
|
||||||
self.update_data()
|
self.update_data()
|
||||||
|
if returnable:
|
||||||
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2)
|
||||||
|
self.results.doubleClicked.disconnect()
|
||||||
|
self.results.doubleClicked.connect(self.accept)
|
||||||
|
|
||||||
def update_widgets(self):
|
def update_widgets(self):
|
||||||
"""
|
"""
|
||||||
@@ -63,7 +71,7 @@ class SearchBox(QDialog):
|
|||||||
for iii, searchable in enumerate(self.object_type.searchables):
|
for iii, searchable in enumerate(self.object_type.searchables):
|
||||||
widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
|
widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
|
||||||
widget.setObjectName(searchable)
|
widget.setObjectName(searchable)
|
||||||
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)
|
||||||
self.update_data()
|
self.update_data()
|
||||||
|
|
||||||
@@ -87,6 +95,13 @@ 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):
|
||||||
|
rows = sorted(set(index.row() for index in
|
||||||
|
self.results.selectedIndexes()))
|
||||||
|
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)}
|
||||||
|
yield output
|
||||||
|
|
||||||
|
|
||||||
class FieldSearch(QWidget):
|
class FieldSearch(QWidget):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import importlib
|
|||||||
import time
|
import time
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, 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 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
|
||||||
@@ -478,23 +478,25 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
|
|
||||||
def set_scripts(self):
|
def set_scripts(self):
|
||||||
"""
|
"""
|
||||||
Imports all functions from "scripts" folder which will run their @registers, adding them to ctx scripts
|
Imports all functions from "scripts" folder, adding them to ctx scripts
|
||||||
"""
|
"""
|
||||||
p = Path(__file__).parent.joinpath("scripts").absolute()
|
if check_if_app():
|
||||||
subs = [item.stem for item in p.glob("*.py") if "__" not in item.stem]
|
p = Path(sys._MEIPASS).joinpath("files", "scripts")
|
||||||
for sub in subs:
|
else:
|
||||||
mod = importlib.import_module(f"tools.scripts.{sub}")
|
p = Path(__file__).parents[2].joinpath("scripts").absolute()
|
||||||
try:
|
if p.__str__() not in sys.path:
|
||||||
func = mod.__getattribute__(sub)
|
sys.path.append(p.__str__())
|
||||||
except AttributeError:
|
modules = p.glob("[!__]*.py")
|
||||||
try:
|
for module in modules:
|
||||||
func = mod.__getattribute__("script")
|
mod = importlib.import_module(module.stem)
|
||||||
except AttributeError:
|
for function in getmembers(mod, isfunction):
|
||||||
continue
|
name = function[0]
|
||||||
if sub in self.startup_scripts.keys():
|
func = function[1]
|
||||||
self.startup_scripts[sub] = func
|
# NOTE: assign function based on its name being in config: startup/teardown
|
||||||
if sub in self.teardown_scripts.keys():
|
if name in self.startup_scripts.keys():
|
||||||
self.teardown_scripts[sub] = func
|
self.startup_scripts[name] = func
|
||||||
|
if name in self.teardown_scripts.keys():
|
||||||
|
self.teardown_scripts[name] = func
|
||||||
|
|
||||||
@timer
|
@timer
|
||||||
def run_startup(self):
|
def run_startup(self):
|
||||||
@@ -502,9 +504,12 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
Runs startup scripts.
|
Runs startup scripts.
|
||||||
"""
|
"""
|
||||||
for script in self.startup_scripts.values():
|
for script in self.startup_scripts.values():
|
||||||
logger.info(f"Running startup script: {script.__name__}")
|
try:
|
||||||
thread = Thread(target=script, args=(ctx,))
|
logger.info(f"Running startup script: {script.__name__}")
|
||||||
thread.start()
|
thread = Thread(target=script, args=(ctx,))
|
||||||
|
thread.start()
|
||||||
|
except AttributeError:
|
||||||
|
logger.error(f"Couldn't run startup script: {script}")
|
||||||
|
|
||||||
@timer
|
@timer
|
||||||
def run_teardown(self):
|
def run_teardown(self):
|
||||||
@@ -512,9 +517,12 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
Runs teardown scripts.
|
Runs teardown scripts.
|
||||||
"""
|
"""
|
||||||
for script in self.teardown_scripts.values():
|
for script in self.teardown_scripts.values():
|
||||||
logger.info(f"Running teardown script: {script.__name__}")
|
try:
|
||||||
thread = Thread(target=script, args=(ctx,))
|
logger.info(f"Running teardown script: {script.__name__}")
|
||||||
thread.start()
|
thread = Thread(target=script, args=(ctx,))
|
||||||
|
thread.start()
|
||||||
|
except AttributeError:
|
||||||
|
logger.error(f"Couldn't run teardown script: {script}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str:
|
def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str:
|
||||||
@@ -874,7 +882,7 @@ class Result(BaseModel, arbitrary_types_allowed=True):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.owner = inspect.stack()[1].function
|
self.owner = stack()[1].function
|
||||||
|
|
||||||
def report(self):
|
def report(self):
|
||||||
from frontend.widgets.pop_ups import AlertPop
|
from frontend.widgets.pop_ups import AlertPop
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
"""
|
|
||||||
Test script for teardown_scripts
|
|
||||||
"""
|
|
||||||
|
|
||||||
# from .. import register_script
|
|
||||||
|
|
||||||
# @register_script
|
|
||||||
def goodbye(ctx):
|
|
||||||
print("\n\nGoodbye. Thank you for using Robotics Submission Tracker.\n\n")
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
"""
|
|
||||||
Test script for startup_scripts
|
|
||||||
"""
|
|
||||||
# from .. import register_script
|
|
||||||
|
|
||||||
# @register_script
|
|
||||||
def hello(ctx):
|
|
||||||
print("\n\nHello! Welcome to Robotics Submission Tracker.\n\n")
|
|
||||||
@@ -36,6 +36,7 @@ a = Analysis(
|
|||||||
("docs\\build", "files\\docs"),
|
("docs\\build", "files\\docs"),
|
||||||
("src\\submissions\\resources\\*", "files\\resources"),
|
("src\\submissions\\resources\\*", "files\\resources"),
|
||||||
("alembic.ini", "files"),
|
("alembic.ini", "files"),
|
||||||
|
("src\\scripts\\*.py", "files\\scripts")
|
||||||
],
|
],
|
||||||
hiddenimports=["pyodbc"],
|
hiddenimports=["pyodbc"],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
|
|||||||
Reference in New Issue
Block a user