class MockMilleniumPatronAPI(MilleniumPatronAPI): """This mocks the API on a higher level than the HTTP level. It is not used in the tests of the MilleniumPatronAPI class. It is used in the Adobe Vendor ID tests but maybe it shouldn't. """ # This user's card has expired. user1 = PatronData( permanent_id="12345", authorization_identifier="0", username="******", authorization_expires = datetime.datetime(2015, 4, 1) ) # This user's card still has ten days on it. the_future = datetime.datetime.utcnow() + datetime.timedelta(days=10) user2 = PatronData( permanent_id="67890", authorization_identifier="5", username="******", authorization_expires = the_future, ) users = [user1, user2] def __init__(self): pass def remote_authenticate(self, barcode, pin): """A barcode that's 14 digits long is treated as valid, no matter which PIN is used. That's so real barcode/PIN combos can be passed through to third parties. Otherwise, valid test PIN is the first character of the barcode repeated four times. """ u = self.dump(barcode) if 'ERRNUM' in u: return False return len(barcode) == 14 or pin == barcode[0] * 4 def remote_patron_lookup(self, patron_or_patrondata): # We have a couple custom barcodes. look_for = patron_or_patrondata.authorization_identifier for u in self.users: if u.authorization_identifier == look_for: return u return None
def remote_authenticate(self, username, password): """Does the Millenium Patron API approve of these credentials? :return: False if the credentials are invalid. If they are valid, a PatronData that serves only to indicate which authorization identifier the patron prefers. """ if self.auth_mode == self.PIN_AUTHENTICATION_MODE: path = "%(barcode)s/%(pin)s/pintest" % dict( barcode=username, pin=password ) url = self.root + path response = self.request(url) data = dict(self._extract_text_nodes(response.content)) if data.get('RETCOD') == '0': return PatronData(authorization_identifier=username, complete=False) return False elif self.auth_mode == self.FAMILY_NAME_AUTHENTICATION_MODE: patrondata = self._remote_patron_lookup(username) if not patrondata: # The patron doesn't even exist. return False # The patron exists; but do the last names match? if self.family_name_match(patrondata.personal_name, password): # Since this is a complete PatronData, we'll be able # to update their account without making a separate # call to /dump. return patrondata return False
def remote_authenticate(self, username, password): if not username or not password: return None now = datetime.datetime.utcnow() one_day = datetime.timedelta(days=1) patrondata = PatronData(authorization_identifier=username, permanent_id=username + "_id", username=username + "_username") if self.valid_patron(username, password, self.patrons): # The patron's authorization expires tomorrow. patrondata.authorization_expires = now + one_day elif self.valid_patron(username, password, self.expired_patrons): # The patron's authorization expired yesterday. patrondata.authorization_expires = now - one_day elif self.valid_patron(username, password, self.patrons_with_fines): # The patron has racked up huge fines. patrondata.fines = Decimal(12345678.90) else: return None return patrondata
def remote_authenticate(self, username, password): # All FirstBook credentials are in upper-case. username = username.upper() # If they fail a PIN test, there is no authenticated patron. if not self.remote_pin_test(username, password): return None # FirstBook keeps track of absolutely no information # about the patron other than the permanent ID, # which is also the authorization identifier. return PatronData( permanent_id=username, authorization_identifier=username, )
def remote_authenticate(self, username, password): """Does the Millenium Patron API approve of these credentials? :return: False if the credentials are invalid. If they are valid, a PatronData that serves only to indicate which authorization identifier the patron prefers. """ path = "%(barcode)s/%(pin)s/pintest" % dict(barcode=username, pin=password) url = self.root + path response = self.request(url) data = dict(self._extract_text_nodes(response.content)) if data.get('RETCOD') == '0': return PatronData(authorization_identifier=username, complete=False) return False
def remote_authenticate(self, username, password): # Create XML doc for request authorization_request = self.create_authorize_request(username, password) # Post request to the server response = self.post_request(authorization_request) # Parse response from server authorized, patron_name, library_identifier = self.parse_authorize_response(response.content) if not authorized: return False # Kansas auth gives very little data about the patron. Only name and a library identifier. return PatronData( permanent_id=username, authorization_identifier=username, personal_name=patron_name, library_identifier=library_identifier, complete=True )
def remote_authenticate(self, username, password): "Fake 'remote' authentication." if not username or not password: return None if not self.valid_patron(username, password): return None username = self.test_identifier patrondata = PatronData( authorization_identifier=username, permanent_id=username + "_id", username=username + "_username", authorization_expires=None, fines=None, ) return patrondata
def generate_patrondata(cls, authorization_identifier): if authorization_identifier.endswith("_username"): username = authorization_identifier identifier = authorization_identifier[:-9] else: identifier = authorization_identifier username = authorization_identifier + "_username" personal_name = "PersonalName" + identifier patrondata = PatronData( authorization_identifier=identifier, permanent_id=identifier + "_id", username=username, personal_name=personal_name, authorization_expires=None, fines=None, ) return patrondata
def remote_authenticate(self, username, password): "Fake 'remote' authentication." if not username or (self.collects_password and not password): return None if not self.valid_patron(username, password): return None if username.endswith("_username"): username = username identifier = username[:-9] else: identifier = username username = identifier + "_username" patrondata = PatronData( authorization_identifier=identifier, permanent_id=identifier + "_id", username=username, authorization_expires=None, fines=None, ) return patrondata
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) # 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.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 # 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, complete=True) return data