Esempio n. 1
0
 def max_outstanding_fines(cls, library):
     max_fines = ConfigurationSetting.for_library(
         cls.MAX_OUTSTANDING_FINES, library
     )
     if max_fines.value is None:
         return None
     return MoneyUtility.parse(max_fines.value)
    def test_has_excess_fines(self):
        # Test the has_excess_fines method.
        patron = self._patron()

        # If you accrue excessive fines you lose borrowing privileges.
        setting = ConfigurationSetting.for_library(
            Configuration.MAX_OUTSTANDING_FINES, self._default_library
        )

        # Verify that all these tests work no matter what data type has been stored in
        # patron.fines.
        for patron_fines in ("1", "0.75", 1, 1.0, Decimal(1), MoneyUtility.parse("1")):
            patron.fines = patron_fines

            # Test cases where the patron's fines exceed a well-defined limit,
            # or when any amount of fines is too much.
            for max_fines in ["$0.50", "0.5", 0.5] + [  # well-defined limit
                "$0",
                "$0.00",
                "0",
                0,
            ]:  # any fines is too much
                setting.value = max_fines
                assert True == PatronUtility.has_excess_fines(patron)

            # Test cases where the patron's fines are below a
            # well-defined limit, or where fines are ignored
            # altogether.
            for max_fines in ["$100", 100] + [  # well-defined-limit
                None,
                "",
            ]:  # fines ignored
                setting.value = max_fines
                assert False == PatronUtility.has_excess_fines(patron)

        # Test various cases where fines in any amount deny borrowing
        # privileges, but the patron has no fines.
        for patron_fines in ("0", "$0", 0, None, MoneyUtility.parse("$0")):
            patron.fines = patron_fines
            for max_fines in ["$0", "$0.00", "0", 0]:
                setting.value = max_fines
                assert False == PatronUtility.has_excess_fines(patron)
Esempio n. 3
0
    def has_excess_fines(cls, patron):
        """Does this patron have fines in excess of the maximum fine amount set for their library?

        :param a Patron:
        :return: A boolean
        """
        if not patron.fines:
            return False

        if isinstance(patron.fines, Money):
            patron_fines = patron.fines
        else:
            patron_fines = MoneyUtility.parse(patron.fines)
        actual_fines = patron_fines.amount
        max_fines = Configuration.max_outstanding_fines(patron.library)
        if max_fines is not None and actual_fines > max_fines.amount:
            return True
        return False
Esempio n. 4
0
    def info_to_patrondata(cls, info):
        """Convert the SIP-specific dictionary obtained from
        SIPClient.patron_information() to an abstract,
        authenticator-independent PatronData object.
        """
        if info.get('valid_patron_password') == 'N':
            # The patron did not authenticate correctly. Don't
            # return any data.
            return None

            # TODO: I'm not 100% convinced that a missing CQ field
            # always means "we don't have passwords so you're
            # authenticated," rather than "you didn't provide a
            # password so we didn't check."
        patrondata = PatronData()
        if 'sipserver_internal_id' in info:
            patrondata.permanent_id = info['sipserver_internal_id']
        if 'patron_identifier' in info:
            patrondata.authorization_identifier = info['patron_identifier']
        if 'email_address' in info:
            patrondata.email_address = info['email_address']
        if 'personal_name' in info:
            patrondata.personal_name = info['personal_name']
        if 'fee_amount' in info:
            fines = info['fee_amount']
        else:
            fines = '0'
        patrondata.fines = MoneyUtility.parse(fines)
        if 'sipserver_patron_class' in info:
            patrondata.external_type = info['sipserver_patron_class']
        for expire_field in [
                'sipserver_patron_expiration', 'polaris_patron_expiration'
        ]:
            if expire_field in info:
                value = info.get(expire_field)
                value = cls.parse_date(value)
                if value:
                    patrondata.authorization_expires = value
                    break
        return patrondata
Esempio n. 5
0
    def info_to_patrondata(cls, info):
        """Convert the SIP-specific dictionary obtained from
        SIPClient.patron_information() to an abstract,
        authenticator-independent PatronData object.
        """
        if info.get('valid_patron_password') == 'N':
            # The patron did not authenticate correctly. Don't
            # return any data.
            return None

            # TODO: I'm not 100% convinced that a missing CQ field
            # always means "we don't have passwords so you're
            # authenticated," rather than "you didn't provide a
            # password so we didn't check."
        patrondata = PatronData()
        if 'sipserver_internal_id' in info:
            patrondata.permanent_id = info['sipserver_internal_id']
        if 'patron_identifier' in info:
            patrondata.authorization_identifier = info['patron_identifier']
        if 'email_address' in info:
            patrondata.email_address = info['email_address']
        if 'personal_name' in info:
            patrondata.personal_name = info['personal_name']
        if 'fee_amount' in info:
            fines = info['fee_amount']
        else:
            fines = '0'
        patrondata.fines = MoneyUtility.parse(fines)
        if 'sipserver_patron_class' in info:
            patrondata.external_type = info['sipserver_patron_class']
        for expire_field in [
                'sipserver_patron_expiration', 'polaris_patron_expiration'
        ]:
            if expire_field in info:
                value = info.get(expire_field)
                value = cls.parse_date(value)
                if value:
                    patrondata.authorization_expires = value
                    break

        # A True value in most (but not all) subfields of the
        # patron_status field will prohibit the patron from borrowing
        # books.
        status = info['patron_status_parsed']
        block_reason = PatronData.NO_VALUE
        for field in SIPClient.PATRON_STATUS_FIELDS_THAT_DENY_BORROWING_PRIVILEGES:
            if status.get(field) is True:
                block_reason = cls.SPECIFIC_BLOCK_REASONS.get(
                    field, PatronData.UNKNOWN_BLOCK)
                if block_reason not in (PatronData.NO_VALUE,
                                        PatronData.UNKNOWN_BLOCK):
                    # Even if there are multiple problems with this
                    # patron's account, we can now present a specific
                    # error message. There's no need to look through
                    # more fields.
                    break
        patrondata.block_reason = block_reason

        # If we can tell by looking at the SIP2 message that the
        # patron has excessive fines, we can use that as the reason
        # they're blocked.
        if 'fee_limit' in info:
            fee_limit = MoneyUtility.parse(info['fee_limit']).amount
            if fee_limit and patrondata.fines > fee_limit:
                patrondata.block_reason = PatronData.EXCESSIVE_FINES

        return patrondata
Esempio n. 6
0
    def patron_dump_to_patrondata(self, current_identifier, content):
        """Convert an HTML patron dump to a PatronData object.

        :param current_identifier: Either the authorization identifier
            the patron just logged in with, or the one currently
            associated with their Patron record. Keeping track of this
            ensures we don't change a patron's preferred authorization
            identifier out from under them.

        :param content: The HTML document containing the patron dump.
        """
        # If we don't see these fields, erase any previous value
        # rather than leaving the old value in place. This shouldn't
        # happen (unless the expiration date changes to an invalid
        # date), but just to be safe.
        permanent_id = PatronData.NO_VALUE
        username = authorization_expires = personal_name = PatronData.NO_VALUE
        email_address = fines = external_type = PatronData.NO_VALUE
        block_reason = PatronData.NO_VALUE
        neighborhood = PatronData.NO_VALUE

        potential_identifiers = []
        for k, v in self._extract_text_nodes(content):
            if k == self.BARCODE_FIELD:
                if any(x.search(v) for x in self.blacklist):
                    # This barcode contains a blacklisted
                    # string. Ignore it, even if this means the patron
                    # ends up with no barcode whatsoever.
                    continue
                # We'll figure out which barcode is the 'right' one
                # later.
                potential_identifiers.append(v)
                # The millenium API doesn't care about spaces, so we add
                # a version of the barcode without spaces to our identifers
                # list as well.
                if " " in v:
                    potential_identifiers.append(v.replace(" ", ""))
            elif k == self.RECORD_NUMBER_FIELD:
                permanent_id = v
            elif k == self.USERNAME_FIELD:
                username = v
            elif k == self.PERSONAL_NAME_FIELD:
                personal_name = v
            elif k == self.EMAIL_ADDRESS_FIELD:
                email_address = v
            elif k == self.FINES_FIELD:
                try:
                    fines = MoneyUtility.parse(v)
                except ValueError:
                    self.log.warning(
                        'Malformed fine amount for patron: "%s". Treating as no fines.'
                    )
                    fines = Money("0", "USD")
            elif k == self.BLOCK_FIELD:
                block_reason = self._patron_block_reason(self.block_types, v)
            elif k == self.EXPIRATION_FIELD:
                try:
                    # Parse the expiration date according to server local
                    # time, not UTC.
                    expires_local = datetime.datetime.strptime(
                        v, self.EXPIRATION_DATE_FORMAT).replace(
                            tzinfo=dateutil.tz.tzlocal())
                    expires_local = expires_local.date()
                    authorization_expires = expires_local
                except ValueError:
                    self.log.warning(
                        'Malformed expiration date for patron: "%s". Treating as unexpirable.',
                        v,
                    )
            elif k == self.PATRON_TYPE_FIELD:
                external_type = v
            elif (k == self.HOME_BRANCH_FIELD and self.neighborhood_mode
                  == self.HOME_BRANCH_NEIGHBORHOOD_MODE):
                neighborhood = v.strip()
            elif (k == self.ADDRESS_FIELD and self.neighborhood_mode
                  == self.POSTAL_CODE_NEIGHBORHOOD_MODE):
                neighborhood = self.extract_postal_code(v)
            elif k == self.ERROR_MESSAGE_FIELD:
                # An error has occured. Most likely the patron lookup
                # failed.
                return None

        # Set the library identifier field
        library_identifier = None
        for k, v in self._extract_text_nodes(content):
            if k == self.library_identifier_field:
                library_identifier = v.strip()

        # We may now have multiple authorization
        # identifiers. PatronData expects the best authorization
        # identifier to show up first in the list.
        #
        # The last identifier in the list is probably the most recently
        # added one. In the absence of any other information, it's the
        # one we should choose.
        potential_identifiers.reverse()

        authorization_identifiers = potential_identifiers
        if not authorization_identifiers:
            authorization_identifiers = PatronData.NO_VALUE
        elif current_identifier in authorization_identifiers:
            # Don't rock the boat. The patron is used to using this
            # identifier and there's no need to change it. Move the
            # currently used identifier to the front of the list.
            authorization_identifiers.remove(current_identifier)
            authorization_identifiers.insert(0, current_identifier)

        data = PatronData(
            permanent_id=permanent_id,
            authorization_identifier=authorization_identifiers,
            username=username,
            personal_name=personal_name,
            email_address=email_address,
            authorization_expires=authorization_expires,
            external_type=external_type,
            fines=fines,
            block_reason=block_reason,
            library_identifier=library_identifier,
            neighborhood=neighborhood,
            # We must cache neighborhood information in the patron's
            # database record because syncing with the ILS is so
            # expensive.
            cached_neighborhood=neighborhood,
            complete=True,
        )
        return data
Esempio n. 7
0
 def max_outstanding_fines(cls):
     max_fines = Configuration.policy(Configuration.MAX_OUTSTANDING_FINES)
     return MoneyUtility.parse(max_fines)
Esempio n. 8
0
    def patron_dump_to_patrondata(self, current_identifier, content):
        """Convert an HTML patron dump to a PatronData object.
        
        :param current_identifier: Either the authorization identifier
        the patron just logged in with, or the one currently
        associated with their Patron record. Keeping track of this
        ensures we don't change a patron's preferred authorization
        identifier out from under them.

        :param content: The HTML document containing the patron dump.
        """       
        # If we don't see these fields, erase any previous value
        # rather than leaving the old value in place. This shouldn't
        # happen (unless the expiration date changes to an invalid
        # date), but just to be safe.
        permanent_id = PatronData.NO_VALUE
        username = authorization_expires = personal_name = PatronData.NO_VALUE
        email_address = fines = external_type = PatronData.NO_VALUE
        block_reason = PatronData.NO_VALUE
        
        potential_identifiers = []
        for k, v in self._extract_text_nodes(content):
            if k == self.BARCODE_FIELD:
                if any(x.search(v) for x in self.blacklist):
                    # This barcode contains a blacklisted
                    # string. Ignore it, even if this means the patron
                    # ends up with no barcode whatsoever.
                    continue
                # We'll figure out which barcode is the 'right' one
                # later.
                potential_identifiers.append(v)
            elif k == self.RECORD_NUMBER_FIELD:
                permanent_id = v
            elif k == self.USERNAME_FIELD:
                username = v
            elif k == self.PERSONAL_NAME_FIELD:
                personal_name = v
            elif k == self.EMAIL_ADDRESS_FIELD:
                email_address = v
            elif k == self.FINES_FIELD:
                try:
                    fines = MoneyUtility.parse(v)
                except ValueError:
                    self.log.warn(
                        'Malformed fine amount for patron: "%s". Treating as no fines.'
                    )
                    fines = Money("0", "USD")
            elif k == self.BLOCK_FIELD:
                block_reason = self._patron_block_reason(self.block_types, v)
            elif k == self.EXPIRATION_FIELD:
                try:
                    expires = datetime.datetime.strptime(
                        v, self.EXPIRATION_DATE_FORMAT).date()
                    authorization_expires = expires
                except ValueError:
                    self.log.warn(
                        'Malformed expiration date for patron: "%s". Treating as unexpirable.',
                        v
                    )
            elif k == self.PATRON_TYPE_FIELD:
                external_type = v
            elif k == self.ERROR_MESSAGE_FIELD:
                # An error has occured. Most likely the patron lookup
                # failed.
                return None

        # We may now have multiple authorization
        # identifiers. PatronData expects the best authorization
        # identifier to show up first in the list.
        #
        # The last identifier in the list is probably the most recently
        # added one. In the absence of any other information, it's the
        # one we should choose.
        potential_identifiers.reverse()
        
        authorization_identifiers = potential_identifiers
        if not authorization_identifiers:
            authorization_identifiers = PatronData.NO_VALUE
        elif current_identifier in authorization_identifiers:
            # Don't rock the boat. The patron is used to using this
            # identifier and there's no need to change it. Move the
            # currently used identifier to the front of the list.
            authorization_identifiers.remove(current_identifier)
            authorization_identifiers.insert(0, current_identifier)

        data = PatronData(
            permanent_id=permanent_id,
            authorization_identifier=authorization_identifiers,
            username=username,
            personal_name=personal_name,
            email_address=email_address,
            authorization_expires=authorization_expires,
            external_type=external_type,
            fines=fines,
            block_reason=block_reason,
            complete=True
        )
        return data
Esempio n. 9
0
 def max_outstanding_fines(cls, library):
     max_fines = ConfigurationSetting.for_library(cls.MAX_OUTSTANDING_FINES,
                                                  library)
     if max_fines.value is None:
         return None
     return MoneyUtility.parse(max_fines.value)
Esempio n. 10
0
    def info_to_patrondata(self, info, validate_password=True):

        """Convert the SIP-specific dictionary obtained from
        SIPClient.patron_information() to an abstract,
        authenticator-independent PatronData object.
        """
        if info.get("valid_patron", "N") == "N":
            # The patron could not be identified as a patron of this
            # library. Don't return any data.
            return None

        if info.get("valid_patron_password") == "N" and validate_password:
            # The patron did not authenticate correctly. Don't
            # return any data.
            return None

            # TODO: I'm not 100% convinced that a missing CQ field
            # always means "we don't have passwords so you're
            # authenticated," rather than "you didn't provide a
            # password so we didn't check."
        patrondata = PatronData()
        if "sipserver_internal_id" in info:
            patrondata.permanent_id = info["sipserver_internal_id"]
        if "patron_identifier" in info:
            patrondata.authorization_identifier = info["patron_identifier"]
        if "email_address" in info:
            patrondata.email_address = info["email_address"]
        if "personal_name" in info:
            patrondata.personal_name = info["personal_name"]
        if "fee_amount" in info:
            fines = info["fee_amount"]
        else:
            fines = "0"
        patrondata.fines = MoneyUtility.parse(fines)
        if "sipserver_patron_class" in info:
            patrondata.external_type = info["sipserver_patron_class"]
        for expire_field in [
            "sipserver_patron_expiration",
            "polaris_patron_expiration",
        ]:
            if expire_field in info:
                value = info.get(expire_field)
                value = self.parse_date(value)
                if value:
                    patrondata.authorization_expires = value
                    break

        # A True value in most (but not all) subfields of the
        # patron_status field will prohibit the patron from borrowing
        # books.
        status = info["patron_status_parsed"]
        block_reason = PatronData.NO_VALUE
        for field in self.fields_that_deny_borrowing:
            if status.get(field) is True:
                block_reason = self.SPECIFIC_BLOCK_REASONS.get(
                    field, PatronData.UNKNOWN_BLOCK
                )
                if block_reason not in (PatronData.NO_VALUE, PatronData.UNKNOWN_BLOCK):
                    # Even if there are multiple problems with this
                    # patron's account, we can now present a specific
                    # error message. There's no need to look through
                    # more fields.
                    break
        patrondata.block_reason = block_reason

        # If we can tell by looking at the SIP2 message that the
        # patron has excessive fines, we can use that as the reason
        # they're blocked.
        if "fee_limit" in info:
            fee_limit = MoneyUtility.parse(info["fee_limit"]).amount
            if fee_limit and patrondata.fines > fee_limit:
                patrondata.block_reason = PatronData.EXCESSIVE_FINES

        return patrondata