Example #1
0
    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()
Example #3
0
    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)
Example #5
0
    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}")
Example #8
0
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
Example #9
0
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
Example #10
0
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"