def __init__(self, app=None, db=None): super().__init__() self._app = app self._db = db self._rpc = self._app.rpc self._personnel_model = PersonnelModel(app=self._app) self._sound_player = SoundPlayer(app=self._app)
def __init__(self, app=None, db=None): super().__init__() self._app = app self._logger = logging.getLogger(__name__) self._db = db self._sound_player = SoundPlayer()
def __init__(self, app=None, db=None): super().__init__() self._logger = logging.getLogger(__name__) self._app = app self._db = db self._backup_thread = QThread() self._backup_worker = None self._sound_player = SoundPlayer() self._validation_model = ValidationModel() self._initialize_validation_model() self._haul_level_validations = HaulLevelValidations(app=self._app, db=self._db) self._on_entry_validations = OnEntryValidations(app=self._app, db=self._db)
def __init__(self, app=None, db=None): super().__init__() self._logger = logging.getLogger(__name__) self._app = app self._db = db self._sound_player = SoundPlayer() self._label_printer = LabelPrinter(app=self._app, db=self._db) self._label_printer.tagIdChanged.connect( self._updated_printer_tag_received) self._label_printer.printerStatusReceived.connect( self._printer_status_received)
def __init__(self, app=None, db=None): super().__init__() self._logger = logging.getLogger(__name__) self._app = app self._db = db # Set up the models self._model = SpecialActionsModel() self._pi_project_model = PiProjectModel() self._sound_player = SoundPlayer() self._label_printer = LabelPrinter(app=self._app, db=self._db) self._label_printer.tagIdChanged.connect( self._updated_printer_tag_received) self._label_printer.printerStatusReceived.connect( self._printer_status_received) self._standardSurveySpecimen = None self._parent_specimen_count = 0 self._row_index = -1 self._row_widget_type = None
def __init__(self): qInstallMessageHandler(FramUtil.qt_msg_handler) # self.app = QApplication(sys.argv) appGuid = 'F3FF80BA-BA05-4277-8063-82A6DB9245A2' self.app = QtSingleApplication(appGuid, sys.argv) if self.app.isRunning(): sys.exit(0) self.app.unhandledExceptionCaught.connect(self.exception_caught) self.engine = QQmlApplicationEngine() self.context = self.engine.rootContext() qmlRegisterType(FramTreeItem, 'FramTreeItem', 1, 0, 'FramTreeItem') # Set Contexts wfs = WindowFrameSize() self.context.setContextProperty('wfs', wfs) fl = FramLog() self.context.setContextProperty('framLog', fl) db = TrawlBackdeckDB() self.context.setContextProperty('db', db) self.sound_player = SoundPlayer() self.label_printer = LabelPrinter(app=self, db=db) self.settings = Settings(db=db) self.state_machine = StateMachine(app=self, db=db) self.serial_port_manager = SerialPortManager(app=self, db=db) self.network_testing = NetworkTesting(app=self, db=db) self.protocol_viewer = ProtocolViewer(app=self, db=db) self.home = Home(app=self, db=db) self.haul_selection = HaulSelection(app=self, db=db) self.process_catch = ProcessCatch(app=self, db=db) self.weigh_baskets = WeighBaskets(app=self, db=db) self.fish_sampling = FishSampling(app=self, db=db) self.salmon_sampling = SalmonSampling(app=self, db=db) self.corals_sampling = CoralsSampling(app=self, db=db) self.special_actions = SpecialActions(app=self, db=db) self.qaqc = QAQC(app=self, db=db) self.reports = Reports(db=db) self.notes = Notes(app=self, db=db) self.context.setContextProperty("soundPlayer", self.sound_player) self.context.setContextProperty("settings", self.settings) self.context.setContextProperty("home", self.home) self.context.setContextProperty("haulSelection", self.haul_selection) self.context.setContextProperty('processCatch', self.process_catch) self.context.setContextProperty("weighBaskets", self.weigh_baskets) self.context.setContextProperty("fishSampling", self.fish_sampling) self.context.setContextProperty("salmonSampling", self.salmon_sampling) self.context.setContextProperty("coralsSampling", self.corals_sampling) self.context.setContextProperty("specialActions", self.special_actions) self.context.setContextProperty("qaqc", self.qaqc) self.context.setContextProperty("reports", self.reports) self.context.setContextProperty("serialPortManager", self.serial_port_manager) self.context.setContextProperty("stateMachine", self.state_machine) self.context.setContextProperty("protocolViewer", self.protocol_viewer) self.context.setContextProperty("networkTesting", self.network_testing) self.context.setContextProperty("notes", self.notes) self.engine.load(QUrl('qrc:/qml/trawl/main_backdeck.qml')) self.win = self.engine.rootObjects()[0] self.msg_box = self.win.findChild(QObject, "dlgUnhandledException") self.engine.quit.connect(self.app.quit) sys.exit(self.app.exec_())
def __init__(self): qInstallMessageHandler(FramUtil.qt_msg_handler) self.rpc = RpcClient() # self.app = QApplication(sys.argv) appGuid = 'F3FF80BA-BA05-4277-8063-82A6DB9245A5' self.app = QtSingleApplication(appGuid, sys.argv) if self.app.isRunning(): sys.exit(0) self.app.unhandledExceptionCaught.connect(self.exception_caught) qmlRegisterType(SortFilterProxyModel, "SortFilterProxyModel", 0, 1, "SortFilterProxyModel") self.engine = QQmlApplicationEngine() self.context = self.engine.rootContext() # qmlRegisterType(FramTreeItem, 'FramTreeItem', 1, 0, 'FramTreeItem') # Set Contexts # wfs = WindowFrameSize() # self.context.setContextProperty('wfs', wfs) fl = FramLog() self.context.setContextProperty('framLog', fl) db = HookAndLineHookCutterDB() self.context.setContextProperty('db', db) self.state_machine = StateMachine(app=self, db=db) self.sound_player = SoundPlayer(app=self, db=db) self.serial_port_manager = SerialPortManager(app=self, db=db) self.sites = Sites(app=self, db=db) self.fish_sampling = FishSampling(app=self, db=db) self.label_printer = LabelPrinter(app=self, db=db) self.notes = Notes(app=self, db=db) # self.qaqc = QAQC(app=self, db=db) self.context.setContextProperty("soundPlayer", self.sound_player) self.context.setContextProperty("stateMachine", self.state_machine) self.context.setContextProperty("sites", self.sites) self.context.setContextProperty("fishSampling", self.fish_sampling) self.context.setContextProperty("serialPortManager", self.serial_port_manager) self.context.setContextProperty("labelPrinter", self.label_printer) self.context.setContextProperty("notes", self.notes) # self.context.setContextProperty("qaqc", self.qaqc) # self.setAttribute(QtCore.Qt.WA_DeleteOnClose) try: self.engine.load( QUrl('qrc:/qml/survey_backdeck/main_backdeck.qml')) self.win = self.engine.rootObjects()[0] self.msg_box = self.win.findChild(QObject, "dlgUnhandledException") self.engine.quit.connect(self.app.quit) sys.exit(self.app.exec_()) except Exception as ex: logging.error(f"bad stuff happening: {ex}")
class Drops(QObject): personnelModelChanged = pyqtSignal() selection_result_obtained = pyqtSignal(QVariant, name="selectionResultsObtained", arguments=[ "results", ]) operationAttributeDeleted = pyqtSignal() new_drop_added = pyqtSignal(QVariant, name="newDropAdded", arguments=[ "dropJson", ]) exception_encountered = pyqtSignal(str, str, name="exceptionEncountered", arguments=["message", "action"]) def __init__(self, app=None, db=None): super().__init__() self._app = app self._db = db self._rpc = self._app.rpc self._personnel_model = PersonnelModel(app=self._app) self._sound_player = SoundPlayer(app=self._app) @pyqtProperty(FramListModel, notify=personnelModelChanged) def personnelModel(self): """ Method to return the self._personnel_model :return: """ return self._personnel_model @pyqtSlot(int, result=QVariant, name="insertOperations") def insert_operations(self, drop_number: int) -> dict: """ Method to insert a new drop and angler operations. This is called by the DropAngler when the Start time button is clicked. Remember that clicking one of the start time buttons activates the times for all three anglers, so this will involved inserting a drop operation as well as three children operations, one for each angler This method will return the Drop ID and Angler IDs if they already exist. :param drop_number: Integer of the drop number :param start_time: string containing the starting time for the drop :return: Operation IDs - dictionary containing the drop, angler A, angler B, and angler C operation IDs """ results = [] logging.info(f"insert_operations: drop_number={str(int(drop_number))}") try: # Get the DROP_TYPE_LU_ID and ANGLER_TYPE_LU_ID from LOOKUPS table sql = "SELECT LOOKUP_ID, VALUE FROM LOOKUPS WHERE Type = 'Operation' AND (VALUE = 'Drop' OR VALUE = 'Angler');" lu_ids = self._rpc.execute_query(sql=sql) if lu_ids: drop_type_lu_id = [x[0] for x in lu_ids if x[1] == "Drop"][0] angler_type_lu_id = [x[0] for x in lu_ids if x[1] == "Angler"][0] # Check if the drop operation already exists in the OPERATIONS table # Handled via UNIQUE constraint on the OPERATIONS table # Insert the Drop Operation sql = "INSERT INTO OPERATIONS(PARENT_OPERATION_ID, OPERATION_NUMBER, OPERATION_TYPE_LU_ID) VALUES(?, ?, ?);" params = [ self._app.state_machine.siteOpId, drop_number, drop_type_lu_id ] drop_op_id = self._rpc.execute_query_get_id(sql=sql, params=params) results = {"Drop " + str(int(drop_number)): drop_op_id} except Exception as ex: if "apsw.ConstraintError" in str(ex) or isinstance( ex, xrc.Fault): # #157: also catch XRC exception logging.info( f"Drop operation exists, getting the existing primary key: {ex}" ) sql = """ SELECT OPERATION_ID FROM OPERATIONS WHERE PARENT_OPERATION_ID = ? AND OPERATION_NUMBER = ? AND OPERATION_TYPE_LU_ID = ?; """ drop_op_id = self._rpc.execute_query(sql=sql, params=params) if drop_op_id: drop_op_id = drop_op_id[0][0] results = {"Drop " + str(int(drop_number)): drop_op_id} else: logging.error(f"Error reached: {ex}") self._app.state_machine.drop = None self._app.state_machine.dropOpId = None self._app.state_machine.anglerAOpId = None self._app.state_machine.anglerBOpId = None self._app.state_machine.anglerCOpId = None message = f"Failed to insert or select drop {drop_number}" action = f"Please try again" self.exception_encountered.emit(message, action) return self._app.state_machine.dropOpId = drop_op_id # Check if the angler operations already exist in the OPERATIONS table # Handled via UNIQUE constraint on the OPERATIONS table between PARENT_OPERATION_ID, OPERATION_NUMBER, OPERATION_TYPE_LU_ID # Insert the three Angler Operations for x in ["A", "B", "C"]: try: sql = "INSERT INTO OPERATIONS(PARENT_OPERATION_ID, OPERATION_NUMBER, OPERATION_TYPE_LU_ID) VALUES(?, ?, ?);" params = [drop_op_id, x, angler_type_lu_id] angler_op_id = self._rpc.execute_query_get_id(sql=sql, params=params) results["Angler " + x] = angler_op_id except Exception as ex: if "apsw.ConstraintError" in str(ex) or isinstance( ex, xrc.Fault): # #157: also catch XRC exception logging.info( f"Angler operation {x} exists, getting the existing primary key: {ex}" ) sql = """ SELECT OPERATION_ID FROM OPERATIONS WHERE PARENT_OPERATION_ID = ? AND OPERATION_NUMBER = ? AND OPERATION_TYPE_LU_ID = ?; """ angler_op_id = self._rpc.execute_query(sql=sql, params=params) if angler_op_id: angler_op_id = angler_op_id[0][0] results["Angler " + x] = angler_op_id else: logging.error(f"Error reached: {ex}") self._app.state_machine.anglerAOpId = None self._app.state_machine.anglerBOpId = None self._app.state_machine.anglerCOpId = None message = f"Failed to insert or select angler {x}" action = f"Please try again" self.exception_encountered.emit(message, action) return if x == "A": self._app.state_machine.anglerAOpId = angler_op_id elif x == "B": self._app.state_machine.anglerBOpId = angler_op_id elif x == "C": self._app.state_machine.anglerCOpId = angler_op_id # logging.info(f"newly inserted operations: {results}") return results @pyqtSlot(QVariant, str, str, str, str, QVariant, name="upsertOperationAttribute") def upsert_operation_attribute(self, operation_id: QVariant, lu_type: str, lu_value: str, value_type: str, value: str, indicator: QVariant = None): """ Method to insert or update an operational attribute :param operation_id: the operation_id as an integer as found in OPERATIONS table :param lu_type: lookup type (from the LOOKUPS table) :param lu_value: lookup value (from the LOOKUPS table) :param value_type: alpha or numeric :param value: new value to insert or update :param indicator: string used to send in additional information about the attribute being upserted. In particular, this is used when a angler person is sent in when the start time has not been captured. This creates new Drop + Angler operation IDs. In this case, the input operation_id is None, so we need to know what to assign to operation_id, i.e. which angler or drop for that matter :return: """ try: # If operation_id is NONE, that means that the operation has not been insert ye # that is a problem logging.info( f"upsertOperationalAttribute, inputs: operation_id={operation_id}, lu_type={lu_type}, " f"lu_value={lu_value}, value_type={value_type}, value={value}, indicator={indicator}" ) # Get the LOOKUP_ID for the OPERATION_ATTRIBUTE type sql = "SELECT LOOKUP_ID FROM LOOKUPS WHERE TYPE = ? AND VALUE = ?" params = [lu_type, lu_value] att_type_lu_id = self._rpc.execute_query(sql=sql, params=params) if att_type_lu_id: att_type_lu_id = att_type_lu_id[0][0] # operation_id is not an int, need to insert a new operation. This occurs if not isinstance(operation_id, int): # Insert the new Drop + Angler Operation IDs. Note that calling this method # also updates the state_machine with the new Drop + Angler operation IDs logging.info( f"operation_id={operation_id} > drop={self._app.state_machine.drop}" ) new_ops = self.insert_operations( drop_number=self._app.state_machine.drop) logging.info( f"New drop/angler operations inserted, new_operations={new_ops}" ) # Emit the drop_dict to update the siteResults JSON back in the DropsScreen.qml drop_dict = dict() drop_key = f"Drop {self._app.state_machine.drop}" drop_dict[drop_key] = { "id": new_ops[drop_key], "Anglers": { "Angler A": { "id": new_ops["Angler A"] }, "Angler B": { "id": new_ops["Angler B"] }, "Angler C": { "id": new_ops["Angler C"] } } } self.new_drop_added.emit(drop_dict) # Set the operation_id to the newly created ID, based on the provided indicator, where # indicator = Angler A, Angler B, Angler C, or Drop # if indicator in new_ops: operation_id = new_ops[indicator] # Check if the OPERATION_ATTRIBUTE type already exists sql = "SELECT * FROM OPERATION_ATTRIBUTES WHERE OPERATION_ID = ? AND ATTRIBUTE_TYPE_LU_ID = ?;" params = [operation_id, att_type_lu_id] results = self._rpc.execute_query(sql=sql, params=params) value_type = "ATTRIBUTE_ALPHA" if value_type == "alpha" else "ATTRIBUTE_NUMERIC" if value_type == "ATTRIBUTE_NUMERIC": value = float(value) if results: # Update the OPERATION_ATTRIBUTES table logging.info(f'update oa') sql = "UPDATE OPERATION_ATTRIBUTES SET " + value_type + " = ? WHERE " + \ "OPERATION_ID = ? AND ATTRIBUTE_TYPE_LU_ID = ?;" params = [value, operation_id, att_type_lu_id] else: # Insert into OPERATION_ATTRIBUTES Table logging.info(f'insert into oa') sql = "INSERT INTO OPERATION_ATTRIBUTES(OPERATION_ID, " + value_type + \ ", ATTRIBUTE_TYPE_LU_ID) VALUES(?, ?, ?);" params = [operation_id, value, att_type_lu_id] logging.info(f"sql={sql}\nparams={params}") results = self._rpc.execute_query(sql=sql, params=params) except Exception as ex: logging.error(f"Error upserting an operation attribute: {ex}") @pyqtSlot(str, name="selectOperationAttributes") def select_operation_attributes(self, set_id: str): """ Method to retrieve all of the OPERATION_ATTRIBUTES entries for all of the drops and anglers for the given set_id. This will need to recurse through each of the OPERATIONS set_id children and then get all of the associated OPERATION_ATTRIBUTES entries for those children :param set_id: str :return: """ sql = """ WITH RECURSIVE children(n) AS ( SELECT OPERATION_ID FROM OPERATIONS WHERE OPERATION_NUMBER = ? UNION SELECT o.OPERATION_ID FROM OPERATIONS o, children WHERE o.PARENT_OPERATION_ID = children.n ) SELECT l.VALUE, OPERATION_ID, PARENT_OPERATION_ID, OPERATION_NUMBER FROM OPERATIONS o INNER JOIN LOOKUPS l ON l.lookup_id = o.OPERATION_TYPE_LU_ID WHERE OPERATION_ID IN children AND l.TYPE = 'Operation' """ # AND l.VALUE IN ['Drop', 'Angler'] params = [ set_id, ] try: ids = [] results = self._rpc.execute_query(sql=sql, params=params) logging.info(f"results: {results}") if results: site = [x for x in results if x[0] == "Site"] drops = { "Drop " + x[3]: { "id": x[1], "Sinker Weight": None, "Recorder Name": None } for x in results if x[0] == "Drop" } ids = [] for k, v in drops.items(): ids.append(v["id"]) ids.extend([x[1] for x in results if x[2] == v['id']]) anglers = { "Angler " + x[3]: { "id": x[1] } for x in results if x[2] == v["id"] } v["Anglers"] = anglers # Query to get all OPERATION_ATTRIBUTES that match the ids (i.e. for all drops + anglers for this given site sql = """ SELECT o.PARENT_OPERATION_ID, oa.OPERATION_ID, l2.VALUE || " " || o.OPERATION_NUMBER, oa.ATTRIBUTE_ALPHA, l.TYPE, l.VALUE, oa.ATTRIBUTE_NUMERIC FROM OPERATION_ATTRIBUTES oa INNER JOIN LOOKUPS l ON oa.ATTRIBUTE_TYPE_LU_ID = l.LOOKUP_ID INNER JOIN OPERATIONS o ON o.OPERATION_ID = oa.OPERATION_ID INNER JOIN LOOKUPS l2 ON o.OPERATION_TYPE_LU_ID = l2.LOOKUP_ID WHERE oa.OPERATION_ID IN """ sql += " " + str(tuple(ids)) oa_results = self._rpc.execute_query(sql=sql) if oa_results: for op_att in oa_results: drop_angler_item_list = [ v["Anglers"] for k, v in drops.items() if v["id"] == op_att[0] ] # Drop Angler Items if len(drop_angler_item_list) > 0: drop_angler_item = drop_angler_item_list[0] angler_item = { k: v for k, v in drop_angler_item.items() if v["id"] == op_att[1] } key = op_att[4] + " " + op_att[5] angler_item[list( angler_item.keys())[0]][key] = op_att[3] # Drop Attributes elif "Drop Attribute" in op_att[4]: drop_item_list = [ v for k, v in drops.items() if v["id"] == op_att[1] ] if len(drop_item_list) == 1: item = drop_item_list[0] if "Sinker Weight" in op_att[5]: item["Sinker Weight"] = op_att[6] elif "Recorder Name" in op_att[5]: item["Recorder Name"] = op_att[3] self.selection_result_obtained.emit(drops) logging.info(f"drops: {drops}") if "Drop 1" in drops: self._app.state_machine.dropOpId = drops["Drop 1"]["id"] self._app.state_machine.anglerAOpId = drops["Drop 1"][ "Anglers"]["Angler A"]["id"] self._app.state_machine.anglerBOpId = drops["Drop 1"][ "Anglers"]["Angler B"]["id"] self._app.state_machine.anglerCOpId = drops["Drop 1"][ "Anglers"]["Angler C"]["id"] logging.info(f"inside Drop 1, after setting state machine") except Exception as ex: logging.error(f"Exception querying operation_attributes: {ex}") @pyqtSlot(int, str, str, name="deleteOperationAttribute") def delete_operation_attribute(self, op_id: int, lu_type: str, lu_value: str): """ Method to delete an individual operation_attributes record :param set_id: :return: """ try: # Delete the record sql = """ DELETE FROM OPERATION_ATTRIBUTES WHERE OPERATION_ID = ? AND ATTRIBUTE_TYPE_LU_ID = (SELECT LOOKUP_ID FROM LOOKUPS WHERE TYPE = ? AND VALUE = ?); """ params = [op_id, lu_type, lu_value] logging.info(f"Deleting parameters: {params}") self._rpc.execute_query(sql=sql, params=params) # Return dropTimeState to enter mode self._app.state_machine.dropTimeState = "enter" self.operationAttributeDeleted.emit() except Exception as ex: logging.error( f"Error deleting an operation_attribute record: {ex}") @pyqtSlot(str, name="playSound") def play_sound(self, sound_name): """ Method to play a sound. The sound_name will indicate the purpose of the sound. Currently the only sound played is when the first Drop/Angler timer hits 4:45, and so has 15 seconds until it should start retrieving the hooks :param sound_name: :return: """ logging.info(f"playing sound: {sound_name}") self._sound_player.play_sound(sound_name=sound_name) @pyqtSlot(str, name="getSoundPlaybackTime", result=QVariant) def get_sound_playback_time(self, begin_fishing_time): """ Method to determine the time that is 4:45 (min:sec) past the begin_fishing_time, for when to play the sound warning the HookMatrix user to have the anglers start pulling in their lines :param begin_fishing_time: :return: """ min_shift = 4 sec_shift = 45 try: if ":" in begin_fishing_time: min, sec = begin_fishing_time.split(":") min = int(min) sec = int(sec) logging.info(f"min={min}, sec={sec}") start_time = arrow.now().replace(tzinfo="US/Pacific").replace( minute=min, second=sec) end_time = start_time.shift(minutes=min_shift, seconds=sec_shift).format("mm:ss") logging.info(f"end time to play the sound: {end_time}") return end_time except Exception as ex: logging.error(f"Error getting the play sound time: {ex}") return "04:45" @pyqtSlot(QVariant, name="getDropIdFromNumber", result=QVariant) def get_drop_id_from_number(self, drop_num): """ Return operation database ID for drop based on site operation id and the drop number :param drop_num: int 1-5 :return: int, operation database id """ try: return self._rpc.execute_query( sql=''' select operation_id from operations where parent_operation_id = ? and operation_number = ? ''', params=[self._app.state_machine.siteOpId, drop_num])[0][0] except ( IndexError, Exception ) as ex: # above query will return nothing if data has yet to be entered logging.debug( f"Unable to get drop op id with drop num {drop_num}; {ex}") return None @pyqtSlot(QVariant, str, name="getAnglerOpId", result=QVariant) def get_angler_op_id(self, drop_op_id, angler_letter): """ Get angler operation DB ID using the parent drop and angler letter :param drop_op_id: database ID from OPERATIONS for parent drop record :param angler_letter: str, A B or C :return: int, DB ID """ try: return self._rpc.execute_query(sql=''' select operation_id from operations where parent_operation_id = ? and operation_number = ? ''', params=[drop_op_id, angler_letter])[0][0] except ( IndexError, Exception ) as ex: # above query will return nothing if data has yet to be entered logging.debug( f"Unable to get angler id with drop op id {drop_op_id} angler {angler_letter}; {ex}" ) return None @pyqtSlot(int, name="getAnglerGearPerfsLabel", result=QVariant) def get_angler_gear_perfs_label(self, angler_op_id): """ Gets gear perfs per angler operation_id, then abbreviates for UI label :param angler_op_id: int; operations table id for angler record :return: emits drop#, angler letter, and Gear Perf label to DropAngler.qml updateGearPerformanceLabel """ default = "Gear\nPerf." # use if all else fails try: # query gets drop and angler info, and group_concats gear perfs perfs = self._rpc.execute_query(sql=''' select group_concat(l.value) as perfs from operation_attributes oa join lookups l on oa.attribute_type_lu_id = l.lookup_id where oa.operation_id = ? and l.type = 'Angler Gear Performance' ''', params=[ angler_op_id, ]) except Exception as ex: logging.error( f"Unable to get gear perfs with angler op id {angler_op_id}; {ex}" ) return default try: if perfs and perfs[0][ 0]: # did query return results, and were there gear perfs? return self._app.gear_performance.abbreviate_gear_perfs( perfs[0][0].split( ",")) # split comma sep string and abbreviate else: return default except IndexError as ex: logging.error(f"Unable to parse gear perfs {perfs}; {ex}") return default @pyqtSlot(int, bool, name="getAnglerHooksLabel", result=QVariant) def get_angler_hooks_label(self, angler_op_id, hooks_enabled): """ Select hooks per op_id aka angler from DB. Left join to CTE ensures result will always have 5 rows, unpopulated hooks shown as null TODO: make this function return a list, not a string then make wrapper for pyQtSlot :param angler_op_id: int, operations DB id :param hooks_enabled: bool, passes thru to format hooks label :return: dict[], one per db row. E.g. [{hookNum: "1", hookContent: "Boccacio", isFish: True}...] """ try: hooks = self._rpc.execute_query(sql=""" with hooks as (--cartesian join CTE creates 5 records per angler select o.operation_id as angler_op_id ,h.hook_num FROM operations o JOIN ( select '1' as hook_num union all select '2' union all select '3' union all select '4' union all select '5' ) h on 1 = 1 WHERE o.operation_id = ? ) select h.hook_num --using hook_num_lbl instead ,case when l.lookup_id is null then '_' else h.hook_num end as hook_num_lbl ,cc.display_name from hooks h left join catch c --left join ensures 5 rows always returned per angler on h.angler_op_id = c.operation_id and h.hook_num = c.receptacle_seq left join catch_content_lu cc on c.hm_catch_content_id = cc.catch_content_id left JOIN lookups l on c.receptacle_type_id = l.lookup_id and l.value = 'Hook' order by h.hook_num desc """, params=[ angler_op_id, ]) except Exception as ex: logging.error( f"Unable to query hooks with angler op id {angler_op_id}; {ex}" ) return '' if len(hooks) > 0: hooks_list = [ { 'hookNum': h[0], 'hookNumLbl': h[1], 'hookContent': h[2], 'isFish': self._app.hooks.is_fish(h[2]) # tie in function from hooks } for h in hooks ] return self.format_hooks_label(hooks_list, hooks_enabled) else: return self.format_hooks_label([], hooks_enabled) @staticmethod def format_hooks_label(hooks_list, hooks_enabled): """ format hooks text for RichText QML text object :param hooks_list: list of dicts (e.g. [{'hookNum': 1, 'hookContent': 'Yelloweye', 'isFish': True}] :param hooks_enabled: allow italic string if disabled :return: string, rich text """ gray = '#808080' black = '#000000' green = '#008000' undeployed = all(h['hookContent'] == 'Undeployed' for h in hooks_list) if not hooks_list and hooks_enabled: return f'<font color=\"{black}\">Hooks<br>_,_,_,_,_</font>' elif not hooks_list and not hooks_enabled: return f'<i><font color=\"{gray}\">Hooks<br>_,_,_,_,_</font></i>' elif undeployed and hooks_enabled: # if all undeployed, set label to UN return 'Hooks<br>UN' elif undeployed and not hooks_enabled: return f'<i><font color=\"{gray}\">Hooks<br>UN</font></i>' elif not hooks_enabled: joined_hooks = ','.join(h['hookNumLbl'] for h in hooks_list) return f'<i><font color=\"{gray}\">Hooks<br>{joined_hooks}</font></i>' else: hooks_lbl = 'Hooks<br>' for hook in hooks_list: hook_num_lbl = hook['hookNumLbl'] is_fish = hook['isFish'] if is_fish: hooks_lbl += f"<font color=\"{green}\">{hook_num_lbl},</font>" elif not hook['hookContent']: hooks_lbl += f"<font color=\"{black}\">_,</font>" else: hooks_lbl += f"<font color=\"{black}\">{hook_num_lbl},</font>" return ''.join( hooks_lbl.rsplit(",", 1) ) # split last comma, joins with empty str (remove last comma) @pyqtSlot(QVariant, name='isAnglerDoneFishing', result=bool) def is_angler_done_fishing(self, angler_op_id): """ check if "At Surface" record exists for angler, used for forward nav purposes Coding function in Drops.py so it can be used independent of hooks/gear perf being filled out TODO: Add as property, for Angler class??? :param angler_op_id: int, angler Operation DB id :return: boolean """ try: results = self._rpc.execute_query(sql=''' select operation_attribute_id from operation_attributes oa join lookups l on oa.attribute_type_lu_id = l.lookup_id where oa.operation_id = ? and l.type = 'Angler Time' and l.value = 'At Surface' ''', params=[ angler_op_id, ]) return len(results) > 0 except Exception as ex: logging.error( f"Unable to query if angler {angler_op_id} is done fishing; {ex}" ) return None
class QAQC(QObject): """ Class for the QAQCScreen. """ validationModelChanged = pyqtSignal() backupStatusChanged = pyqtSignal(bool, str, arguments=["success", "msg"]) def __init__(self, app=None, db=None): super().__init__() self._logger = logging.getLogger(__name__) self._app = app self._db = db self._backup_thread = QThread() self._backup_worker = None self._sound_player = SoundPlayer() self._validation_model = ValidationModel() self._initialize_validation_model() self._haul_level_validations = HaulLevelValidations(app=self._app, db=self._db) self._on_entry_validations = OnEntryValidations(app=self._app, db=self._db) def _initialize_validation_model(self): """ Method to initialize the validation model :return: """ self._validations = [] validation_template = { "name": "", "description": "", "type": "", "comments": "", "commentAdded": "", "status": "", "errors": "", "errorCount": None, "object": "", "method": "", "validationId": None } try: validations = ValidationsLu.select(ValidationsLu, TypesLu).join(TypesLu, on=( TypesLu.type_id == ValidationsLu.validation_type).alias( "types")) \ .where(TypesLu.category == "Validation", TypesLu.type == "Haul Level", ValidationsLu.is_active == "True") for validation in validations: new_validation = deepcopy(validation_template) new_validation["name"] = validation.name new_validation["description"] = validation.description new_validation["type"] = validation.types.type new_validation["object"] = validation.object new_validation["method"] = validation.method new_validation["validationId"] = validation.validation self._validations.append(new_validation) except Exception as ex: logging.error("validations table query failed: " + str(ex)) self._validation_model.setItems(self._validations) @pyqtProperty(QVariant) def haulLevelValidations(self): """ Return the self._haul_level_validations :return: """ return self._haul_level_validations @pyqtProperty(QVariant) def onEntryValidations(self): """ Return the self._on_entry_validations :return: """ return self._on_entry_validations def _backup_status_received(self, success, msg): """ Method to catch the pyqtSignal from the BackupFilesWorker that indicates if the backup was successful or not and a message about the backup :param success: bool - True = successful backup / False = unsuccesful backup :param msg: :return: """ self._backup_thread.quit() self.backupStatusChanged.emit(success, msg) @pyqtSlot() def backup_files(self): if self._backup_thread and self._backup_thread.isRunning(): # Show a dialog popup saying hold on, we're still backing up the database success = False msg = "Backing up trawl_backdeck.db\n\n" msg += "Backup is still in progress, please be patient" self.backupStatusChanged.emit(success, msg) else: self._backup_worker = BackupFilesWorker(app=self._app) self._backup_worker.moveToThread(self._backup_thread) self._backup_worker.backupStatus.connect( self._backup_status_received) self._backup_thread.started.connect(self._backup_worker.run) self._backup_thread.start() @pyqtProperty(QVariant, notify=validationModelChanged) def ValidationModel(self): """ return the ValidationModel :return: """ return self._validation_model @pyqtSlot(result=bool) def fishSamplingSameSexCheck(self): """ Method to check if fish sampling has at least 10 fish and they're all of the same sex :return: """ model = self._app.fish_sampling.model sexes = [x["sex"] for x in model.items] if model.count >= 10 and sexes.count(sexes[0]) == len(sexes): self._sound_player.play_sound(sound_name="error") return False return True @pyqtSlot(result=bool) def fishSamplingRepetitiveLengthsCheck(self, index): """ Method to check if fish sampling has 7 fish in a row with the same length values. If so, might be due to the fishmeter board buffer overflowing :param: index - int - current length that was added :return: bool - success - whether the test is passed or not """ model = self._app.fish_sampling.model if model.count >= 7 and index - 6 > 0: lengths = [x["linealValue"] for x in model.items[index - 6:index]] if lengths.count(lengths[0]) == len(lengths): self._sound_player.play_sound(sound_name="error") return False return True @pyqtSlot(QVariant, QVariant, result=bool) def fishSamplingLeaveAgeWeightTabCheck(self, weight, age): """ Method to check when in fish sampling and the user attempts to leave the Age-Weight tab. This checks to see if the weight or age value is blank. If one of them is blank, it fails :param weight: QVariant - should be a float, but could be None or an empty string :param age: QVariant - should be an int, but could be None or an empty string :return: """ # 2019 Patch - Tanner crab - do not do a age check - Taxonomy IDs: 1011, 1012, 1013, 1014 tanner_crabs = [1011, 1012, 1013, 1014] # logging.info(f"taxonomy_id = {self._app.state_machine.species['taxonomy_id']}") if self._app.state_machine.species["taxonomy_id"] not in tanner_crabs: if weight is None or weight == "" or age is None or age == "": return False return True @pyqtSlot(result=QVariant) def runHaulLevelValidations(self): """ Method called by the TrawlValidateDialog.qml to run Haul-level validations. This method in turn calls a series of individual haul-level validations methods :return: dict - dictionary of the results of the validations """ if self._app.state_machine.haul[ "haul_id"] is None or self._app.state_machine.haul[ "haul_id"] == "": return for validation in self._validations: try: object = getattr(self, validation["object"]) method = getattr(object, validation["method"]) result = method() for x in ["status", "errors", "errorCount"]: if x in result: validation[x] = result[x] except Exception as ex: logging.error('Haul Level Validation Error: {0}'.format(ex)) return self._validations
class SpecialActions(QObject): """ Class for the SpecialActionsScreen """ modelChanged = pyqtSignal() modelInitialized = pyqtSignal() specimenTypeChanged = pyqtSignal() parentSpecimenCountChanged = pyqtSignal() rowIndexChanged = pyqtSignal() rowWidgetTypeChanged = pyqtSignal() piProjectModelChanged = pyqtSignal() printerStatusReceived = pyqtSignal( str, bool, str, arguments=["comport", "success", "message"]) def __init__(self, app=None, db=None): super().__init__() self._logger = logging.getLogger(__name__) self._app = app self._db = db # Set up the models self._model = SpecialActionsModel() self._pi_project_model = PiProjectModel() self._sound_player = SoundPlayer() self._label_printer = LabelPrinter(app=self._app, db=self._db) self._label_printer.tagIdChanged.connect( self._updated_printer_tag_received) self._label_printer.printerStatusReceived.connect( self._printer_status_received) self._standardSurveySpecimen = None self._parent_specimen_count = 0 self._row_index = -1 self._row_widget_type = None def _printer_status_received(self, comport, success, message): """ Method to catch the message coming back from the printer :param comport: :param success: :param message: :return: """ self.printerStatusReceived.emit(comport, success, message) # logging.info('message received: ' + str(message)) def _updated_printer_tag_received(self, tag_id): """ Method used to catch the newly updated printer tag and then use that to derive the tvSamples rowIndex in question and then to an upsert on that row to save the item["value"] to the database :param str: :return: """ # logging.info('new tag_id: ' + str(tag_id)) # Update the model previous_tag_id = None if not tag_id[-1:].isdigit(): if tag_id[-1:] == "A": previous_tag_id = tag_id[:-1] else: char = chr(ord(tag_id[-1:]) - 1) previous_tag_id = str(tag_id[:-1]) + char # logging.info('previous_tag_id: ' + str(previous_tag_id)) index = self._model.get_item_index(rolename="value", value=previous_tag_id) if index != -1: self._model.setProperty(index=index, property="value", value=tag_id) self.upsert_specimen(row_index=index) # logging.info('index found and upserted complete, index: ' + str(index)) @pyqtSlot(str, int, str, str) def printLabel(self, comport, pi_id, action, specimen_number): """ Method called from QML to print a label. This passes a request to the self._label_printer object :return: """ self._label_printer.print_job(comport=comport, pi_id=pi_id, action=action, specimen_number=specimen_number) @pyqtProperty(QVariant, notify=modelChanged) def model(self): """ return the SpecimensModel :return: """ return self._model @pyqtProperty(QVariant, notify=piProjectModelChanged) def piProjectModel(self): return self._pi_project_model @pyqtProperty(str) def rowWidgetType(self): """ Method to return the widgetType of the currently selected tvSamples row. This is used to keep track of what type of automatic measurement the row can take in from scales, barcode reader, etc. :return: """ return self._row_widget_type @rowWidgetType.setter def rowWidgetType(self, value): """ Method to set the self._row_widget_type :param value: str - enumerated values include: id, measurement, coral, salmon, sex - same as the states in special actions :return: """ # if value not in ["id", "measurement", "coral", "salmon", "sex", "sponge", "maturityLevel", "yesno"]: # return self._row_widget_type = value self.rowWidgetTypeChanged.emit() @pyqtProperty(int) def rowIndex(self): """ Method to return the currently selected row of the tvSamples TableView :return: """ return self._row_index @rowIndex.setter def rowIndex(self, value): """ Method to set the self._row_index to keep track of the currently selected row in tvSamples This is needed when taking in a measurement from the barcode scanner or an automatic length / weight measurement :param value: :return: """ if not isinstance(value, int): return self._row_index = value self.rowIndexChanged.emit() @pyqtProperty(QVariant, notify=parentSpecimenCountChanged) def parentSpecimenCount(self): return self._parent_specimen_count @parentSpecimenCount.setter def parentSpecimenCount(self, value): if not isinstance(value, int): return self._parent_specimen_count = value self.parentSpecimenCountChanged.emit() @pyqtProperty(QVariant, notify=specimenTypeChanged) def standardSurveySpecimen(self): return self._standardSurveySpecimen @standardSurveySpecimen.setter def standardSurveySpecimen(self, value): if not isinstance(value, bool) and not isinstance(value, None): return self._standardSurveySpecimen = value self.specimenTypeChanged.emit() @pyqtSlot(int, result=str) def get_tag_id(self, row_index): """ Method to get a new tag ID :return: """ # mapping = {"ovaryNumber": {"type": "000", "action": "Ovary", "id": "ovarySpecimenId"}, # "stomachNumber": {"type": "001", "action": "Stomach", "id": "stomachSpecimenId"}, # "tissueNumber": {"type": "002", "action": "Tissue", "id": "tissueSpecimenId"}, # "finclipNumber": {"type": "003", "action": "Finclip", "id": "finclipSpecimenId"}} # if action not in mapping: # return if not isinstance(row_index, int) or row_index == -1: return item = self._model.get(row_index) pi_id = item["piId"] action_type_id = item["specialActionId"] value = item["value"] specimen_id = item["specimenId"] try: # Item 1 - Year / Item 2 - Vessel for setting in Settings.select(): if setting.parameter == "Survey Year": year = setting.value try: now_year = datetime.now().strftime("%Y") if year != now_year: year = now_year except Exception as ex2: year = datetime.now().strftime("%Y") logging.info(f"unable to update the year: {ex2}") logging.info(f"year = {year}") elif setting.parameter == "Vessel ID": vessel_id = setting.value # Item 3 - Haul ID haul_number = str(self._app.state_machine.haul["haul_number"]) if len(haul_number) > 3: haul_number = haul_number[-3:] # Item 4 - Specimen Type Code try: pi_action_code_id = \ PiActionCodesLu.select(PiActionCodesLu) \ .join(PrincipalInvestigatorLu, on=(PiActionCodesLu.principal_investigator == PrincipalInvestigatorLu.principal_investigator)) \ .join(TypesLu, on=(PiActionCodesLu.action_type == TypesLu.type_id).alias('types')) \ .where(PrincipalInvestigatorLu.principal_investigator == pi_id, TypesLu.type_id == action_type_id).get().pi_action_code except DoesNotExist as ex: pi_action_code_id = 999 # Give it a bogus entry # logging.info('pi action code: ' + str(pi_action_code_id)) specimen_type_id = str(pi_action_code_id).zfill(3) # Item 5 - Specimen Number # Query for specimen number - get the latest one for the given specimen type (i.e. ovary, stomach, tissue, finclip) spec_num_length = 20 # if pi_action_code_id != 999: # specimens = (Specimen.select(Specimen, TypesLu) # .join(SpeciesSamplingPlanLu, # on=(SpeciesSamplingPlanLu.species_sampling_plan==Specimen.species_sampling_plan).alias('plan')) # .join(PrincipalInvestigatorLu, # on=(SpeciesSamplingPlanLu.principal_investigator==PrincipalInvestigatorLu.principal_investigator).alias('pi')) # .join(TypesLu, on=(Specimen.action_type == TypesLu.type_id).alias('types')) # .where(TypesLu.type_id == action_type_id, # PrincipalInvestigatorLu.principal_investigator==pi_id, # fn.length(Specimen.alpha_value) == spec_num_length).order_by(Specimen.alpha_value.desc())) # specimens = (Specimen.select(fn.substr(Specimen.alpha_value, 18, 3).alias('specimen_number')) # .join(SpeciesSamplingPlanLu, # on=(SpeciesSamplingPlanLu.species_sampling_plan == Specimen.species_sampling_plan).alias('plan')) # .join(PrincipalInvestigatorLu, # on=(SpeciesSamplingPlanLu.principal_investigator == PrincipalInvestigatorLu.principal_investigator).alias('pi')) # .join(TypesLu, on=(Specimen.action_type == TypesLu.type_id).alias('types')) # .where(TypesLu.type_id == action_type_id, # PrincipalInvestigatorLu.principal_investigator == pi_id, # ((fn.length(Specimen.alpha_value) == spec_num_length) | # (fn.length(Specimen.alpha_value) == spec_num_length + 1)), # (fn.substr(Specimen.alpha_value, 1, 4) == year), # (fn.substr(Specimen.alpha_value, 6, 3) == vessel_id)).order_by( # fn.substr(Specimen.alpha_value, 18, 3).desc())) specimens = (Specimen.select( fn.substr(Specimen.alpha_value, 18, 3).alias('specimen_number') ).join( TypesLu, on=(Specimen.action_type == TypesLu.type_id).alias('types') ).where(TypesLu.type_id == action_type_id, ((fn.length(Specimen.alpha_value) == spec_num_length) | (fn.length(Specimen.alpha_value) == spec_num_length + 1)), (fn.substr(Specimen.alpha_value, 1, 4) == year), (fn.substr(Specimen.alpha_value, 6, 3) == vessel_id)).order_by( fn.substr(Specimen.alpha_value, 18, 3).desc())) # else: # # where_clause = (((fn.length(Specimen.alpha_value) == spec_num_length) | # (fn.length(Specimen.alpha_value) == spec_num_length + 1)) & # (Specimen.alpha_value.contains("-999-"))) # specimens = (Specimen.select(Specimen) # .join(SpeciesSamplingPlanLu, # on=(SpeciesSamplingPlanLu.species_sampling_plan == Specimen.species_sampling_plan).alias('plan')) # .join(PrincipalInvestigatorLu, # on=( # SpeciesSamplingPlanLu.principal_investigator == PrincipalInvestigatorLu.principal_investigator).alias( # 'pi')) # .where(where_clause).order_by(Specimen.alpha_value.desc())) # Get the newest specimen. Note that one may not exist as it hasn't been created yet try: last_specimen_num = specimens.get().specimen_number # last_specimen_num = specimens.get().alpha_value except DoesNotExist as dne: last_specimen_num = None logging.info('last specimen num: ' + str(last_specimen_num)) """ Use Cases 1. No existing SPECIMEN record exists for this specimen_type - insert a new one by one-upping the last number for this specimen_type 2. An existing SPECIMEN exists for this specimen_type - so a number should already be added, don't override then, correct? We should only give the next number up ever after having queried the specimen table for the last number for this specimen_type - which is what we have in last_specimen_num """ # logging.info('value: ' + str(value)) if specimen_id is None or specimen_id == "" or \ value is None or value == "" or len(value) < spec_num_length or value == "Error": # No specimen record exists for this specimen_type, so we're creating a new specimen_value # So one up the highest number and put an "a" at the end of it if last_specimen_num: specimen_num = str( int(re.sub(r'[^\d.]+', '', last_specimen_num)[-3:]) + 1).zfill(3) else: specimen_num = "001" else: # Specimen record exists, then nothing to do here. Clicking the print button will up the last # alpha character return item["value"] sep = "-" tag_id = year + sep + vessel_id + sep + haul_number + sep + specimen_type_id + \ sep + specimen_num # One final confirmation that this tag_id does not already exist in the database dup_count = Specimen.select().where( Specimen.alpha_value.contains(tag_id)).count() if dup_count > 0: logging.error("duplicate tag found: {0}, count: {1}".format( tag_id, dup_count)) return "" except Exception as ex: logging.info('get_tag_id error: ' + str(ex)) tag_id = "Error" # logging.info('tag_id: ' + str(tag_id)) return tag_id def _get_widget_type(self, display_name): """ Method to return the type of the widget with a given display name. This drives which UI widgets are displayed on the right side of SpecialActionsScreen.qml :param display_name: str - text of the specialAction role that is displayed in the tvSamples TableView :return: """ if not isinstance(display_name, str): return display_name = display_name.lower() widget_type = "id" if "sex" in display_name: widget_type = "sex" elif "id" in display_name: widget_type = "id" elif "length" in display_name or \ "width" in display_name or \ "weight" in display_name: widget_type = "measurement" taxon_id = self._app.state_machine.species["taxonomy_id"] if self._app.process_catch.checkSpeciesType("salmon", taxonId=taxon_id): widget_type = "salmon" elif self._app.process_catch.checkSpeciesType("coral", taxonId=taxon_id): widget_type = "coral" elif self._app.process_catch.checkSpeciesType("sponge", taxonId=taxon_id): widget_type = "sponge" return widget_type def _get_value_type(self, value): """ Method to convert the value to an appropriate type given the property :param value: :return: """ try: value = float(value) return "numeric" except ValueError as ex: try: value = int(value) return "numeric" except ValueError as ex_int: pass return "alpha" @pyqtSlot() def initialize_pi_project_list(self): """ Method to initialize the tvProjects list in the dlgSpecimen when Add Specimen is clicked :return: """ self._pi_project_model.clear() taxon_id = self._app.state_machine.species["taxonomy_id"] plans = SpeciesSamplingPlanLu.select(SpeciesSamplingPlanLu, PrincipalInvestigatorLu) \ .join(PrincipalInvestigatorLu, on=(SpeciesSamplingPlanLu.principal_investigator==PrincipalInvestigatorLu.principal_investigator).alias('pi')) \ .where((SpeciesSamplingPlanLu.parent_species_sampling_plan.is_null(True)) & \ ( ( (SpeciesSamplingPlanLu.plan_name == "FRAM Standard Survey") & ( ( ((SpeciesSamplingPlanLu.display_name == "Salmon") | (SpeciesSamplingPlanLu.display_name == "Coral") | (SpeciesSamplingPlanLu.display_name == "Sponge")) & (SpeciesSamplingPlanLu.taxonomy == taxon_id) ) | (SpeciesSamplingPlanLu.display_name == "Whole Specimen ID")) ) | ((SpeciesSamplingPlanLu.taxonomy == taxon_id) & (SpeciesSamplingPlanLu.plan_name != "FRAM Standard Survey") ) ) ) \ .order_by(PrincipalInvestigatorLu.last_name) for plan in plans: is_coral = self._app.process_catch.checkSpeciesType( "coral", taxon_id) if is_coral: if plan.display_name == "Whole Specimen ID": continue is_sponge = self._app.process_catch.checkSpeciesType( "sponge", taxon_id) if is_sponge: if plan.display_name == "Whole Specimen ID": continue is_salmon = self._app.process_catch.checkSpeciesType( "salmon", taxon_id) if is_salmon: pass plan_name = plan.plan_name if plan_name == "FRAM Standard Survey": plan_name = plan.display_name item = { "piId": plan.pi.principal_investigator, "principalInvestigator": plan.pi.last_name, "planId": plan.species_sampling_plan, "planName": plan_name } self._pi_project_model.appendItem(item) self.modelInitialized.emit() def _create_list_template(self): """ Method used to create the tvSpecimens list items that are applicable to this given species/taxonomy id This is used when initializing the list from either the Process Catch or the Fish Sampling Screen :return: """ templates = [] # Create a blank templates (i.e. no values) tableview items from existing protocols, but data is not populated taxon_id = self._app.state_machine.species["taxonomy_id"] plans = self._app.protocol_viewer.get_special_actions( taxon_id=taxon_id) parent_plans = [x for x in plans if x["parentPlan"] is None] for parent_plan in parent_plans: # self.parentSpecimenCount += 1 # Have a protocol at the very top species sampling plan record, add it's actions to the list if parent_plan["topProtocol"] is not None: for action in parent_plan["actions"]: is_coral = self._app.process_catch.checkSpeciesType( "coral", taxon_id) is_sponge = self._app.process_catch.checkSpeciesType( "sponge", taxon_id) if is_coral: if action["displayName"] == "Whole Specimen ID": # Don't include the Whole Specimen ID as an option for corals, as that is already included # self.parentSpecimenCount -= 1 continue specialAction = "Coral " + action["displayName"] elif is_sponge: if action["displayName"] == "Whole Specimen ID": continue specialAction = "Sponge " + action["displayName"] else: specialAction = action["displayName"] item = { "parentSpecimenNumber": None, # self.parentSpecimenCount, "parentSpecimenId": None, "specimenId": None, "specialActionId": action["actionTypeId"], "principalInvestigator": parent_plan["pi"], "piId": parent_plan["piId"], "specialAction": specialAction, "widgetType": action["widgetType"], "planId": parent_plan["plan"], "value": None } templates.append(item) # self._model.appendItem(item) # Get all of the children species sampling plans and add their actions child_plans = [ x for x in plans if x["parentPlan"] == parent_plan["plan"] ] for child_plan in child_plans: for action in child_plan["actions"]: is_coral = self._app.process_catch.checkSpeciesType( "coral", taxon_id) is_sponge = self._app.process_catch.checkSpeciesType( "sponge", taxon_id) if is_coral: if action["displayName"] == "Whole Specimen ID": continue specialAction = "Coral " + action["displayName"] elif is_sponge: if action["displayName"] == "Whole Specimen ID": continue specialAction = "Sponge " + action["displayName"] else: specialAction = action["displayName"] item = { "parentSpecimenNumber": None, # self.parentSpecimenCount, "parentSpecimenId": None, "specimenId": None, "specialActionId": action["actionTypeId"], "principalInvestigator": parent_plan["pi"], "piId": parent_plan["piId"], "specialAction": specialAction, "widgetType": action["widgetType"], "planId": parent_plan["plan"], "value": None } templates.append(item) # self._model.appendItem(item) return templates @pyqtSlot() def initialize_fish_sampling_list(self): """ Method to initialize the tvSamples list when the screen is called from the FishSamplingScreen.qml screen. Query the database to retrieve existing specimens that have already been collected for this taxonomy_id for this given haul > could be at the ProcessCatch or the FishSampling level. Will need to treat them differently if they come from ProcessCatchScreen v. FishSamplingScreen :return: """ self._model.clear() templates = self._create_list_template() try: where_clause = (Specimen.specimen == self._app.state_machine. specimen["parentSpecimenId"]) parent = (Specimen.select( Specimen, SpeciesSamplingPlanLu, PrincipalInvestigatorLu, TypesLu).join( SpeciesSamplingPlanLu, on=(Specimen.species_sampling_plan == SpeciesSamplingPlanLu .species_sampling_plan).alias("plan")).join( PrincipalInvestigatorLu, on=(SpeciesSamplingPlanLu.principal_investigator == PrincipalInvestigatorLu.principal_investigator ).alias("pi")).join( TypesLu, JOIN.LEFT_OUTER, on=(Specimen.action_type == TypesLu.type_id ).alias("types")).where(where_clause) ).get() self.parentSpecimenCount = self._app.state_machine.specimen[ "specimenNumber"] for template in templates: # Add the current template to the model current_item = deepcopy(template) current_item["parentSpecimenNumber"] = self.parentSpecimenCount current_item[ "parentSpecimenId"] = self._app.state_machine.specimen[ "parentSpecimenId"] self._model.appendItem(current_item) index = self._model.count - 1 # Get the existing child from the database try: child = Specimen.select(Specimen, TypesLu) \ .join(TypesLu, on=(Specimen.action_type == TypesLu.type_id).alias("types")) \ .where((Specimen.parent_specimen == parent.specimen) & (Specimen.species_sampling_plan == current_item["planId"]) & (Specimen.action_type == current_item["specialActionId"])).get() # Update the value + specimenId, assuming that a record exists in the database (i.e. child exists) value = None # TODO - Todd Hay - might want to change the below logic when taking IDs # For instance, what if we have both a printed ID label, which has alpha # characters and then someone tries to take a barcode. This would # continue to show the printed alpha tag ID and new show the barcode if child.alpha_value is not None: value = child.alpha_value elif child.numeric_value is not None: value = child.numeric_value if index is not None: self._model.setProperty(index=index, property="specimenId", value=child.specimen) self._model.setProperty(index=index, property="value", value=value) except DoesNotExist as ex: # Could not find a child record in the database, skip updating the model specimenId + value fields pass except Exception as ex: logging.info('other exception: ' + str(ex)) pass except DoesNotExist as ex: logging.error("record does not exist: " + str(ex)) except Exception as ex: logging.error("General exception: " + str(ex)) @pyqtSlot() def initialize_process_catch_list(self): """ Method to initialize the tvSamples list when the screen is called from the ProcessCatchScreen.qml. This will list out all of the special actions for a given specimen :return: """ self._model.clear() self.parentSpecimenCount = 0 templates = self._create_list_template() """ Query the database to retrieve existing specimens that have already been collected for this taxonomy_id for this given haul > could be at the ProcessCatch or the FishSampling level. Will need to treat them differently if they come from ProcessCatchScreen v. FishSamplingScreen as follows: - ProcessCatchScreen.qml - - FishSamplingScreen.qml - """ # Get All of the Parents first try: if self._app.state_machine.previousScreen == "processcatch": where_clause = ( (Specimen.catch == self._app.state_machine.species["catch_id"]) & (Specimen.parent_specimen.is_null(True)) & ((SpeciesSamplingPlanLu.plan_name != "FRAM Standard Survey") | ((SpeciesSamplingPlanLu.plan_name == "FRAM Standard Survey") & ((SpeciesSamplingPlanLu.display_name == "Whole Specimen ID") | (SpeciesSamplingPlanLu.display_name == "Coral") | (SpeciesSamplingPlanLu.display_name == "Salmon") | (SpeciesSamplingPlanLu.display_name == "Sponge"))))) elif self._app.state_machine.previousScreen == "fishsampling": where_clause = ( (Specimen.parent_specimen == self._app.state_machine.specimen["parentSpecimenId"]) & ((SpeciesSamplingPlanLu.plan_name != "FRAM Standard Survey") | ((SpeciesSamplingPlanLu.plan_name == "FRAM Standard Survey") & (SpeciesSamplingPlanLu.display_name == "Whole Specimen ID")))) parents = (Specimen.select( Specimen, SpeciesSamplingPlanLu, PrincipalInvestigatorLu, TypesLu).join( SpeciesSamplingPlanLu, on=(Specimen.species_sampling_plan == SpeciesSamplingPlanLu .species_sampling_plan).alias("plan")).join( PrincipalInvestigatorLu, on=(SpeciesSamplingPlanLu.principal_investigator == PrincipalInvestigatorLu.principal_investigator ).alias("pi")).join( TypesLu, JOIN.LEFT_OUTER, on=(Specimen.action_type == TypesLu.type_id ).alias("types")).where(where_clause)) current_parent_specimen_id = -1 for parent in parents: # Get all of the special actions that match this PI + Plan template = [ x for x in templates if x["piId"] == parent.plan.pi.principal_investigator and x["planId"] == parent.plan.species_sampling_plan ] # logging.info('template: ' + str(template)) if current_parent_specimen_id != parent.specimen: if self._app.state_machine.previousScreen == "processcatch": self.parentSpecimenCount += 1 elif self._app.state_machine.previousScreen == "fishsampling": self.parentSpecimenCount = self._app.state_machine.specimen[ "specimenNumber"] # Add each of the items in the current template to the model. Later we add in the actual values for item in template: current_item = deepcopy(item) current_item[ "parentSpecimenNumber"] = self.parentSpecimenCount if self._app.state_machine.previousScreen == "processcatch": current_item["parentSpecimenId"] = parent.specimen elif self._app.state_machine.previousScreen == "fishsampling": current_item[ "parentSpecimenId"] = self._app.state_machine.specimen[ "parentSpecimenId"] self._model.appendItem(current_item) # Iterate through all of the specimen children children = Specimen.select(Specimen, TypesLu) \ .join(TypesLu, on=(Specimen.action_type == TypesLu.type_id).alias("types")) \ .where(Specimen.parent_specimen == parent.specimen) for child in children: if child.types.subtype is not None and child.types.subtype != "": specialAction = child.types.subtype + " " + child.types.type else: specialAction = child.types.type # Coral - need to prepend specialAction with Coral as appropriate, otherwise # extra model rows are added. This is a bad hack. It deals with the fact that for our actions, we have # nothing specific to corals, yet we display the term Coral in the Special Actions table, where in actions # we do have the 3 specific Salmon actions...I don't like this difference at all. # 05/11/2018 - added in the same issue for Sponge as for coral, as the survey team members want to start # treating sponges similarly # taxon_id = parent.plan.taxonomy_id taxon_id = parent.plan.taxonomy.taxonomy if self._app.process_catch.checkSpeciesType( "coral", taxonId=taxon_id): specialAction = "Coral " + specialAction elif self._app.process_catch.checkSpeciesType( "sponge", taxonId=taxon_id): specialAction = "Sponge " + specialAction # Get the proper value, i.e. alpha or numeric value value = None # TODO - Todd Hay - might want to change the below logic when taking IDs # For instance, what if we have both a printed ID label, which has alpha # characters and then someone tries to take a barcode. This would # continue to show the printed alpha tag ID and new show the barcode if child.alpha_value is not None: value = child.alpha_value elif child.numeric_value is not None: value = child.numeric_value """ Need to update 2 values in the item. First need to find the exact item. - specimenId - value """ index = [ i for i, x in enumerate(self._model.items) if x["piId"] == parent.plan.pi.principal_investigator and x["planId"] == parent.plan.species_sampling_plan and x["specialActionId"] == child.types.type_id and x["parentSpecimenNumber"] == self.parentSpecimenCount ] if index is not None: index = index[0] self._model.setProperty(index=index, property="specimenId", value=child.specimen) self._model.setProperty(index=index, property="value", value=value) current_parent_specimen_id = parent.specimen except DoesNotExist as ex: logging.error("record does not exist: " + str(ex)) except Exception as ex: logging.error("General exception: " + str(ex)) # logging.info('model count: ' + str(self._model.count)) # logging.info('model items: ' + str(self._model.items)) # Add in the new template items at the bottom of the list if self._app.state_machine.previousScreen == "processcatch" or \ (self._app.state_machine.previousScreen == "fishsampling" and self._model.count == 0): current_pi_id = -1 current_plan_id = -1 for template in templates: if template["piId"] != current_pi_id or template[ "planId"] != current_plan_id: if self._app.state_machine.previousScreen == "processcatch": self.parentSpecimenCount += 1 elif self._app.state_machine.previousScreen == "fishsampling": self.parentSpecimenCount = self._app.state_machine.specimen[ "specimenNumber"] template[ "parentSpecimenId"] = self._app.state_machine.specimen[ "parentSpecimenId"] # self.parentSpecimenCount += 1 current_pi_id = template["piId"] current_plan_id = template["planId"] template["parentSpecimenNumber"] = self.parentSpecimenCount self._model.appendItem(template) self.modelInitialized.emit() @pyqtSlot(int, int, int) def add_model_item(self, pi_id, plan_id, count): """ :return: """ items = [ x for x in self._model.items if x["piId"] == pi_id and x["planId"] == plan_id ] for i in range(count): self.parentSpecimenCount += 1 parent_specimen_number = -1 for item in items: new_item = deepcopy(item) if parent_specimen_number == -1: parent_specimen_number = new_item["parentSpecimenNumber"] if new_item["parentSpecimenNumber"] == parent_specimen_number: new_item["parentSpecimenNumber"] = self.parentSpecimenCount new_item["specimenId"] = None new_item["parentSpecimenId"] = None new_item["value"] = None self.model.appendItem(new_item) else: break @pyqtSlot(int, result=bool) def if_exist_otolith_id(self, otolith_id): specimen = Specimen.select().where( Specimen.numeric_value == otolith_id) if specimen.count() > 0: return True else: return False @pyqtSlot(int) def upsert_specimen(self, row_index): """ Method to perform an insert or replace of a given specimen, if it exists :paremt row_index: int - index of the row being updated in tvSamples :param specimen_id: :return: """ if not isinstance(row_index, int) or row_index == -1: return logging.info("upserting row: {0}".format(row_index)) try: if isinstance(row_index, QVariant) or isinstance( row_index, QJSValue): row_index = row_index.toVariant() item = self._model.get(row_index) value = item["value"] value_type = self._get_value_type(value=value) special_action = item["specialAction"] specimen_id = item["specimenId"] logging.info('specimen_id: ' + str(specimen_id) + ', row_index: ' + str(row_index) + ', item: ' + str(item)) if specimen_id is None: # Inserting a new record # logging.info('inserting a record') # Check if a parent record exists in a neighbor specimen, i.e. a specimen with the same parentSpecimenNumber parentSpecimenId = -1 parentSpecimenNumber = item["parentSpecimenNumber"] sibling_specimens = [ x for x in self._model.items if x["parentSpecimenNumber"] == parentSpecimenNumber ] for sibling in sibling_specimens: if sibling["parentSpecimenId"] is not None and sibling[ "parentSpecimenId"] != "": parentSpecimenId = sibling["parentSpecimenId"] break species_sampling_plan = item["planId"] if parentSpecimenId == -1: logging.info('no parent found, inserting one...') try: q = Specimen.insert( catch=self._app.state_machine.species["catch_id"], species_sampling_plan=species_sampling_plan) q.execute() parentSpecimenId = Specimen.select().order_by( Specimen.specimen.desc()).get().specimen except DoesNotExist as ex: logging.error('error inserting the parent: ' + str(ex)) # Use INSERT OR REPLACE statement, peewee upsert statement if value_type == "numeric": q = Specimen.insert( parent_specimen=parentSpecimenId, catch=self._app.state_machine.species["catch_id"], species_sampling_plan=species_sampling_plan, action_type=item["specialActionId"], numeric_value=item["value"]) elif value_type == "alpha": q = Specimen.insert( parent_specimen=parentSpecimenId, catch=self._app.state_machine.species["catch_id"], species_sampling_plan=species_sampling_plan, action_type=item["specialActionId"], alpha_value=item["value"]) q.execute() new_specimen_id = Specimen.select().order_by( Specimen.specimen.desc()).get().specimen # Update the model with the new parentSpecimenId and specimenId as appropriate from the database self._model.setProperty(index=row_index, property="parentSpecimenId", value=parentSpecimenId) self._model.setProperty(index=row_index, property="specimenId", value=new_specimen_id) new_item = self._model.get(row_index) logging.info('inserted a record, new model item: ' + str(new_item)) else: # Doing an update to an existing specimen record if value_type == "numeric": q = Specimen.update(numeric_value=value, alpha_value=None).where( Specimen.specimen == specimen_id) elif value_type == "alpha": q = Specimen.update(alpha_value=value, numeric_value=None).where( Specimen.specimen == specimen_id) q.execute() # TODO Todd Hay - Move all of the sounds to the SerialPortManager.py > data_received method # as we should play a sound once a serial port feed is received # Play the appropriate sound if item["specialAction"].lower() in [ "is sex length sample", "is age weight sample" ]: return if "coral specimen id" in item["specialAction"].lower(): self._sound_player.play_sound(sound_name="takeBarcode") elif "sponge specimen id" in item["specialAction"].lower(): self._sound_player.play_sound(sound_name="takeBarcode") elif "otolith age id" in item["specialAction"].lower(): self._sound_player.play_sound(sound_name="takeBarcode") elif "tissue id" in item["specialAction"].lower() and \ "sudmant" in item["principalInvestigator"].lower(): self._sound_player.play_sound(sound_name="takeSudmantBarcode") elif "length" in item["specialAction"].lower(): self._sound_player.play_sound(sound_name="takeLength") elif "width" in item["specialAction"].lower(): self._sound_player.play_sound(sound_name="takeWidth") elif "weight" in item["specialAction"].lower(): self._sound_player.play_sound(sound_name="takeWeight") except Exception as ex: logging.error( "Error updating the special project information: {0}".format( ex)) @pyqtSlot(int) def delete_specimen(self, specimen_id): """ Method to perform an insert or replace of a given specimen, if it exists :param specimen_id: :return: """ if not isinstance(specimen_id, int): return # This should be an individual instance of a specimen, i.e. not a whole fish try: logging.info( 'deleting a record, specimen_id: {0}'.format(specimen_id)) # Delete from the database specimen = Specimen.select().where( Specimen.specimen == specimen_id).get() parent_specimen_id = specimen.parent_specimen.specimen specimen.delete_instance(recursive=True, delete_nullable=True) # Remove the specimenId from the model index = self._model.get_item_index(rolename="specimenId", value=specimen_id) if index != -1: self._model.setProperty(index=index, property="specimenId", value=None) parent_specimen_number = self._model.get( index)["parentSpecimenNumber"] # Delete the parent specimen if no children exist anymore count = Specimen.select().where( Specimen.parent_specimen == parent_specimen_id).count() if count == 0: specimen = Specimen.select().where( Specimen.specimen == parent_specimen_id).get() specimen.delete_instance(recursive=True, delete_nullable=True) # Empty the parentSpecimenNumber from the model if index != -1: sibling_specimens = [ x for x in self._model.items if x["parentSpecimenNumber"] == parent_specimen_number ] for sibling in sibling_specimens: sibling["parentSpecimenId"] = None except DoesNotExist as ex: logging.info('Error deleting specimen: ' + str(ex))
class Drops(QObject): personnelModelChanged = pyqtSignal() selection_result_obtained = pyqtSignal(QVariant, name="selectionResultsObtained", arguments=[ "results", ]) operationAttributeDeleted = pyqtSignal() new_drop_added = pyqtSignal(QVariant, name="newDropAdded", arguments=[ "dropJson", ]) exception_encountered = pyqtSignal(str, str, name="exceptionEncountered", arguments=["message", "action"]) def __init__(self, app=None, db=None): super().__init__() self._app = app self._db = db self._rpc = self._app.rpc self._personnel_model = PersonnelModel(app=self._app) self._sound_player = SoundPlayer(app=self._app) @pyqtProperty(FramListModel, notify=personnelModelChanged) def personnelModel(self): """ Method to return the self._personnel_model :return: """ return self._personnel_model @pyqtSlot(int, result=QVariant, name="insertOperations") def insert_operations(self, drop_number: int) -> dict: """ Method to insert a new drop and angler operations. This is called by the DropAngler when the Start time button is clicked. Remember that clicking one of the start time buttons activates the times for all three anglers, so this will involved inserting a drop operation as well as three children operations, one for each angler This method will return the Drop ID and Angler IDs if they already exist. :param drop_number: Integer of the drop number :param start_time: string containing the starting time for the drop :return: Operation IDs - dictionary containing the drop, angler A, angler B, and angler C operation IDs """ results = [] logging.info(f"insert_operations: drop_number={str(int(drop_number))}") try: # Get the DROP_TYPE_LU_ID and ANGLER_TYPE_LU_ID from LOOKUPS table sql = "SELECT LOOKUP_ID, VALUE FROM LOOKUPS WHERE Type = 'Operation' AND (VALUE = 'Drop' OR VALUE = 'Angler');" lu_ids = self._rpc.execute_query(sql=sql) if lu_ids: drop_type_lu_id = [x[0] for x in lu_ids if x[1] == "Drop"][0] angler_type_lu_id = [x[0] for x in lu_ids if x[1] == "Angler"][0] # Check if the drop operation already exists in the OPERATIONS table # Handled via UNIQUE constraint on the OPERATIONS table # Insert the Drop Operation sql = "INSERT INTO OPERATIONS(PARENT_OPERATION_ID, OPERATION_NUMBER, OPERATION_TYPE_LU_ID) VALUES(?, ?, ?);" params = [ self._app.state_machine.siteOpId, drop_number, drop_type_lu_id ] drop_op_id = self._rpc.execute_query_get_id(sql=sql, params=params) results = {"Drop " + str(int(drop_number)): drop_op_id} except Exception as ex: if "apsw.ConstraintError" in str(ex): logging.info( f"Drop operation exists, getting the existing primary key: {ex}" ) sql = """ SELECT OPERATION_ID FROM OPERATIONS WHERE PARENT_OPERATION_ID = ? AND OPERATION_NUMBER = ? AND OPERATION_TYPE_LU_ID = ?; """ drop_op_id = self._rpc.execute_query(sql=sql, params=params) if drop_op_id: drop_op_id = drop_op_id[0][0] results = {"Drop " + str(int(drop_number)): drop_op_id} else: logging.error(f"Error reached: {ex}") self._app.state_machine.drop = None self._app.state_machine.dropOpId = None self._app.state_machine.anglerAOpId = None self._app.state_machine.anglerBOpId = None self._app.state_machine.anglerCOpId = None message = f"Failed to insert or select drop {drop_number}" action = f"Please try again" self.exception_encountered.emit(message, action) return self._app.state_machine.dropOpId = drop_op_id # Check if the angler operations already exist in the OPERATIONS table # Handled via UNIQUE constraint on the OPERATIONS table between PARENT_OPERATION_ID, OPERATION_NUMBER, OPERATION_TYPE_LU_ID # Insert the three Angler Operations for x in ["A", "B", "C"]: try: sql = "INSERT INTO OPERATIONS(PARENT_OPERATION_ID, OPERATION_NUMBER, OPERATION_TYPE_LU_ID) VALUES(?, ?, ?);" params = [drop_op_id, x, angler_type_lu_id] angler_op_id = self._rpc.execute_query_get_id(sql=sql, params=params) results["Angler " + x] = angler_op_id except Exception as ex: if "apsw.ConstraintError" in str(ex): logging.info( f"Angler operation {x} exists, getting the existing primary key: {ex}" ) sql = """ SELECT OPERATION_ID FROM OPERATIONS WHERE PARENT_OPERATION_ID = ? AND OPERATION_NUMBER = ? AND OPERATION_TYPE_LU_ID = ?; """ angler_op_id = self._rpc.execute_query(sql=sql, params=params) if angler_op_id: angler_op_id = angler_op_id[0][0] results["Angler " + x] = angler_op_id else: logging.error(f"Error reached: {ex}") self._app.state_machine.anglerAOpId = None self._app.state_machine.anglerBOpId = None self._app.state_machine.anglerCOpId = None message = f"Failed to insert or select angler {x}" action = f"Please try again" self.exception_encountered.emit(message, action) return if x == "A": self._app.state_machine.anglerAOpId = angler_op_id elif x == "B": self._app.state_machine.anglerBOpId = angler_op_id elif x == "C": self._app.state_machine.anglerCOpId = angler_op_id # logging.info(f"newly inserted operations: {results}") return results @pyqtSlot(QVariant, str, str, str, str, QVariant, name="upsertOperationAttribute") def upsert_operation_attribute(self, operation_id: QVariant, lu_type: str, lu_value: str, value_type: str, value: str, indicator: QVariant = None): """ Method to insert or update an operational attribute :param operation_id: the operation_id as an integer as found in OPERATIONS table :param lu_type: lookup type (from the LOOKUPS table) :param lu_value: lookup value (from the LOOKUPS table) :param value_type: alpha or numeric :param value: new value to insert or update :param indicator: string used to send in additional information about the attribute being upserted. In particular, this is used when a angler person is sent in when the start time has not been captured. This creates new Drop + Angler operation IDs. In this case, the input operation_id is None, so we need to know what to assign to operation_id, i.e. which angler or drop for that matter :return: """ try: # If operation_id is NONE, that means that the operation has not been insert ye # that is a problem logging.info( f"upsertOperationalAttribute, inputs: operation_id={operation_id}, lu_type={lu_type}, " f"lu_value={lu_value}, value_type={value_type}, value={value}, indicator={indicator}" ) # Get the LOOKUP_ID for the OPERATION_ATTRIBUTE type sql = "SELECT LOOKUP_ID FROM LOOKUPS WHERE TYPE = ? AND VALUE = ?" params = [lu_type, lu_value] att_type_lu_id = self._rpc.execute_query(sql=sql, params=params) if att_type_lu_id: att_type_lu_id = att_type_lu_id[0][0] # operation_id is not an int, need to insert a new operation. This occurs if not isinstance(operation_id, int): # Insert the new Drop + Angler Operation IDs. Note that calling this method # also updates the state_machine with the new Drop + Angler operation IDs logging.info( f"operation_id={operation_id} > drop={self._app.state_machine.drop}" ) new_ops = self.insert_operations( drop_number=self._app.state_machine.drop) logging.info( f"New drop/angler operations inserted, new_operations={new_ops}" ) # Emit the drop_dict to update the siteResults JSON back in the DropsScreen.qml drop_dict = dict() drop_key = f"Drop {self._app.state_machine.drop}" drop_dict[drop_key] = { "id": new_ops[drop_key], "Anglers": { "Angler A": { "id": new_ops["Angler A"] }, "Angler B": { "id": new_ops["Angler B"] }, "Angler C": { "id": new_ops["Angler C"] } } } self.new_drop_added.emit(drop_dict) # Set the operation_id to the newly created ID, based on the provided indicator, where # indicator = Angler A, Angler B, Angler C, or Drop # if indicator in new_ops: operation_id = new_ops[indicator] # Check if the OPERATION_ATTRIBUTE type already exists sql = "SELECT * FROM OPERATION_ATTRIBUTES WHERE OPERATION_ID = ? AND ATTRIBUTE_TYPE_LU_ID = ?;" params = [operation_id, att_type_lu_id] results = self._rpc.execute_query(sql=sql, params=params) value_type = "ATTRIBUTE_ALPHA" if value_type == "alpha" else "ATTRIBUTE_NUMERIC" if value_type == "ATTRIBUTE_NUMERIC": value = float(value) if results: # Update the OPERATION_ATTRIBUTES table logging.info(f'update oa') sql = "UPDATE OPERATION_ATTRIBUTES SET " + value_type + " = ? WHERE " + \ "OPERATION_ID = ? AND ATTRIBUTE_TYPE_LU_ID = ?;" params = [value, operation_id, att_type_lu_id] else: # Insert into OPERATION_ATTRIBUTES Table logging.info(f'insert into oa') sql = "INSERT INTO OPERATION_ATTRIBUTES(OPERATION_ID, " + value_type + \ ", ATTRIBUTE_TYPE_LU_ID) VALUES(?, ?, ?);" params = [operation_id, value, att_type_lu_id] logging.info(f"sql={sql}\nparams={params}") results = self._rpc.execute_query(sql=sql, params=params) except Exception as ex: logging.error(f"Error upserting an operation attribute: {ex}") @pyqtSlot(str, name="selectOperationAttributes") def select_operation_attributes(self, set_id: str): """ Method to retrieve all of the OPERATION_ATTRIBUTES entries for all of the drops and anglers for the given set_id. This will need to recurse through each of the OPERATIONS set_id children and then get all of the associated OPERATION_ATTRIBUTES entries for those children :param set_id: str :return: """ sql = """ WITH RECURSIVE children(n) AS ( SELECT OPERATION_ID FROM OPERATIONS WHERE OPERATION_NUMBER = ? UNION SELECT o.OPERATION_ID FROM OPERATIONS o, children WHERE o.PARENT_OPERATION_ID = children.n ) SELECT l.VALUE, OPERATION_ID, PARENT_OPERATION_ID, OPERATION_NUMBER FROM OPERATIONS o INNER JOIN LOOKUPS l ON l.lookup_id = o.OPERATION_TYPE_LU_ID WHERE OPERATION_ID IN children AND l.TYPE = 'Operation' """ # AND l.VALUE IN ['Drop', 'Angler'] params = [ set_id, ] try: ids = [] results = self._rpc.execute_query(sql=sql, params=params) logging.info(f"results: {results}") if results: site = [x for x in results if x[0] == "Site"] drops = { "Drop " + x[3]: { "id": x[1], "Sinker Weight": None, "Recorder Name": None } for x in results if x[0] == "Drop" } ids = [] for k, v in drops.items(): ids.append(v["id"]) ids.extend([x[1] for x in results if x[2] == v['id']]) anglers = { "Angler " + x[3]: { "id": x[1] } for x in results if x[2] == v["id"] } v["Anglers"] = anglers # Query to get all OPERATION_ATTRIBUTES that match the ids (i.e. for all drops + anglers for this given site sql = """ SELECT o.PARENT_OPERATION_ID, oa.OPERATION_ID, l2.VALUE || " " || o.OPERATION_NUMBER, oa.ATTRIBUTE_ALPHA, l.TYPE, l.VALUE, oa.ATTRIBUTE_NUMERIC FROM OPERATION_ATTRIBUTES oa INNER JOIN LOOKUPS l ON oa.ATTRIBUTE_TYPE_LU_ID = l.LOOKUP_ID INNER JOIN OPERATIONS o ON o.OPERATION_ID = oa.OPERATION_ID INNER JOIN LOOKUPS l2 ON o.OPERATION_TYPE_LU_ID = l2.LOOKUP_ID WHERE oa.OPERATION_ID IN """ sql += " " + str(tuple(ids)) oa_results = self._rpc.execute_query(sql=sql) if oa_results: for op_att in oa_results: drop_angler_item_list = [ v["Anglers"] for k, v in drops.items() if v["id"] == op_att[0] ] # Drop Angler Items if len(drop_angler_item_list) > 0: drop_angler_item = drop_angler_item_list[0] angler_item = { k: v for k, v in drop_angler_item.items() if v["id"] == op_att[1] } key = op_att[4] + " " + op_att[5] angler_item[list( angler_item.keys())[0]][key] = op_att[3] # Drop Attributes elif "Drop Attribute" in op_att[4]: drop_item_list = [ v for k, v in drops.items() if v["id"] == op_att[1] ] if len(drop_item_list) == 1: item = drop_item_list[0] if "Sinker Weight" in op_att[5]: item["Sinker Weight"] = op_att[6] elif "Recorder Name" in op_att[5]: item["Recorder Name"] = op_att[3] self.selection_result_obtained.emit(drops) logging.info(f"drops: {drops}") if "Drop 1" in drops: self._app.state_machine.dropOpId = drops["Drop 1"]["id"] self._app.state_machine.anglerAOpId = drops["Drop 1"][ "Anglers"]["Angler A"]["id"] self._app.state_machine.anglerBOpId = drops["Drop 1"][ "Anglers"]["Angler B"]["id"] self._app.state_machine.anglerCOpId = drops["Drop 1"][ "Anglers"]["Angler C"]["id"] logging.info(f"inside Drop 1, after setting state machine") except Exception as ex: logging.error(f"Exception querying operation_attributes: {ex}") @pyqtSlot(int, str, str, name="deleteOperationAttribute") def delete_operation_attribute(self, op_id: int, lu_type: str, lu_value: str): """ Method to delete an individual operation_attributes record :param set_id: :return: """ try: # Delete the record sql = """ DELETE FROM OPERATION_ATTRIBUTES WHERE OPERATION_ID = ? AND ATTRIBUTE_TYPE_LU_ID = (SELECT LOOKUP_ID FROM LOOKUPS WHERE TYPE = ? AND VALUE = ?); """ params = [op_id, lu_type, lu_value] logging.info(f"Deleting parameters: {params}") self._rpc.execute_query(sql=sql, params=params) # Return dropTimeState to enter mode self._app.state_machine.dropTimeState = "enter" self.operationAttributeDeleted.emit() except Exception as ex: logging.error( f"Error deleting an operation_attribute record: {ex}") @pyqtSlot(str, name="playSound") def play_sound(self, sound_name): """ Method to play a sound. The sound_name will indicate the purpose of the sound. Currently the only sound played is when the first Drop/Angler timer hits 4:45, and so has 15 seconds until it should start retrieving the hooks :param sound_name: :return: """ logging.info(f"playing sound: {sound_name}") self._sound_player.play_sound(sound_name=sound_name) @pyqtSlot(str, name="getSoundPlaybackTime", result=QVariant) def get_sound_playback_time(self, begin_fishing_time): """ Method to determine the time that is 4:45 (min:sec) past the begin_fishing_time, for when to play the sound warning the HookMatrix user to have the anglers start pulling in their lines :param begin_fishing_time: :return: """ min_shift = 4 sec_shift = 45 try: if ":" in begin_fishing_time: min, sec = begin_fishing_time.split(":") min = int(min) sec = int(sec) logging.info(f"min={min}, sec={sec}") start_time = arrow.now().replace(tzinfo="US/Pacific").replace( minute=min, second=sec) end_time = start_time.shift(minutes=min_shift, seconds=sec_shift).format("mm:ss") logging.info(f"end time to play the sound: {end_time}") return end_time except Exception as ex: logging.error(f"Error getting the play sound time: {ex}") return "04:45"