def test_look_up_zip(self): validator = GeographicValidator() us_zip_unformatted = validator.look_up_zip("06759", "US") assert isinstance(us_zip_unformatted, uszipcode.SimpleZipcode) us_zip_formatted = validator.look_up_zip("06759", "US", True) eq_(us_zip_formatted, {'06759': u'Litchfield, CT'}) ca_zip_unformatted = validator.look_up_zip("R2V", "CA") assert isinstance(ca_zip_unformatted, pypostalcode.PostalCode) ca_zip_formatted = validator.look_up_zip("R2V", "CA", True) eq_(ca_zip_formatted, {'R2V': u'Winnipeg (Seven Oaks East), Manitoba'})
def test_is_zip(self): validator = GeographicValidator() assert validator.is_zip("06759", "US") == True assert validator.is_zip("J2S", "US") == False assert validator.is_zip("1234", "US") == False assert validator.is_zip("1a234", "US") == False assert validator.is_zip("J2S", "CA") == True assert validator.is_zip("06759", "CA") == False assert validator.is_zip("12S", "CA") == False # "J2S 0A1" is a legit Canadian zipcode, but pypostalcode, which we use for looking up Canadian zipcodes, # only takes the FSA (the first three characters). assert validator.is_zip("J2S 0A1", "CA") == False
def get_extra_geographic_information(self, value): validator = GeographicValidator() for country in value: zips = [x for x in value[country] if validator.is_zip(x, country)] other = [x for x in value[country] if not x in zips] zips_with_extra_info = [] for zip in zips: info = validator.look_up_zip(zip, country, formatted=True) zips_with_extra_info.append(info) value[country] = zips_with_extra_info + other return value
def test_is_zip(self): validator = GeographicValidator() eq_(validator.is_zip("06759", "US"), True) eq_(validator.is_zip("J2S", "US"), False) eq_(validator.is_zip("1234", "US"), False) eq_(validator.is_zip("1a234", "US"), False) eq_(validator.is_zip("J2S", "CA"), True) eq_(validator.is_zip("06759", "CA"), False) eq_(validator.is_zip("12S", "CA"), False) # "J2S 0A1" is a legit Canadian zipcode, but pypostalcode, which we use for looking up Canadian zipcodes, # only takes the FSA (the first three characters). eq_(validator.is_zip("J2S 0A1", "CA"), False)
def process_post(self, validators_by_type=None): self.require_system_admin() library = None is_new = False if validators_by_type is None: validators_by_type = dict() validators_by_type['geographic'] = GeographicValidator() validators_by_type['announcements'] = AnnouncementListValidator() library_uuid = flask.request.form.get("uuid") library = self.get_library_from_uuid(library_uuid) if isinstance(library, ProblemDetail): return library short_name = flask.request.form.get("short_name") short_name_not_unique = self.check_short_name_unique( library, short_name) if short_name_not_unique: return short_name_not_unique error = self.validate_form_fields() if error: return error if not library: (library, is_new) = self.create_library(short_name, library_uuid) else: self.require_library_manager(library) name = flask.request.form.get("name") if name: library.name = name if short_name: library.short_name = short_name configuration_settings = self.library_configuration_settings( library, validators_by_type) if isinstance(configuration_settings, ProblemDetail): return configuration_settings if is_new: # Now that the configuration settings are in place, create # a default set of lanes. create_default_lanes(self._db, library) return Response(unicode(library.uuid), 201) else: return Response(unicode(library.uuid), 200)
def test_ask_registry(self, monkeypatch): validator = GeographicValidator() registry_1 = "https://registry_1_url" registry_2 = "https://registry_2_url" registry_3 = "https://registry_3_url" registries = self._registries([registry_1, registry_2, registry_3], monkeypatch) true_response = MockRequestsResponse(200, content="{}") unknown_response = MockRequestsResponse(200, content='{"unknown": "place"}') ambiguous_response = MockRequestsResponse( 200, content='{"ambiguous": "place"}') problem_response = MockRequestsResponse(404) # Registry 1 knows about the place self.responses.append(true_response) response_1 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) assert response_1 == True assert len(self.requests) == 1 request_1 = self.requests.pop() assert ( request_1[0] == 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 says the place is unknown, but Registry 2 finds it. self.responses.append(true_response) self.responses.append(unknown_response) response_2 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) assert response_2 == True assert len(self.requests) == 2 request_2 = self.requests.pop() assert ( request_2[0] == 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() assert ( request_1[0] == 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry_1 says the place is ambiguous and Registry_2 says it's unknown, but Registry_3 finds it. self.responses.append(true_response) self.responses.append(unknown_response) self.responses.append(ambiguous_response) response_3 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) assert response_3 == True assert len(self.requests) == 3 request_3 = self.requests.pop() assert ( request_3[0] == 'https://registry_3_url/coverage?coverage={"CA": "Victoria, BC"}') request_2 = self.requests.pop() assert ( request_2[0] == 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() assert ( request_1[0] == 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 returns a problem detail, but Registry 2 finds the place self.responses.append(true_response) self.responses.append(problem_response) response_4 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) assert response_4 == True assert len(self.requests) == 2 request_2 = self.requests.pop() assert ( request_2[0] == 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() assert ( request_1[0] == 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 returns a problem detail and the other two registries can't find the place self.responses.append(unknown_response) self.responses.append(ambiguous_response) self.responses.append(problem_response) response_5 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) assert response_5.status_code == 502 assert (response_5.detail == "Unable to contact the registry at https://registry_1_url.") assert len(self.requests) == 3 request_3 = self.requests.pop() assert ( request_3[0] == 'https://registry_3_url/coverage?coverage={"CA": "Victoria, BC"}') request_2 = self.requests.pop() assert ( request_2[0] == 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() assert ( request_1[0] == 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}')
def test_find_location_through_registry(self): get = self.do_request test = self original_ask_registry = GeographicValidator().ask_registry class Mock(GeographicValidator): called_with = [] def mock_ask_registry(self, service_area_object, db): places = {"US": ["Chicago"], "CA": ["Victoria, BC"]} service_area_info = json.loads( urllib.parse.unquote(service_area_object)) nation = list(service_area_info.keys())[0] city_or_county = list(service_area_info.values())[0] if city_or_county == "ERROR": test.responses.append(MockRequestsResponse(502)) elif city_or_county in places[nation]: self.called_with.append(service_area_info) test.responses.append( MockRequestsResponse(200, content=json.dumps( dict(unknown=None, ambiguous=None)))) else: self.called_with.append(service_area_info) test.responses.append( MockRequestsResponse( 200, content=json.dumps( dict(unknown=[city_or_county])))) return original_ask_registry(service_area_object, db, get) mock = Mock() mock.ask_registry = mock.mock_ask_registry self._registry("https://registry_url") us_response = mock.find_location_through_registry("Chicago", self._db) assert len(mock.called_with) == 1 assert {"US": "Chicago"} == mock.called_with[0] assert us_response == "US" mock.called_with = [] ca_response = mock.find_location_through_registry( "Victoria, BC", self._db) assert len(mock.called_with) == 2 assert {"US": "Victoria, BC"} == mock.called_with[0] assert {"CA": "Victoria, BC"} == mock.called_with[1] assert ca_response == "CA" mock.called_with = [] nowhere_response = mock.find_location_through_registry( "Not a real place", self._db) assert len(mock.called_with) == 2 assert {"US": "Not a real place"} == mock.called_with[0] assert {"CA": "Not a real place"} == mock.called_with[1] assert nowhere_response == None error_response = mock.find_location_through_registry("ERROR", self._db) assert (error_response.detail == "Unable to contact the registry at https://registry_url.") assert error_response.status_code == 502
def test_format_as_string(self): # GeographicValidator.format_as_string just turns its output into JSON. value = {"CA": ["Victoria, BC"], "US": []} as_string = GeographicValidator().format_as_string(value) assert as_string == json.dumps(value)
def test_ask_registry(self): validator = GeographicValidator() registry_1 = self._registry("https://registry_1_url") registry_2 = self._registry("https://registry_2_url") registry_3 = self._registry("https://registry_3_url") true_response = MockRequestsResponse(200, content="{}") unknown_response = MockRequestsResponse(200, content='{"unknown": "place"}') ambiguous_response = MockRequestsResponse( 200, content='{"ambiguous": "place"}') problem_response = MockRequestsResponse(404) # Registry 1 knows about the place self.responses.append(true_response) response_1 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_1, True) eq_(len(self.requests), 1) request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 says the place is unknown, but Registry 2 finds it. self.responses.append(true_response) self.responses.append(unknown_response) response_2 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_2, True) eq_(len(self.requests), 2) request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry_1 says the place is ambiguous and Registry_2 says it's unknown, but Registry_3 finds it. self.responses.append(true_response) self.responses.append(unknown_response) self.responses.append(ambiguous_response) response_3 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_3, True) eq_(len(self.requests), 3) request_3 = self.requests.pop() eq_(request_3[0], 'https://registry_3_url/coverage?coverage={"CA": "Victoria, BC"}') request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 returns a problem detail, but Registry 2 finds the place self.responses.append(true_response) self.responses.append(problem_response) response_4 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_4, True) eq_(len(self.requests), 2) request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 returns a problem detail and the other two registries can't find the place self.responses.append(unknown_response) self.responses.append(ambiguous_response) self.responses.append(problem_response) response_5 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_5.status_code, 502) eq_(response_5.detail, "Unable to contact the registry at https://registry_1_url.") eq_(len(self.requests), 3) request_3 = self.requests.pop() eq_(request_3[0], 'https://registry_3_url/coverage?coverage={"CA": "Victoria, BC"}') request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}')
def test_ask_registry(self): validator = GeographicValidator() registry_1 = self._registry("https://registry_1_url") registry_2 = self._registry("https://registry_2_url") registry_3 = self._registry("https://registry_3_url") true_response = MockRequestsResponse(200, content="{}") unknown_response = MockRequestsResponse(200, content='{"unknown": "place"}') ambiguous_response = MockRequestsResponse(200, content='{"ambiguous": "place"}') problem_response = MockRequestsResponse(404) # Registry 1 knows about the place self.responses.append(true_response) response_1 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_1, True) eq_(len(self.requests), 1) request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 says the place is unknown, but Registry 2 finds it. self.responses.append(true_response) self.responses.append(unknown_response) response_2 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_2, True) eq_(len(self.requests), 2) request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry_1 says the place is ambiguous and Registry_2 says it's unknown, but Registry_3 finds it. self.responses.append(true_response) self.responses.append(unknown_response) self.responses.append(ambiguous_response) response_3 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_3, True) eq_(len(self.requests), 3) request_3 = self.requests.pop() eq_(request_3[0], 'https://registry_3_url/coverage?coverage={"CA": "Victoria, BC"}') request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 returns a problem detail, but Registry 2 finds the place self.responses.append(true_response) self.responses.append(problem_response) response_4 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_4, True) eq_(len(self.requests), 2) request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}') # Registry 1 returns a problem detail and the other two registries can't find the place self.responses.append(unknown_response) self.responses.append(ambiguous_response) self.responses.append(problem_response) response_5 = validator.ask_registry(json.dumps({"CA": "Victoria, BC"}), self._db, self.do_request) eq_(response_5.status_code, 502) eq_(response_5.detail, "Unable to contact the registry at https://registry_1_url.") eq_(len(self.requests), 3) request_3 = self.requests.pop() eq_(request_3[0], 'https://registry_3_url/coverage?coverage={"CA": "Victoria, BC"}') request_2 = self.requests.pop() eq_(request_2[0], 'https://registry_2_url/coverage?coverage={"CA": "Victoria, BC"}') request_1 = self.requests.pop() eq_(request_1[0], 'https://registry_1_url/coverage?coverage={"CA": "Victoria, BC"}')
def test_libraries_post_create(self): class TestFileUpload(StringIO): headers = {"Content-Type": "image/png"} image_data = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x06PLTE\xffM\x00\x01\x01\x01\x8e\x1e\xe5\x1b\x00\x00\x00\x01tRNS\xcc\xd24V\xfd\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82' original_validate = GeographicValidator().validate_geographic_areas class MockValidator(GeographicValidator): def __init__(self): self.was_called = False def validate_geographic_areas(self, values, db): self.was_called = True return original_validate(values, db) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "The New York Public Library"), ("short_name", "nypl"), ("library_description", "Short description of library"), (Configuration.WEBSITE_URL, "https://library.library/"), (Configuration.TINY_COLLECTION_LANGUAGES, ['ger']), (Configuration.LIBRARY_SERVICE_AREA, ['06759', 'everywhere', 'MD', 'Boston, MA']), (Configuration.LIBRARY_FOCUS_AREA, ['Manitoba', 'Broward County, FL', 'QC']), (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, "*****@*****.**"), (Configuration.HELP_EMAIL, "*****@*****.**"), (Configuration.FEATURED_LANE_SIZE, "5"), (Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, FacetConstants.ORDER_RANDOM), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_TITLE, ''), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_RANDOM, ''), ]) flask.request.files = MultiDict([ (Configuration.LOGO, TestFileUpload(image_data)), ]) validator = MockValidator() response = self.manager.admin_library_settings_controller.process_post( validator) eq_(response.status_code, 201) library = get_one(self._db, Library, short_name="nypl") eq_(library.uuid, response.response[0]) eq_(library.name, "The New York Public Library") eq_(library.short_name, "nypl") eq_( "5", ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, library).value) eq_( FacetConstants.ORDER_RANDOM, ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value) eq_( json.dumps( [FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]), ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value) eq_( "data:image/png;base64,%s" % base64.b64encode(image_data), ConfigurationSetting.for_library(Configuration.LOGO, library).value) eq_(validator.was_called, True) eq_( '{"CA": [], "US": ["06759", "everywhere", "MD", "Boston, MA"]}', ConfigurationSetting.for_library( Configuration.LIBRARY_SERVICE_AREA, library).value) eq_( '{"CA": ["Manitoba", "Quebec"], "US": ["Broward County, FL"]}', ConfigurationSetting.for_library(Configuration.LIBRARY_FOCUS_AREA, library).value) # When the library was created, default lanes were also created # according to its language setup. This library has one tiny # collection (not a good choice for a real library), so only # two lanes were created: "Other Languages" and then "German" # underneath it. [german, other_languages] = sorted(library.lanes, key=lambda x: x.display_name) eq_(None, other_languages.parent) eq_(['ger'], other_languages.languages) eq_(other_languages, german.parent) eq_(['ger'], german.languages)
def test_libraries_post_create(self, logo_properties): class TestFileUpload(BytesIO): headers = {"Content-Type": "image/png"} # Pull needed properties from logo fixture image_data, expected_logo_data_url, image = [ logo_properties[key] for key in ("raw_bytes", "data_url", "image") ] # LibrarySettingsController scales down images that are too large, # so we fail here if our test fixture image is large enough to cause # a mismatch between the expected data URL and the one configured. assert max(*image.size) <= Configuration.LOGO_MAX_DIMENSION original_geographic_validate = GeographicValidator( ).validate_geographic_areas class MockGeographicValidator(GeographicValidator): def __init__(self): self.was_called = False def validate_geographic_areas(self, values, db): self.was_called = True return original_geographic_validate(values, db) original_announcement_validate = ( AnnouncementListValidator().validate_announcements) class MockAnnouncementListValidator(AnnouncementListValidator): def __init__(self): self.was_called = False def validate_announcements(self, values): self.was_called = True return original_announcement_validate(values) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "The New York Public Library"), ("short_name", "nypl"), ("library_description", "Short description of library"), (Configuration.WEBSITE_URL, "https://library.library/"), (Configuration.TINY_COLLECTION_LANGUAGES, ["ger"]), ( Configuration.LIBRARY_SERVICE_AREA, ["06759", "everywhere", "MD", "Boston, MA"], ), ( Configuration.LIBRARY_FOCUS_AREA, ["Manitoba", "Broward County, FL", "QC"], ), ( Announcements.SETTING_NAME, json.dumps([self.active, self.forthcoming]), ), ( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, "*****@*****.**", ), (Configuration.HELP_EMAIL, "*****@*****.**"), (Configuration.FEATURED_LANE_SIZE, "5"), ( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, FacetConstants.ORDER_RANDOM, ), ( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_TITLE, "", ), ( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_RANDOM, "", ), ]) flask.request.files = MultiDict([ (Configuration.LOGO, TestFileUpload(image_data)), ]) geographic_validator = MockGeographicValidator() announcement_validator = MockAnnouncementListValidator() validators = dict( geographic=geographic_validator, announcements=announcement_validator, ) response = self.manager.admin_library_settings_controller.process_post( validators) assert response.status_code == 201 library = get_one(self._db, Library, short_name="nypl") assert library.uuid == response.get_data(as_text=True) assert library.name == "The New York Public Library" assert library.short_name == "nypl" assert ("5" == ConfigurationSetting.for_library( Configuration.FEATURED_LANE_SIZE, library).value) assert ( FacetConstants.ORDER_RANDOM == ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library, ).value) assert (json.dumps([FacetConstants.ORDER_TITLE ]) == ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library, ).value) assert (expected_logo_data_url == ConfigurationSetting.for_library( Configuration.LOGO, library).value) assert geographic_validator.was_called == True assert ('{"US": ["06759", "everywhere", "MD", "Boston, MA"], "CA": []}' == ConfigurationSetting.for_library( Configuration.LIBRARY_SERVICE_AREA, library).value) assert ('{"US": ["Broward County, FL"], "CA": ["Manitoba", "Quebec"]}' == ConfigurationSetting.for_library( Configuration.LIBRARY_FOCUS_AREA, library).value) # Announcements were validated. assert announcement_validator.was_called == True # The validated result was written to the database, such that we can # parse it as a list of Announcement objects. announcements = Announcements.for_library(library).announcements assert [self.active["id"], self.forthcoming["id"]] == [x.id for x in announcements] assert all(isinstance(x, Announcement) for x in announcements) # When the library was created, default lanes were also created # according to its language setup. This library has one tiny # collection (not a good choice for a real library), so only # two lanes were created: "Other Languages" and then "German" # underneath it. [german, other_languages] = sorted(library.lanes, key=lambda x: x.display_name) assert None == other_languages.parent assert ["ger"] == other_languages.languages assert other_languages == german.parent assert ["ger"] == german.languages