def __init__(self, db):
        super().__init__()
        self._logger = logging.getLogger(__name__)
        self._data = db
        self._hauls_model = HaulSetModel()
        self._current_haul = None
        self._internal_haul_idx = None  # FramListModel index
        self._hauls_count = 0
        self._trip_id = None
        self._fishing_locations = ObserverFishingLocations()

        self._beaufort = db.beaufort
        self._gearperf = db.gearperf

        self._obs_ret_models = dict()
        self._obs_ret_max_count = 0
        self._vessel_ret_models = dict()
        self._vessel_ret_max_count = 0
        # Hauls fields that must be specified in UI (can't be left empty)
        self._required_field_names = (  # As known in peewee
            'observer_total_catch',  # Visual OTC
            'otc_weight_method',
            'brd_present',
            # No: 'fit',
            # No: 'cal_weight',  # Has 'No Scale' value if not-calibrated.
            'gear_type',
            'gear_performance',
            'beaufort_value',
        )
        self._thread_ter = None  # Runs TER in background

        random.seed()  # ensure device_random_seed is randomly seeded
        self._device_random_seed = int(ObserverDBUtil.get_or_set_setting('biolist_rng_seed', random.randint(1, 10000)))
        self._trip_seed = 0
示例#2
0
    def __init__(self, db):
        super().__init__()
        self._logger = logging.getLogger(__name__)
        self._data = db
        self._sets_model = HaulSetModel()  # SetModel == HaulModel
        self._current_set = None
        self._internal_set_idx = None  # FramListModel index
        self._sets_count = 0
        self._trip_id = None
        self._current_trip = None
        self._fishing_locations = ObserverFishingLocations()

        self._beaufort = db.beaufort
        self._gearperf_fg = db.gearperf_fg

        self._otc_weight_method = None
        self._obs_ret_models = dict()
        self._obs_ret_max_count = 0
        self._vessel_ret_models = dict()
        self._vessel_ret_max_count = 0
        self._seabird_geartypes = ['7', '9', '19', '20']
        self._current_biolist_num = self._default_biolist_num = 0

        # TODO sets fields that must be specified in UI (can't be left empty)
        self._required_field_names = (  # As known in peewee
            'otc_weight_method',
            'gear_type',
            'beaufort_value',
            'deterrent_used',  # seabird
        )
        self._thread_ter = None  # Runs TER in background

        self._trip_seed = 0
示例#3
0
class Hauls(QObject):
    modelChanged = pyqtSignal(name='modelChanged')
    currentHaulIdChanged = pyqtSignal(str, name='currentHaulIdChanged')
    parameterChanged = pyqtSignal(name='parameterChanged')
    obsRetModelChanged = pyqtSignal(name='obsRetModelChanged')
    maxObsRetModelLengthChanged = pyqtSignal(
        int, name='maxObsRetModelLengthChanged')
    maxVesselRetModelLengthChanged = pyqtSignal(
        int, name='maxVesselRetModelLengthChanged')
    currentBiolistNumChanged = pyqtSignal(name='currentBiolistNumChanged')
    unusedSignal = pyqtSignal(name='unusedSignal')  # quiet warnings
    otcWeightChanged = pyqtSignal(name='otcWeightChanged')

    def __init__(self, db):
        super().__init__()
        self._logger = logging.getLogger(__name__)
        self._data = db
        self._hauls_model = HaulSetModel()
        self._current_haul = None
        self._internal_haul_idx = None  # FramListModel index
        self._hauls_count = 0
        self._trip_id = None
        self._fishing_locations = ObserverFishingLocations()

        self._beaufort = db.beaufort
        self._gearperf = db.gearperf

        self._obs_ret_models = dict()
        self._obs_ret_max_count = 0
        self._vessel_ret_models = dict()
        self._vessel_ret_max_count = 0
        # Hauls fields that must be specified in UI (can't be left empty)
        self._required_field_names = (  # As known in peewee
            'observer_total_catch',  # Visual OTC
            'otc_weight_method',
            'brd_present',
            # No: 'fit',
            # No: 'cal_weight',  # Has 'No Scale' value if not-calibrated.
            'gear_type',
            'gear_performance',
            'beaufort_value',
        )
        self._thread_ter = None  # Runs TER in background

        random.seed()  # ensure device_random_seed is randomly seeded
        self._device_random_seed = int(
            ObserverDBUtil.get_or_set_setting('biolist_rng_seed',
                                              random.randint(1, 10000)))
        self._trip_seed = 0

    @pyqtSlot(name="youAreUp")
    def you_are_up(self):
        """
        Called by ObserverSM.qml when context switches to Hauls screen.
        Used to pull counts of haul-level issues from the current trip's last Trip Error Report, if any.
        :return: None
        """
        self._logger.info(f"Screen is active. Current TripID={self._trip_id}.")
        self._logger.info(
            f"Count of hauls (from HaulsModel)={self._hauls_model.count}.")
        current_user_id = ObserverDBUtil.get_current_user_id()

        # If a Trip Error Report has been run for this trip, get the latest run's haul-level error counts, per-haul.
        trip_issues, run_date = TripChecksOptecsManager.get_issues_from_last_ter_run(
            self._trip_id, self._logger)
        if not trip_issues:
            for row in range(self._hauls_model.count):
                self._hauls_model.setProperty(row, "errors", "N/A")
            self._logger.debug(
                "No Trip Error Report has been run for this trip yet. Haul-level error counts to N/A."
            )
        else:
            haul_error_counts = self._calculate_per_haul_errors(trip_issues)

            view_model_rows = self._hauls_model.items
            # Update view model with haul-level errors, on a haul-by-haul basis
            for row_idx in range(self._hauls_model.count):
                view_model_row = view_model_rows[row_idx]
                haul_num = int(view_model_row["fishing_activity_num"])
                errors_this_haul = haul_error_counts[haul_num] \
                    if haul_num in haul_error_counts else 0
                self._hauls_model.setProperty(row_idx, "errors",
                                              str(errors_this_haul))
            self._logger.debug(
                f"Updated view model with Trip Error Report with run date {run_date}."
            )

    @staticmethod
    def _calculate_per_haul_errors(trip_issues: Iterable) -> Dict[int, int]:
        per_haul_errors = {}  # Key: haul number. TODO: Use default dict
        for issue in trip_issues:
            # TODO: only count haul-level errors or below
            # Perhaps done. If there's no haul number, is the issue not haul-related?
            if issue.fishing_activity_num is None:
                pass
            elif issue.fishing_activity_num not in per_haul_errors:
                per_haul_errors[int(issue.fishing_activity_num)] = 1
            else:
                per_haul_errors[int(issue.fishing_activity_num)] += 1
        return per_haul_errors

    def load_hauls(self, trip_id):
        """
        Load hauls from database, build FramListModel
        :return:
        """
        self._hauls_model.clear()
        hauls_q = FishingActivities.select().where(
            FishingActivities.trip == trip_id)
        self._hauls_count = hauls_q.count()
        if self._hauls_count > 0:
            for haul in hauls_q:  # Build FramListModel
                self._hauls_model.add_haul(haul)
        self.modelChanged.emit()
        self._trip_id = trip_id
        self._set_trip_biolist_seed(trip_id)
        return self._hauls_count

    @pyqtSlot(name='reset')
    def reset(self):
        """
        Clear hauls, reset states
        """
        self._current_haul = None
        self._hauls_model = HaulSetModel()

    @pyqtProperty(QVariant, notify=modelChanged)
    def HaulsModel(self):
        return self._hauls_model

    @pyqtProperty(QVariant, notify=modelChanged)
    def SetsModel(self):
        """
        TODO Fixed Gear
        """
        return self._sets_model

    @pyqtProperty(QVariant, notify=currentHaulIdChanged)
    def locations(self):
        return self._fishing_locations

    @pyqtProperty(int, notify=modelChanged)
    def haul_count(self):
        return self._hauls_count

    @property
    def current_haul(self):
        return self._current_haul

    @pyqtProperty(str)
    def current_fishing_activity_id(self):
        return str(self._current_haul.fishing_activity
                   ) if self._current_haul else None

    @pyqtProperty(str, notify=currentHaulIdChanged)
    def currentHaulId(self):
        return str(self._current_haul.fishing_activity_num
                   ) if self._current_haul else None

    @pyqtProperty(QVariant, notify=currentHaulIdChanged)
    def currentHaulDBId(self):
        return self._current_haul.fishing_activity if self._current_haul else None

    @pyqtSlot()
    def refresh(self):
        """
        Called to reload haul info (currently used for changes in Locations model)
        @return:
        """
        self.load_hauls(self._trip_id)

    @currentHaulId.setter
    def currentHaulId(self, current_id):
        """
        Assigned in HaulsScreen.  Also sets current_haulset_id with dbid here
        :param current_id: haul number (not DB ID)
        :return: None
        """
        self._logger.debug('Set currentHaul using ID {}'.format(current_id))
        try:
            self._current_haul = FishingActivities.get(
                FishingActivities.trip == self._trip_id,
                FishingActivities.fishing_activity_num == current_id)
            # FIELD-1471: setting db id for downstream use in baskets
            ObserverDBUtil.db_save_setting('current_haulset_id',
                                           self._current_haul.fishing_activity)
            self._logger.debug(
                f"Setting current_haulset_id to {self._current_haul.fishing_activity}"
            )
            self._internal_haul_idx = self._hauls_model.get_item_index(
                'fishing_activity_num',
                self._current_haul.fishing_activity_num)
            self._fishing_locations.load_fishing_locations(
                fishing_activity_id=self._current_haul.fishing_activity)
        except FishingActivities.DoesNotExist:
            self._logger.info("Can't get haul ID for trip {}, num {}".format(
                self._trip_id, current_id))
            self._current_haul = None
            return

        self.modelChanged.emit()

    @pyqtSlot(str, result=bool, name='removeHaul')
    def removeHaul(self, haul_id):
        result = self.HaulsModel.remove_haul(haul_id)
        self.currentHaulId = self.HaulsModel.most_recent_haul_id()
        return result

    @pyqtSlot(str, result=str, name='getBeaufortDesc')
    def getBeaufortDesc(self, value):
        """
        Given a single digit value, get description of beaufort scale value
        @param value: '0' - '9'
        @return: str description
        """
        return self._beaufort.get(value, 'No Description')

    @pyqtSlot(str, result=str, name='getGearPerfDesc')
    def getGearPerfDesc(self, value):
        """
        Given a single digit value, get description of gear perf value
        @param value: '1' - '7'
        @return: str description
        """
        return self._gearperf.get(value, 'No Description')

    def _set_cur_prop(self, model_prop, value):
        """
        Helper function - set current haul properties in FramListModel
        @param model_prop: property name
        @param value: value to store
        @return:
        """
        self._hauls_model.setProperty(self._internal_haul_idx, model_prop,
                                      value)

    @pyqtSlot(str, result='QVariant', name='getDataOrHaulDefault')
    def get_data_or_default(self, data_name):
        """
        If a Haul #1 has set this data, then get that (and set it.)
        @param data_name: name of data
        @return: db value or default
        """

        haul_data = self.getData(data_name=data_name)
        if haul_data is not None and haul_data != '':
            return haul_data
        elif self._current_haul.fishing_activity_num == 1 and data_name != 'efp':
            self._logger.debug(
                f'First haul, not retrieving default for {data_name}')
            return None
        else:  # Get defaults from first_haul
            try:
                first_haul = FishingActivities.get(
                    (FishingActivities.trip == self._current_haul.trip),
                    (FishingActivities.fishing_activity_num == 1))
                if data_name == 'target_strategy':
                    default_data = self._get_target_code(
                        first_haul.target_strategy)
                    self.setData(data_name, default_data)
                    return default_data
                elif data_name == 'gear_type':
                    default_data = first_haul.gear_type
                    self.setData(data_name, default_data)
                    return default_data
                elif data_name == 'efp':
                    default_data = self._get_efp(first_haul)
                    efp_data = True if default_data else None  # For DB EFP special case
                    self.setData(data_name, efp_data)
                    return default_data
                elif data_name == 'brd_present':
                    default_data = self._get_brd_present(first_haul)
                    self.setData(data_name, default_data)
                    return default_data
                else:
                    self._logger.error(
                        f'Get Default requested, but field {data_name} is not default-approved.'
                    )
                    return ''
            except Exception as e:
                self._logger.error(f'Cannot get default {data_name}: {e}')
                return ''

    @staticmethod
    def _get_efp(fishing_activity):
        return True if fishing_activity.efp == 'EFP' else None

    @staticmethod
    def _get_efp_local(fishing_activity):
        # tri-state: 1, 0, None
        val = fishing_activity.efp_localonly
        if val == 1:
            return True
        elif val == 0:
            return False
        else:
            return None

    @staticmethod
    def _get_brd_present(fishing_activity):
        if fishing_activity.brd_present is not None:
            return True if fishing_activity.brd_present == 'TRUE' else False
        else:
            return None

    @pyqtSlot(str, result='QVariant', name='getData')
    def getData(self, data_name):
        """
        Shortcut to get data from the DB that doesn't deserve its own property
        (Note, tried to use a dict to simplify this, but DB cursors were not updating)
        :return: Value found in DB
        """
        if self._current_haul is None:
            logging.warning('Attempt to get data with null current haul.')
            return None
        data_name = data_name.lower()
        return_val = None
        if data_name == 'observer_total_catch':
            return_val = self._current_haul.observer_total_catch
        elif data_name == 'otc_weight_method':
            return_val = self._current_haul.otc_weight_method
        elif data_name == 'fit':
            return_val = self._current_haul.fit
        elif data_name == 'brd_present':
            return self._get_brd_present(self._current_haul)
        elif data_name == 'efp':
            # Center IFQ Database (target of DB Sync) convention: 'EFP' or null.
            efp_val = self._get_efp(self._current_haul)
            local_efp_val = self._get_efp_local(self._current_haul)
            return local_efp_val if local_efp_val is not None else efp_val  # tristate bool
        elif data_name == 'cal_weight':
            return_val = self._current_haul.cal_weight
        elif data_name == 'beaufort_value':
            return_val = self._current_haul.beaufort_value
        elif data_name == 'gear_performance':
            return_val = self._current_haul.gear_performance
        elif data_name == 'target_strategy':
            return_val = self._get_target_code(
                self._current_haul.target_strategy)
        elif data_name == 'gear_type':
            return_val = self._current_haul.gear_type
        else:
            logging.warning(
                'Attempt to get unknown data name: {}'.format(data_name))

        return '' if return_val is None else return_val

    @pyqtProperty(int, notify=unusedSignal)
    def retainedHaulWeight(self):
        """
        assumes fishing_activity_id is loaded, catch if not???
        left joins, should always return a number, even if no retained
        :return: int (sum of retained catch weights for haul)
        """
        return FishingActivities.select(fn.COALESCE(fn.sum(Catches.catch_weight), 0))\
            .join(Catches, JOIN.LEFT_OUTER).where(
            (Catches.fishing_activity == self._current_haul.fishing_activity) &
            (Catches.catch_disposition == 'R')
        ).scalar()

    @pyqtSlot(str, QVariant, name='setData')
    def setData(self, data_name, data_val):
        """
        Set misc data to the DB
        :return:
        """
        if self._current_haul is None:
            logging.warning('Attempt to set data with null current haul.')
            return
        data_name = data_name.lower()
        if data_name == 'observer_total_catch':
            self._current_haul.observer_total_catch = float(
                data_val) if data_val else 0.0
        elif data_name == 'otc_weight_method':
            self._current_haul.otc_weight_method = int(
                data_val) if data_val else 0
        elif data_name == 'fit':
            # FIELD-1374: Numpad Backspace (<-) key returns empty string rather than 0. Treat as '0'.
            fit_val = data_val if data_val != "" else '0'
            self._current_haul.fit = str(fit_val)
        elif data_name == 'brd_present':
            if data_val:
                self._current_haul.brd_present = 'TRUE'
            else:
                self._current_haul.brd_present = 'FALSE'
        elif data_name == 'efp':
            # Center IFQ Database (target of DB Sync) convention: 'EFP' or ''.
            if data_val:
                self._current_haul.efp = 'EFP'
                self._current_haul.efp_localonly = 1
            else:
                self._current_haul.efp = None  # NULL in Database, but False for model prop
                self._current_haul.efp_localonly = 0
        elif data_name == 'cal_weight':
            self._current_haul.cal_weight = data_val
        elif data_name == 'beaufort_value':
            self._current_haul.beaufort_value = data_val
        elif data_name == 'gear_performance':
            self._current_haul.gear_performance = data_val
        elif data_name == 'target_strategy':
            self._current_haul.target_strategy = self._lookup_target_strat_id(
                data_val)
            # Translate for listview on HaulsScreen
            self._set_cur_prop(
                'target_strategy_code',
                self._get_target_code(self._current_haul.target_strategy))
        elif data_name == 'gear_type':
            if ' ' in data_val:
                data_val = data_val.split(' ')[0]  # Extract value
                data_val = str(int(data_val))  # remove leading zero
            self._current_haul.gear_type = data_val
        else:
            logging.warning(
                'Attempt to set unknown data name: {}'.format(data_name))
            return
        self._current_haul.save()
        self._set_cur_prop(data_name, data_val)

        logging.debug('Set {} to {}'.format(data_name, data_val))
        self.modelChanged.emit()

        if data_name == 'observer_total_catch':
            self.otcWeightChanged.emit()

    def _lookup_target_strat_id(self, strat: str):
        """
        Use first 4 chars as a code and look up CATCH_CATEGORY_ID
        @param strat: code + optional text
        @return: int ID
        """
        if strat is None or len(strat) < 3:
            self._logger.debug('Got bad strat ID to look up: {}'.format(strat))
            return

        find_code = strat.split(' ')[0]
        try:
            found = CatchCategories.get(CatchCategories.catch_category_code %
                                        find_code)
            self._logger.info('Found ID {} for {}'.format(
                found.catch_category, find_code))
            return found.catch_category
        except CatchCategories.DoesNotExist:
            self._logger.warning('Did not find ID for {}'.format(find_code))
            return None

    def _get_target_code(self, target_id: int):
        """
        Find pacfic code for id
        @param target_id: catch_category_id
        @return: int ID
        """
        if target_id is None:
            return ''
        try:
            found = CatchCategories.get(
                CatchCategories.catch_category == target_id)
            return found.catch_category_code
        except CatchCategories.DoesNotExist:
            self._logger.warning('Did not find code for {}'.format(target_id))
            return None

    @pyqtProperty(int, notify=maxObsRetModelLengthChanged)
    def maxObsRetModelLength(self):
        return self._obs_ret_max_count

    @pyqtProperty(int, notify=maxVesselRetModelLengthChanged)
    def maxVesselRetModelLength(self):
        return self._vessel_ret_max_count

    @pyqtProperty(QVariant, notify=unusedSignal)
    def requiredHaulFieldsAreSpecified(self):
        """
        Have all the prerequisite fields in the Haul Details screen been filled in for the current haul
        so that navigation to Catch screen is allowed?

        :return: True if gear type is not empty.
                False otherwise.
        """
        is_filled = [
            self.required_haul_field_is_specified(f)
            for f in self._required_field_names
        ]

        result = all(is_filled)

        self._logger.debug(
            "{}OK to navigate to Catch tab.".format("" if result else "Not "))
        return result

    @pyqtSlot(QVariant, result=QVariant, name='requiredHaulFieldIsSpecified')
    def required_haul_field_is_specified(self, field_name):
        """
            Expecting peewee Hauls field name, not name of text field in QML
            Return True if the field has a value, False otherwise.
            Raise exception if unknown field name.
        """
        if field_name not in self._required_field_names:
            raise Exception(
                f'Unexpected peewee Hauls field name: {field_name}')
        field_data = self.getData(field_name)
        if field_data is None:
            field_ok = False
        else:
            if isinstance(field_data, str):
                field_ok = len(field_data) > 0
            else:
                field_ok = True

        self._logger.debug(
            f'{field_name} -> {field_data} is {"" if field_ok else "not "}specified.'
        )
        return field_ok

    @pyqtProperty(QVariant, notify=unusedSignal)
    def isCalWeightSpecified(self):
        cal_wt = self.getData('cal_weight')
        if cal_wt:
            return True

        return False

    @pyqtProperty(bool, notify=unusedSignal)
    def isShrimpGear(self):
        return self.getData('gear_type') in ['12', '13']

    @pyqtSlot(QVariant, result=QVariant, name='getObserverRetModel')
    def getObserverRetModel(self, haul_db_id):
        """
        Load observer retained model
        @param haul_db_id: Database ID
        @return:
        """
        new_model = ObserverRetainedModel()
        new_model.load_observer_retained(haul_db_id)
        if new_model.count > self._obs_ret_max_count:
            self._obs_ret_max_count = new_model.count
            self.maxObsRetModelLengthChanged.emit(self._obs_ret_max_count)
        self._obs_ret_models[haul_db_id] = new_model
        return self._obs_ret_models[haul_db_id]

    @pyqtSlot(QVariant, str, result=bool, name='ccIsInObserverRetModel')
    def ccIsInObserverRetModel(self, haul_db_id, catch_category_code):
        """
        Given a catch category and a haul ID, return whether
        this catch category is in the list of Observer-Retained catch categories.
        @param haul_db_id: Database ID
        @param catch_category_code: string (category code such as 'ALBC')
        @return: True if  catch_category_code is in list of observer-retained categories.
        """
        if haul_db_id not in self._obs_ret_models:
            self._logger.debug(
                'ccIsInObserverRetModel unexpectedly called before getObserverRetModel'
            )
            new_model = ObserverRetainedModel()
            new_model.load_observer_retained(haul_db_id)
            self._obs_ret_models[haul_db_id] = new_model

        observer_retained_model = self._obs_ret_models[haul_db_id]
        return observer_retained_model.is_item_in_model(
            'cc_code', catch_category_code)

    @pyqtSlot(QVariant, result=QVariant, name='getVesselRetModel')
    def getVesselRetModel(self, haul_db_id):
        new_model = VesselRetainedModel()
        new_model.load_vessel_retained(haul_db_id)
        self._update_vessel_ret_max(new_model)
        self._vessel_ret_models[haul_db_id] = new_model

        return self._vessel_ret_models[haul_db_id]

    @pyqtSlot(QVariant, QVariant, name='addVesselRetained')
    def addVesselRetained(self, haul_db_id, vessel_info):
        self._vessel_ret_models[haul_db_id].add_vessel_ret(
            haul_db_id, vessel_info)
        self._update_vessel_ret_max(self._vessel_ret_models[haul_db_id])

    def _update_vessel_ret_max(self, model):
        if model.count > self._vessel_ret_max_count:
            self._vessel_ret_max_count = model.count
            # self._logger.info('Vessel ret max now{}'.format(self._vessel_ret_max_count))
            self.maxVesselRetModelLengthChanged.emit(
                self._vessel_ret_max_count)

    @pyqtSlot(QVariant, result=bool, name='checkHaulEmpty')
    def check_haul_empty(self, haul_id):
        """
        Queries DB to see if there are data records associated with a haul
        @param haul_id: DBID
        @return: True if haul is empty and can be deleted, False otherwise
        """
        if not haul_id:
            self._logger.warning('Invalid haul ID passed to check_haul_empty.')
            return False

        catches_q = Catches.select().where(Catches.fishing_activity == haul_id)
        if len(catches_q) > 0:
            self._logger.debug(
                'Haul {} is not empty, has {} catches associated.'.format(
                    haul_id, len(catches_q)))
            return False
        return True

    @pyqtSlot(QVariant, result=int)
    def create_haul(self, haul_num):
        """
        @param haul_num: ID local to this trip
        @return: haul db ID
        """
        self.load_hauls(trip_id=self._trip_id)
        observer_id = ObserverDBUtil.get_current_user_id()
        newhaul = FishingActivities.create(
            trip=self._trip_id,
            fishing_activity_num=haul_num,
            created_by=observer_id,
            created_date=ObserverDBUtil.get_arrow_datestr())
        logging.info('Created FishingActivities (haul {}) for trip={}'.format(
            newhaul.fishing_activity_num, newhaul.trip))

        self.HaulsModel.add_haul(newhaul)
        self.currentHaulId = newhaul.fishing_activity_num
        self._create_biolist_comment()
        return int(newhaul.fishing_activity)

    @pyqtSlot(QVariant, result=bool, name='deleteHaul')
    def delete_haul(self, haul_id):
        if haul_id is None or not self.check_haul_empty(haul_id):
            return

        trip_id = self._trip_id
        self._current_haul = None  # FIELD-1817 always clear current haul ID if deleting

        # Delete from DB
        haul = FishingActivities.get(
            FishingActivities.fishing_activity == haul_id)
        ObserverDBUtil.log_peewee_model_instance(self._logger, haul,
                                                 'Deleting haul')
        haul.delete_instance(recursive=True)

        # Delete from model
        result = self._hauls_model.remove_haul_set(haul_id)

        # Check for hauls with greater fishing_activity_num # than this one, and if found, decrement them.
        renumber_hauls = FishingActivities.select().where(
            (FishingActivities.fishing_activity > haul_id)
            & (FishingActivities.trip == trip_id))
        for h in renumber_hauls:
            old_num = h.fishing_activity_num
            h.fishing_activity_num -= 1
            h.save()
            fishing_number_row = self._hauls_model.get_haul_set_index(
                h.fishing_activity)
            self._logger.info(
                f'Renumbered FISHING_ACTIVITY_NUM {old_num} to {h.fishing_activity_num} for haul {h.fishing_activity}'
            )
            self._hauls_model.setProperty(fishing_number_row,
                                          'fishing_activity_num',
                                          h.fishing_activity_num)

        return result

    # TODO: Create setter like in FG?
    @pyqtProperty(QVariant, notify=currentBiolistNumChanged)
    def currentBiolistNum(self):
        return self._get_biolist_num()

    def _create_biolist_comment(self):
        """
        Replaces _save_notes_biolist_num func
        Adds biolist string to comment table instead of saving directly to fishing_activity.notes.
        Comment record is picked up for comment parsing later (FIELD-2071)
        :return: None
        """
        if not self._current_haul:
            self._logger.error(
                'Tried to save biolist num, but current haul not set.')
            return
        BIOLIST_NOTE_PREFIX = 'Biolist #'
        APPSTATE = f"haul_details_state::Haul {self.currentHaulId} Details"  # could make a param, but shouldn't change

        # check if biolist comment already exists
        existing_comments = Comment.select().where(
            (Comment.trip == self._current_haul.trip)
            & (Comment.fishing_activity == self._current_haul.fishing_activity)
            &
            (fn.Lower(Comment.comment).contains(BIOLIST_NOTE_PREFIX.lower())))

        if not existing_comments:
            Comment.create(
                username=ObserverDBUtil.get_setting('current_user'),
                comment_date=ObserverDBUtil.get_arrow_datestr(),
                comment=f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum}",
                appstateinfo=APPSTATE,
                trip=self._current_haul.trip,
                fishing_activity=self._current_haul.fishing_activity)
            self._logger.debug(
                f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum} comment created."
            )
        else:  # not sure if this will ever get used for trawl, but will update if necessary
            query = Comment.update(
                username=ObserverDBUtil.get_setting('current_user'),
                comment_date=ObserverDBUtil.get_arrow_datestr(),
                comment=f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum}",
                appstateinfo=APPSTATE,
            ).where((Comment.fishing_activity ==
                     self._current_haul.fishing_activity)
                    & (Comment.trip == self._current_haul.trip)
                    & (Comment.comment.regexp('^' + BIOLIST_NOTE_PREFIX +
                                              '\d+$')
                       )  # starts with Biolist #, ends with nums
                    )
            query.execute()
            self._logger.debug(
                f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum} comment updated."
            )

    @pyqtSlot(name='updateBiolistNote')
    def _save_notes_biolist_num(self):
        """
        Store BIOLIST num to notes
        NOTE: most functionality here has been replaced by self._create_biolist_comment
        :return:
        """
        if not self._current_haul:
            self._logger.error(
                'Tried to save biolist num, but current haul not set.')
            return
        notes = self._current_haul.notes if self._current_haul.notes else ''
        BIOLIST_NOTE_PREFIX = 'Biolist #'
        if notes.find(BIOLIST_NOTE_PREFIX) == -1:
            notes = f'{notes} {BIOLIST_NOTE_PREFIX}{self.currentBiolistNum}'
            self._current_haul.notes = notes
            self._current_haul.save()
            self._logger.debug(f'Saved Notes: {notes}')

    def _get_biolist_num(self):
        """
        Each device will have a static random seed stored in observersettings database.
        When a new trip ID is set, then we use that as a seed too.
        Auto-increments when new hauls are added, and is random when new trip starts
        @return:
        """
        haul_num = self._current_haul.fishing_activity_num if self._current_haul else 0
        return (self._trip_seed + haul_num) % 3 + 1

    def isTrainingMode(self):
        """
        @return: True if training mode
        """
        mode = ObserverDBUtil.get_setting('training')
        return True if mode == 'TRUE' else False

    def _set_trip_biolist_seed(self, trip_id):
        """
        For new trip, get a new biolist num seed
        @param trip_id: used as int to seed RNG
        @return:
        """
        if trip_id is None:
            self._logger.warning(f'Warn, None trip id passed to RNG.')
            return

        if not self.isTrainingMode():
            random.seed(self._device_random_seed +
                        int(trip_id))  # repeatable RNG seed
            self._trip_seed = random.randint(1, 1000)
            self._logger.debug(
                f'RNG seed {self._device_random_seed} with trip id {trip_id} -> {self._trip_seed} trip seed'
            )
        else:
            random.seed(self._device_random_seed)
            self._trip_seed = 2
            self._logger.info(f'TRAINING MODE: SET BIOLIST SEED')
示例#4
0
 def reset(self):
     """
     Clear hauls, reset states
     """
     self._current_haul = None
     self._hauls_model = HaulSetModel()
示例#5
0
class Sets(QObject):
    modelChanged = pyqtSignal(name='modelChanged')
    currentSetIdChanged = pyqtSignal(str, name='currentSetIdChanged')
    parameterChanged = pyqtSignal(name='parameterChanged')
    obsRetModelChanged = pyqtSignal(name='obsRetModelChanged')
    maxObsRetModelLengthChanged = pyqtSignal(
        int, name='maxObsRetModelLengthChanged')
    maxVesselRetModelLengthChanged = pyqtSignal(
        int, name='maxVesselRetModelLengthChanged')
    currentBiolistNumChanged = pyqtSignal(name='currentBiolistNumChanged')
    otcWeightMethodChanged = pyqtSignal(QVariant,
                                        name='otcWeightMethodChanged')
    otcFGWeightChanged = pyqtSignal(QVariant,
                                    name='otcFGWeightChanged',
                                    arguments=['otc'])
    unusedSignal = pyqtSignal(name='unusedSignal')  # quiet warnings

    def __init__(self, db):
        super().__init__()
        self._logger = logging.getLogger(__name__)
        self._data = db
        self._sets_model = HaulSetModel()  # SetModel == HaulModel
        self._current_set = None
        self._internal_set_idx = None  # FramListModel index
        self._sets_count = 0
        self._trip_id = None
        self._current_trip = None
        self._fishing_locations = ObserverFishingLocations()

        self._beaufort = db.beaufort
        self._gearperf_fg = db.gearperf_fg

        self._otc_weight_method = None
        self._obs_ret_models = dict()
        self._obs_ret_max_count = 0
        self._vessel_ret_models = dict()
        self._vessel_ret_max_count = 0
        self._seabird_geartypes = ['7', '9', '19', '20']
        self._current_biolist_num = self._default_biolist_num = 0

        # TODO sets fields that must be specified in UI (can't be left empty)
        self._required_field_names = (  # As known in peewee
            'otc_weight_method',
            'gear_type',
            'beaufort_value',
            'deterrent_used',  # seabird
        )
        self._thread_ter = None  # Runs TER in background

        self._trip_seed = 0

    @pyqtSlot(name="youAreUp")
    def you_are_up(self):
        """
        Called by ObserverSM.qml when context switches to Sets screen.
        Used to pull counts of haul-level issues from the current trip's last Trip Error Report, if any.
        :return: None
        """
        self._logger.info(f"Screen is active. Current TripID={self._trip_id}.")
        self._logger.info(
            f"Count of sets (from SetsModel)={self._sets_model.count}.")
        current_user_id = ObserverDBUtil.get_current_user_id()

        # If a Trip Error Report has been run for this trip, get the latest run's haul-level error counts, per-haul.
        trip_issues, run_date = TripChecksOptecsManager.get_issues_from_last_ter_run(
            self._trip_id, self._logger)
        if not trip_issues:
            for row in range(self._sets_model.count):
                self._sets_model.setProperty(row, "errors", "N/A")
            self._logger.debug(
                "No Trip Error Report has been run for this trip yet. Set-level error counts to N/A."
            )
        else:
            set_error_counts = self._calculate_per_set_errors(trip_issues)

            view_model_rows = self._sets_model.items
            # Update view model with haul-level errors, on a haul-by-haul basis
            for row_idx in range(self._sets_model.count):
                view_model_row = view_model_rows[row_idx]
                haul_num = int(view_model_row["fishing_activity_num"])
                errors_this_haul = set_error_counts[haul_num] \
                    if haul_num in set_error_counts else 0
                self._sets_model.setProperty(row_idx, "errors",
                                             str(errors_this_haul))
            self._logger.debug(
                f"Updated view model with Trip Error Report with run date {run_date}."
            )

    @staticmethod
    def _calculate_per_set_errors(trip_issues: Iterable) -> Dict[int, int]:
        per_set_errors = {}  # Key: haul number. TODO: Use default dict
        for issue in trip_issues:
            # TODO: only count haul-level errors or below
            # Perhaps done. If there's no haul number, is the issue not haul-related?
            if issue.fishing_activity_num is None:
                pass
            elif issue.fishing_activity_num not in per_set_errors:
                per_set_errors[int(issue.fishing_activity_num)] = 1
            else:
                per_set_errors[int(issue.fishing_activity_num)] += 1
        return per_set_errors

    def load_sets(self, trip_id):
        """
        Load sets from database, build FramListModel
        :return:
        """
        self._sets_model.clear()
        sets_q = FishingActivities.select().where(
            FishingActivities.trip == trip_id)
        self._sets_count = sets_q.count()
        self._logger.info(f'Loading {self._sets_count} sets')
        if self._sets_count > 0:
            for s in sets_q:  # Build FramListModel
                self._sets_model.add_set(s)
        self.modelChanged.emit()
        self._trip_id = trip_id
        self._current_trip = Trips.get(Trips.trip == trip_id)
        return self._sets_count

    @pyqtSlot(name='reset')
    def reset(self):
        """
        Clear sets, reset states
        """
        self._current_set = None
        self._sets_model = HaulSetModel()

    @pyqtProperty(QVariant, notify=modelChanged)
    def SetsModel(self):
        return self._sets_model

    @pyqtProperty(QVariant, notify=currentSetIdChanged)
    def locations(self):
        return self._fishing_locations

    @pyqtProperty(int, notify=modelChanged)
    def set_count(self):
        return self._sets_count

    @pyqtProperty(bool, notify=modelChanged)
    def requireSeabird(self):
        if self._current_set.gear_type in self._seabird_geartypes:
            return True
        else:
            return False

    @property
    def current_set(self):
        return self._current_set

    @pyqtProperty(QVariant, notify=currentSetIdChanged)
    def currentGearType(self):
        return self._current_set.gear_type if self._current_set else None

    @pyqtProperty(str)
    def current_fishing_activity_id(self):
        return str(
            self._current_set.fishing_activity) if self._current_set else None

    @pyqtProperty(str, notify=currentSetIdChanged)
    def currentSetId(self):
        return str(self._current_set.fishing_activity_num
                   ) if self._current_set else None

    @pyqtProperty(QVariant, notify=currentSetIdChanged)
    def currentSetDBId(self):
        return self._current_set.fishing_activity if self._current_set else None

    @pyqtSlot(int, float, name='updateTripHookCounts')
    def update_trip_hook_counts(self, trip_id, avg_hook_count):
        # The below line is a hacky fix, can't figure out what
        # is resetting total_hooks_kp to an incorrect value
        # right before this is run
        self._current_trip.total_hooks_kp = avg_hook_count
        sets_q = self.get_all_sets_with_gear_segments(trip_id)
        for s in sets_q:
            self._update_set_hook_counts(s, avg_hook_count)

    @pyqtProperty(str, notify=otcWeightMethodChanged)
    def otcWeightMethod(self):
        return self._otc_weight_method

    @pyqtSlot(float, int, name="updateModelOTC")
    def update_model_otc(self, otc_fg, fishing_activity_num):
        self._set_cur_prop('observer_total_catch', otc_fg)

    def _update_set_hook_counts(self, set_rec, avg_hook_count):
        if not avg_hook_count:  # None or 0
            avg_hook_count = 1

        if set_rec.tot_gear_segments is not None:
            new_total_hooks = round(avg_hook_count * set_rec.tot_gear_segments)
            if set_rec.total_hooks != new_total_hooks:
                set_rec.total_hooks = new_total_hooks
                self._logger.debug(
                    f'Set {set_rec.fishing_activity_num} new total hooks {new_total_hooks}'
                )
                set_rec.save()
                self._recalculate_catches_hooks_sampled(
                    set_rec, avg_hook_count)
                new_total_hooks_unrounded = avg_hook_count * set_rec.tot_gear_segments
                new_otc = ObserverCatches.calculate_OTC_FG(
                    self._logger, set_rec, new_total_hooks_unrounded)
                self.update_model_otc(new_otc, set_rec.fishing_activity_num)
                self.otcFGWeightChanged.emit(new_otc)

        if set_rec.gear_segments_lost is not None:
            new_lost_hooks = round(avg_hook_count * set_rec.gear_segments_lost)
            if set_rec.total_hooks_lost != new_lost_hooks:
                set_rec.total_hooks_lost = new_lost_hooks
                self._logger.debug(
                    f'Set {set_rec.fishing_activity_num} new total lost hooks {new_lost_hooks}'
                )
                set_rec.save()

    def _recalculate_catches_hooks_sampled(self, set_rec, avg_hook_count):
        catches_q = Catches.select().where(
            Catches.fishing_activity == set_rec.fishing_activity)

        for c in catches_q:
            if avg_hook_count and c.gear_segments_sampled:
                old_hooks = c.hooks_sampled
                new_hooks = round(avg_hook_count * c.gear_segments_sampled)
                self._logger.info(
                    f'Catch {c.catch} Hooks sampled {old_hooks} -> {new_hooks}'
                )
                c.hooks_sampled = new_hooks
                c.save()

    @pyqtSlot()
    def refresh(self):
        """
        Called to reload haul info (currently used for changes in Locations model)
        @return:
        """
        self.load_sets(self._trip_id)

    @currentSetId.setter
    def currentSetId(self, current_id):
        self._logger.debug('Set currentSet using ID {}'.format(current_id))
        try:
            self._current_set = FishingActivities.get(
                FishingActivities.trip == self._trip_id,
                FishingActivities.fishing_activity_num == current_id)
            self._internal_set_idx = self._sets_model.get_item_index(
                'fishing_activity_num', self._current_set.fishing_activity_num)
            self._fishing_locations.load_fishing_locations(
                fishing_activity_id=self._current_set.fishing_activity)
            if self._current_set.biolist_localonly is None:
                self._current_set.biolist_localonly = self._default_biolist_num
                self._save_notes_biolist_num()
        except FishingActivities.DoesNotExist:
            self._logger.info("Can't get set ID for trip {}, num {}".format(
                self._trip_id, current_id))
            self._current_set = None
            return

        self.modelChanged.emit()

    @pyqtSlot(str, result=bool, name='removeSet')
    def removeSet(self, set_id):
        result = self.SetsModel.remove_haul_set(set_id)
        self.currentSetId = self.SetsModel.most_recent_haul_set_id()
        return result

    @pyqtSlot(str, result=str, name='getBeaufortDesc')
    def getBeaufortDesc(self, value):
        """
        Given a single digit value, get description of beaufort scale value
        @param value: '0' - '9'
        @return: str description
        """
        return self._beaufort.get(value, 'No Description')

    @pyqtSlot(str, result=str, name='getGearPerfDesc')
    def getGearPerfDesc(self, value):
        """
        Given a single digit value, get description of gear perf value
        @param value: '1' - '8'
        @return: str description
        """
        return self._gearperf_fg.get(value, 'No Description')

    def _set_cur_prop(self, model_prop, value):
        """
        Helper function - set current haul properties in FramListModel
        @param model_prop: property name
        @param value: value to store
        @return:
        """
        if self._internal_set_idx is not None:
            self._sets_model.setProperty(self._internal_set_idx, model_prop,
                                         value)
        else:
            self._logger.warning(
                f'_internal_set_idx is None, skipping prop update {model_prop}: {value}'
            )

    @pyqtSlot(str, result='QVariant', name='getDataOrSetDefault')
    def get_data_or_default(self, data_name):
        """
        If a Set #1 has set this data, then get that (and set it.)
        @param data_name: name of data
        @return: db value or default
        """

        set_data = self.getData(data_name=data_name)
        if set_data is not None and set_data != '':
            return set_data
        elif self._current_set.fishing_activity_num == 1 and data_name != 'efp':
            self._logger.debug(
                f'First set, not retrieving default for {data_name}')
            return None
        else:  # Get defaults from first_set
            try:
                first_set = FishingActivities.get(
                    (FishingActivities.trip == self._current_set.trip),
                    (FishingActivities.fishing_activity_num == 1))
                if data_name == 'target_strategy':
                    default_data = self._get_target_code(
                        first_set.target_strategy)
                    self.setData(data_name, default_data)
                    return default_data
                elif data_name == 'gear_type':
                    default_data = first_set.gear_type
                    self.setData(data_name, default_data)
                    return default_data
                elif data_name == 'efp':
                    default_data = self._get_efp(first_set)
                    efp_data = True if default_data else None  # For DB EFP special case
                    self.setData(data_name, efp_data)
                    return default_data
                elif data_name == 'deterrent_used':
                    default_data = self._get_deterrent_used(first_set)
                    self.setData(data_name, default_data)
                    return default_data
                else:
                    self._logger.error(
                        f'Get Default requested, but field {data_name} is not default-approved.'
                    )
                    return ''
            except Exception as e:
                self._logger.error(f'Cannot get default {data_name}: {e}')
                return ''

    @staticmethod
    def _get_efp(fishing_activity):
        return True if fishing_activity.efp == 'EFP' else None

    @staticmethod
    def _get_efp_local(fishing_activity):
        # tri-state: 1, 0, None
        val = fishing_activity.efp_localonly
        if val == 1:
            return True
        elif val == 0:
            return False
        else:
            return None

    @staticmethod
    def _get_brd_present(fishing_activity):
        if fishing_activity.brd_present is not None:
            return True if fishing_activity.brd_present == 'TRUE' else False
        else:
            return None

    @staticmethod
    def _get_deterrent_used(fishing_activity):
        if fishing_activity.deterrent_used is not None:
            return True if fishing_activity.deterrent_used == '1' else False
        else:
            return None

    @pyqtSlot(str, result='QVariant', name='getData')
    def getData(self, data_name):
        """
        Shortcut to get data from the DB that doesn't deserve its own property
        (Note, tried to use a dict to simplify this, but DB cursors were not updating)
        :return: Value found in DB
        """
        if self._current_set is None:
            logging.warning('Attempt to get data with null current haul.')
            return None
        data_name = data_name.lower()
        return_val = None
        if data_name == 'observer_total_catch':
            return_val = self._current_set.observer_total_catch
        elif data_name == 'otc_weight_method':
            return_val = self._current_set.otc_weight_method
        elif data_name == 'fit':
            return_val = self._current_set.fit
        elif data_name == 'brd_present':
            return self._get_brd_present(self._current_set)
        elif data_name == 'efp':
            # Center IFQ Database (target of DB Sync) convention: 'EFP' or null.
            efp_val = self._get_efp(self._current_set)
            local_efp_val = self._get_efp_local(self._current_set)
            return local_efp_val if local_efp_val is not None else efp_val  # tristate bool
        elif data_name == 'cal_weight':
            return_val = self._current_set.cal_weight
        elif data_name == 'beaufort_value':
            return_val = self._current_set.beaufort_value
        elif data_name == 'avg_soak_time':
            return_val = self._current_set.avg_soak_time
        elif data_name == 'tot_gear_segments':
            return_val = self._current_set.tot_gear_segments
        elif data_name == 'gear_segments_lost':
            return_val = self._current_set.gear_segments_lost
        elif data_name == 'total_hooks':
            return_val = self._current_set.total_hooks
        elif data_name == 'total_hooks_lost':
            return_val = self._current_set.total_hooks_lost
        elif data_name == 'deterrent_used':  # seabird
            return_val = self._current_set.deterrent_used
        elif data_name == 'gear_performance':
            return_val = self._current_set.gear_performance
        elif data_name == 'target_strategy':
            return_val = self._get_target_code(
                self._current_set.target_strategy)
        elif data_name == 'gear_type':
            return_val = self._current_set.gear_type
        else:
            logging.warning(
                'Attempt to get unknown data name: {}'.format(data_name))

        return '' if return_val is None else return_val

    @pyqtSlot(str, QVariant, name='setData')
    def setData(self, data_name, data_val):
        """
        Set misc data to the DB
        :return:
        """
        if self._current_set is None:
            logging.warning('Attempt to set data with null current haul.')
            return
        data_name = data_name.lower()
        if data_name == 'observer_total_catch':
            self._current_set.observer_total_catch = float(
                data_val) if data_val else 0.0
            self.otcFGWeightChanged.emit(data_val)
        elif data_name == 'otc_weight_method':
            self._current_set.otc_weight_method = int(
                data_val) if data_val else 0
        elif data_name == 'fit':
            # FIELD-1374: Numpad Backspace (<-) key returns empty string rather than 0. Treat as '0'.
            fit_val = data_val if data_val != "" else '0'
            self._current_set.fit = str(fit_val)
        elif data_name == 'brd_present':
            if data_val:
                self._current_set.brd_present = 'TRUE'
            else:
                self._current_set.brd_present = 'FALSE'
        elif data_name == 'deterrent_used':
            if data_val:
                self._current_set.deterrent_used = '1'
            else:
                self._current_set.deterrent_used = '0'
        elif data_name == 'efp':
            # Center IFQ Database (target of DB Sync) convention: 'EFP' or ''.
            if data_val:
                self._current_set.efp = 'EFP'
                self._current_set.efp_localonly = 1
            else:
                self._current_set.efp = None  # NULL in Database, but False for model prop
                self._current_set.efp_localonly = 0
        elif data_name == 'cal_weight':
            self._current_set.cal_weight = data_val
        elif data_name == 'beaufort_value':
            self._current_set.beaufort_value = data_val
        elif data_name == 'tot_gear_segments':
            self._current_set.tot_gear_segments = int(
                data_val) if data_val else None
        elif data_name == 'gear_segments_lost':
            self._current_set.gear_segments_lost = int(
                data_val) if data_val else None
        elif data_name == 'total_hooks':
            self._current_set.total_hooks = int(data_val) if data_val else None
        elif data_name == 'total_hooks_unrounded':
            self._current_set.total_hooks_unrounded = float(
                data_val) if data_val else None
        elif data_name == 'total_hooks_lost':
            self._current_set.total_hooks_lost = int(
                data_val) if data_val else None
        elif data_name == 'gear_performance':
            self._current_set.gear_performance = data_val
        elif data_name == 'target_strategy':
            self._current_set.target_strategy = self._lookup_target_strat_id(data_val) \
                if data_val else None
            # Translate for listview on HaulsScreen
            self._set_cur_prop(
                'target_strategy_code',
                self._get_target_code(self._current_set.target_strategy))
        elif data_name == 'gear_type':
            if ' ' in data_val:
                data_val = data_val.split(' ')[0]  # Extract value
                data_val = str(int(data_val))  # remove leading zero
            self._current_set.gear_type = data_val
        elif data_name == 'avg_soak_time':
            if ' ' in data_val:
                data_val = data_val.split(' ')[0]  # Extract value
                data_val = str(int(data_val))  # remove leading zero
            self._current_set.avg_soak_time = data_val
        else:
            logging.warning(
                'Attempt to set unknown data name: {}'.format(data_name))
            return
        self._current_set.save()
        self._set_cur_prop(data_name, data_val)
        logging.debug('Set {} to {}'.format(data_name, data_val))

        if data_name in ['tot_gear_segments']:
            self._update_set_hook_counts(self._current_set,
                                         self._current_trip.total_hooks_kp)
        self.modelChanged.emit()

    def _lookup_target_strat_id(self, strat: str):
        """
        Use first 4 chars as a code and look up CATCH_CATEGORY_ID
        @param strat: code + optional text
        @return: int ID
        """
        if strat is None or len(strat) < 3:
            self._logger.debug('Got bad strat ID to look up: {}'.format(strat))
            return

        find_code = strat.split(' ')[0]
        try:
            found = CatchCategories.get(CatchCategories.catch_category_code %
                                        find_code)
            self._logger.info('Found ID {} for {}'.format(
                found.catch_category, find_code))
            return found.catch_category
        except CatchCategories.DoesNotExist:
            self._logger.warning('Did not find ID for {}'.format(find_code))
            return None

    def _get_target_code(self, target_id: int):
        """
        Find pacfic code for id
        @param target_id: catch_category_id
        @return: int ID
        """
        if target_id is None:
            return ''
        try:
            found = CatchCategories.get(
                CatchCategories.catch_category == target_id)
            return found.catch_category_code
        except CatchCategories.DoesNotExist:
            self._logger.warning('Did not find code for {}'.format(target_id))
            return None

    @pyqtProperty(int, notify=maxObsRetModelLengthChanged)
    def maxObsRetModelLength(self):
        return self._obs_ret_max_count

    @pyqtProperty(int, notify=maxVesselRetModelLengthChanged)
    def maxVesselRetModelLength(self):
        return self._vessel_ret_max_count

    @pyqtProperty(QVariant, notify=unusedSignal)
    def requiredSetsFieldsAreSpecified(self):
        """
        Have all the prerequisite fields in the Haul Details screen been filled in for the current haul
        so that navigation to Catch screen is allowed?

        :return: True if gear type is not empty.
                False otherwise.
        """
        if not self.requireSeabird:
            req_fields = filter(lambda x: x != 'deterrent_used',
                                self._required_field_names)
        else:
            req_fields = self._required_field_names
        is_filled = [
            self.required_set_field_is_specified(f) for f in req_fields
        ]

        result = all(is_filled)

        # self._logger.debug("{}OK to navigate to Catch tab.".format("" if result else "Not "))

        return result and self.currentBiolistNum > 0

    @pyqtSlot(QVariant, result=QVariant, name='requiredSetFieldIsSpecified')
    def required_set_field_is_specified(self, field_name):
        """
            Expecting peewee Hauls field name, not name of text field in QML
            Return True if the field has a value, False otherwise.
            Raise exception if unknown field name.
        """
        if field_name not in self._required_field_names:
            raise Exception(
                f'Unexpected peewee Hauls field name: {field_name}')
        field_data = self.getData(field_name)
        if field_data is None:
            field_ok = False
        else:
            if isinstance(field_data, str):
                field_ok = len(field_data) > 0
            else:
                field_ok = True

        # self._logger.debug(f'{field_name} -> {field_data} is {"" if field_ok else "not "}specified.')
        return field_ok

    @pyqtProperty(QVariant, notify=unusedSignal)
    def isCalWeightSpecified(self):
        cal_wt = self.getData('cal_weight')
        if cal_wt:
            return True

        return False

    @pyqtSlot(QVariant, result=QVariant, name='getObserverRetModel')
    def getObserverRetModel(self, haul_db_id):
        """
        Load observer retained model
        @param haul_db_id: Database ID
        @return:
        """
        new_model = ObserverRetainedModel()
        new_model.load_observer_retained(haul_db_id, is_fixed_gear=True)
        if new_model.count > self._obs_ret_max_count:
            self._obs_ret_max_count = new_model.count
            self.maxObsRetModelLengthChanged.emit(self._obs_ret_max_count)
        self._obs_ret_models[haul_db_id] = new_model
        return self._obs_ret_models[haul_db_id]

    @pyqtSlot(QVariant, str, result=bool, name='ccIsInObserverRetModel')
    def ccIsInObserverRetModel(self, haul_db_id, catch_category_code):
        """
        Given a catch category and a haul ID, return whether
        this catch category is in the list of Observer-Retained catch categories.
        @param haul_db_id: Database ID
        @param catch_category_code: string (category code such as 'ALBC')
        @return: True if  catch_category_code is in list of observer-retained categories.
        """
        if haul_db_id not in self._obs_ret_models:
            self._logger.debug(
                'ccIsInObserverRetModel unexpectedly called before getObserverRetModel'
            )
            new_model = ObserverRetainedModel()
            new_model.load_observer_retained(haul_db_id)
            self._obs_ret_models[haul_db_id] = new_model

        observer_retained_model = self._obs_ret_models[haul_db_id]
        return observer_retained_model.is_item_in_model(
            'cc_code', catch_category_code)

    @pyqtSlot(QVariant, result=QVariant, name='getVesselRetModel')
    def getVesselRetModel(self, haul_db_id):
        new_model = VesselRetainedModel()
        new_model.load_vessel_retained(haul_db_id)
        self._update_vessel_ret_max(new_model)
        self._vessel_ret_models[haul_db_id] = new_model

        return self._vessel_ret_models[haul_db_id]

    @pyqtSlot(QVariant, QVariant, name='addVesselRetained')
    def addVesselRetained(self, haul_db_id, vessel_info):
        self._vessel_ret_models[haul_db_id].add_vessel_ret(
            haul_db_id, vessel_info)
        self._update_vessel_ret_max(self._vessel_ret_models[haul_db_id])

    def _update_vessel_ret_max(self, model):
        if model.count > self._vessel_ret_max_count:
            self._vessel_ret_max_count = model.count
            # self._logger.info('Vessel ret max now{}'.format(self._vessel_ret_max_count))
            self.maxVesselRetModelLengthChanged.emit(
                self._vessel_ret_max_count)

    @pyqtSlot(QVariant, result=bool, name='checkSetEmpty')
    def check_set_empty(self, haul_id):
        """
        Queries DB to see if there are data records associated with a haul
        @param haul_id: DBID
        @return: True if haul is empty and can be deleted, False otherwise
        """
        if not haul_id:
            self._logger.warning('Invalid haul ID passed to check_haul_empty.')
            return False

        catches_q = Catches.select().where(Catches.fishing_activity == haul_id)
        if len(catches_q) > 0:
            self._logger.debug(
                'Haul {} is not empty, has {} catches associated.'.format(
                    haul_id, len(catches_q)))
            return False
        return True

    @pyqtSlot(QVariant, result=int, name='createSet')
    def create_set(self, set_num):
        """
        @param set_num: ID local to this trip
        @return: haul db ID
        """
        self.load_sets(trip_id=self._trip_id)
        observer_id = ObserverDBUtil.get_current_user_id()
        newset = FishingActivities.create(
            trip=self._trip_id,
            fishing_activity_num=set_num,
            created_by=observer_id,
            created_date=ObserverDBUtil.get_arrow_datestr())
        logging.info('Created FishingActivities (set {}) for trip={}'.format(
            newset.fishing_activity_num, self._trip_id))
        self.SetsModel.add_set(newset)
        self.currentSetId = newset.fishing_activity_num
        return int(newset.fishing_activity)

    @pyqtSlot(QVariant, result=bool, name='deleteSet')
    def delete_set(self, set_id):
        if set_id is None or not self.check_set_empty(set_id):
            return

        trip_id = self._trip_id
        self._current_set = None  # FIELD-1817 always clear current haul ID if deleting

        # Delete from DB
        set = FishingActivities.get(
            FishingActivities.fishing_activity == set_id)
        ObserverDBUtil.log_peewee_model_instance(self._logger, set,
                                                 'Deleting haul')
        set.delete_instance(recursive=True)

        # Delete from model
        result = self._sets_model.remove_haul_set(set_id)

        # Check for hauls with greater fishing_activity_num # than this one, and if found, decrement them.
        renumber_hauls = FishingActivities.select().where(
            (FishingActivities.fishing_activity > set_id)
            & (FishingActivities.trip == trip_id))
        for h in renumber_hauls:
            old_num = h.fishing_activity_num
            h.fishing_activity_num -= 1
            h.save()
            fishing_number_row = self._sets_model.get_haul_set_index(
                h.fishing_activity)
            self._logger.info(
                f'Renumbered FISHING_ACTIVITY_NUM {old_num} to {h.fishing_activity_num} for haul {h.fishing_activity}'
            )
            self._sets_model.setProperty(fishing_number_row,
                                         'fishing_activity_num',
                                         h.fishing_activity_num)

        return result

    @pyqtProperty(QVariant, notify=currentBiolistNumChanged)
    def currentBiolistNum(self):
        return self._get_biolist_num()

    @currentBiolistNum.setter
    def currentBiolistNum(self, bio_num):
        self._current_biolist_num = bio_num
        self._default_biolist_num = bio_num  # this is used as new default
        if self._current_set:
            self._current_set.biolist_localonly = bio_num
            self._current_set.save()
        self._create_biolist_comment()

    def _create_biolist_comment(self):
        """
        Replaces _save_notes_biolist_num func
        Adds biolist string to comment table instead of saving directly to fishing_activity.notes.
        Comment record is picked up for comment parsing later (FIELD-2071)
        :return: None
        """
        if not self._current_set:
            self._logger.error(
                'Tried to save biolist num, but current set not set.')
            return
        BIOLIST_NOTE_PREFIX = 'Biolist #'
        APPSTATE = f"set_details_state::Set {self.currentSetId} Details"  # could make a param, but shouldn't change

        # check if biolist comment already exists
        existing_comments = Comment.select().where(
            (Comment.trip == self._current_set.trip)
            & (Comment.fishing_activity == self._current_set.fishing_activity)
            &
            (fn.Lower(Comment.comment).contains(BIOLIST_NOTE_PREFIX.lower())))

        if not existing_comments:
            Comment.create(
                username=ObserverDBUtil.get_setting('current_user'),
                comment_date=ObserverDBUtil.get_arrow_datestr(),
                comment=f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum}",
                appstateinfo=APPSTATE,
                trip=self._current_set.trip,
                fishing_activity=self._current_set.fishing_activity)
            self._logger.info(
                f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum} comment created."
            )
        else:  # if biolist comment already exists, and user changes it, update it
            query = Comment.update(
                username=ObserverDBUtil.get_setting('current_user'),
                comment_date=ObserverDBUtil.get_arrow_datestr(),
                comment=f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum}",
                appstateinfo=APPSTATE,
            ).where((
                Comment.fishing_activity == self._current_set.fishing_activity)
                    & (Comment.trip == self._current_set.trip)
                    & (Comment.comment.regexp('^' + BIOLIST_NOTE_PREFIX +
                                              '\d+$')
                       )  # starts with Biolist #, ends with nums
                    )
            query.execute()
            self._logger.info(
                f"{BIOLIST_NOTE_PREFIX}{self.currentBiolistNum} comment updated."
            )

    @pyqtSlot(name='updateBiolistNote')
    def _save_notes_biolist_num(self):
        """
        Store BIOLIST num to notes
        NOTE: most functionality here has been replaced by self._create_biolist_comment
        :return:
        """
        if not self._current_set:
            self._logger.error(
                'Tried to save biolist num, but current set not indicated.')
            return
        notes = self._current_set.notes if self._current_set.notes else ''
        BIOLIST_NOTE_PREFIX = 'Biolist #'
        bio_location = notes.find(BIOLIST_NOTE_PREFIX)
        if bio_location == -1:
            notes = f'{notes} {BIOLIST_NOTE_PREFIX}{self.currentBiolistNum}'
            self._current_set.notes = notes
            self._current_set.save()
            self._logger.debug(f'Saved Notes: {notes}')
        else:
            # exists... update with right number
            if notes.find(BIOLIST_NOTE_PREFIX +
                          str(self.currentBiolistNum)) == -1:
                newnotes = notes[:bio_location] + 'Biolist #' + str(
                    self.currentBiolistNum)
                self._current_set.notes = newnotes
                self._current_set.save()
                self._logger.debug(f'Updated Notes: {newnotes}')
            pass

    def _get_biolist_num(self):
        """
        For Sets,
        4 = Nearshore Fixed Gear (NSFG)
        5 = Non-Nearshort Fixed Gear (Non-NSFG)
        """
        if self._current_set and self._current_set.biolist_localonly:
            self._default_biolist_num = self._current_set.biolist_localonly  # this is used as new default
            return self._current_set.biolist_localonly
        else:
            return self._current_biolist_num

    @staticmethod
    def get_all_sets_with_gear_segments(trip_id):
        return FishingActivities.select().where(
            (FishingActivities.trip == trip_id)
            & (FishingActivities.tot_gear_segments.is_null(False)
               | FishingActivities.gear_segments_lost.is_null(False)))
示例#6
0
 def reset(self):
     """
     Clear sets, reset states
     """
     self._current_set = None
     self._sets_model = HaulSetModel()