diff --git a/passerelle/contrib/toulouse_maelis/activity_schemas.py b/passerelle/contrib/toulouse_maelis/activity_schemas.py index 1575a3a2..58584a11 100644 --- a/passerelle/contrib/toulouse_maelis/activity_schemas.py +++ b/passerelle/contrib/toulouse_maelis/activity_schemas.py @@ -145,15 +145,33 @@ SUBSCRIPTION_SCHEMA = { 'unflatten': True, } -DELETE_BASKET_LINE_SCHEMA = { +BASKET_SCHEMA = { 'type': 'object', 'properties': { - 'line_id': { + 'basket_id': { 'type': 'string', - 'pattern': '[A-Za-z0-9]+', + 'pattern': '^[A-Za-z0-9]+$', }, }, 'required': [ + 'basket_id', + ], +} + +BASKET_LINE_SCHEMA = { + 'type': 'object', + 'properties': { + 'basket_id': { + 'type': 'string', + 'pattern': '^[A-Za-z0-9]+$', + }, + 'line_id': { + 'type': 'string', + 'pattern': '^[A-Za-z0-9]+$', + }, + }, + 'required': [ + 'basket_id', 'line_id', ], } diff --git a/passerelle/contrib/toulouse_maelis/models.py b/passerelle/contrib/toulouse_maelis/models.py index 4e93b871..0b3d28a0 100644 --- a/passerelle/contrib/toulouse_maelis/models.py +++ b/passerelle/contrib/toulouse_maelis/models.py @@ -199,12 +199,17 @@ class ToulouseMaelis(BaseResource, HTTPResource): nurseries = self.call('Ape', 'readNurseryList', request={}) self.update_referential('Nursery', nurseries, 'idActivity', 'libelle') + def update_invoice_referentials(self): + data = self.get_referential_data('Invoice', 'Regie') + self.update_referential('Regie', data, 'code', 'libelle') + def daily(self): try: self.update_family_referentials() self.update_site_referentials() self.update_activity_referentials() self.update_ape_referentials() + self.update_invoice_referentials() except UpdateError as e: self.logger.warning('Erreur sur la mise à jour: %s' % e) @@ -565,15 +570,24 @@ class ToulouseMaelis(BaseResource, HTTPResource): ] return data - def get_basket_raw(self, family_id): - return self.call( - 'Activity', - 'getFamilyBasket', - getFamilyBasketRequestBean={ - 'numFamily': family_id, - }, + def get_baskets_raw(self, family_id): + return ( + self.call( + 'Activity', + 'getFamilyBasket', + getFamilyBasketRequestBean={ + 'numFamily': family_id, + }, + ) + or [] ) + def get_basket_raw(self, family_id, basket_id): + for basket in self.get_baskets_raw(family_id): + if basket['id'] == basket_id: + return basket + raise APIError("no '%s' basket on family" % basket_id) + @endpoint( display_category='Famille', description='Lister les catégories', @@ -3085,17 +3099,20 @@ class ToulouseMaelis(BaseResource, HTTPResource): @endpoint( display_category='Inscriptions', - description="Lecture du panier de la famille", - name='get-basket', + description="Obtenir les paniers de la famille", + name='get-baskets', perm='can_access', parameters={ 'NameID': {'description': 'Publik NameID'}, 'family_id': {'description': 'Numéro de DUI'}, }, ) - def get_basket(self, request, NameID=None, family_id=None): + def get_baskets(self, request, NameID=None, family_id=None): family_id = family_id or self.get_link(NameID).family_id - return {'data': self.get_basket_raw(family_id)} + baskets = self.get_baskets_raw(family_id) + for item in baskets: + item['text'] = self.get_referential_value('Regie', item['codeRegie']) + return {'data': baskets} @endpoint( display_category='Inscriptions', @@ -3106,15 +3123,13 @@ class ToulouseMaelis(BaseResource, HTTPResource): 'NameID': {'description': 'Publik NameID'}, 'family_id': {'description': 'Numéro de DUI'}, }, - methods=['post'], + post={'request_body': {'schema': {'application/json': activity_schemas.BASKET_SCHEMA}}}, ) - def update_basket_time(self, request, NameID=None, family_id=None): + def update_basket_time(self, request, post_data, NameID=None, family_id=None): family_id = family_id or self.get_link(NameID).family_id - basket = self.get_basket_raw(family_id) - if not basket or not basket.get('id'): - raise APIError("no basket on '%s' family" % family_id) + self.get_basket_raw(family_id, post_data['basket_id']) - self.call('Activity', 'updateBasketTime', idBasket=basket['id']) + self.call('Activity', 'updateBasketTime', idBasket=post_data['basket_id']) return {'data': 'ok'} @endpoint( @@ -3126,25 +3141,22 @@ class ToulouseMaelis(BaseResource, HTTPResource): 'NameID': {'description': 'Publik NameID'}, 'family_id': {'description': 'Numéro de DUI'}, }, - post={'request_body': {'schema': {'application/json': activity_schemas.DELETE_BASKET_LINE_SCHEMA}}}, + post={'request_body': {'schema': {'application/json': activity_schemas.BASKET_LINE_SCHEMA}}}, ) def delete_basket_line(self, request, post_data, NameID=None, family_id=None): family_id = family_id or self.get_link(NameID).family_id - basket = self.get_basket_raw(family_id) - line_id = post_data['line_id'] - if not basket or not basket.get('id'): - raise APIError("no basket on '%s' family" % family_id) + basket = self.get_basket_raw(family_id, post_data['basket_id']) for line in basket['lignes']: - if line['id'] == line_id: + if line['id'] == post_data['line_id']: break else: - raise APIError("no '%s' basket line on '%s' family" % (line_id, family_id)) + raise APIError("no '%s' basket line on basket" % post_data['line_id']) response = self.call( 'Activity', 'deletePersonUnitBasket', deletePersonUnitBasketRequestBean={ - 'idBasketLine': line_id, + 'idBasketLine': post_data['line_id'], }, ) return {'data': response} @@ -3158,19 +3170,17 @@ class ToulouseMaelis(BaseResource, HTTPResource): 'NameID': {'description': 'Publik NameID'}, 'family_id': {'description': 'Numéro de DUI'}, }, - methods=['post'], + post={'request_body': {'schema': {'application/json': activity_schemas.BASKET_SCHEMA}}}, ) - def delete_basket(self, request, NameID=None, family_id=None): + def delete_basket(self, request, post_data, NameID=None, family_id=None): family_id = family_id or self.get_link(NameID).family_id - basket = self.get_basket_raw(family_id) - if not basket or not basket.get('id'): - raise APIError("no basket on '%s' family" % family_id) + self.get_basket_raw(family_id, post_data['basket_id']) self.call( 'Activity', 'deleteBasket', deleteBasketRequestBean={ - 'idBasket': basket['id'], + 'idBasket': post_data['basket_id'], 'idUtilisat': NameID or 'Middle-office', }, ) @@ -3185,19 +3195,17 @@ class ToulouseMaelis(BaseResource, HTTPResource): 'NameID': {'description': 'Publik NameID'}, 'family_id': {'description': 'Numéro de DUI'}, }, - methods=['post'], + post={'request_body': {'schema': {'application/json': activity_schemas.BASKET_SCHEMA}}}, ) - def validate_basket(self, request, NameID=None, family_id=None): + def validate_basket(self, request, post_data, NameID=None, family_id=None): family_id = family_id or self.get_link(NameID).family_id - basket = self.get_basket_raw(family_id) - if not basket or not basket.get('id'): - raise APIError("no basket on '%s' family" % family_id) + self.get_basket_raw(family_id, post_data['basket_id']) response = self.call( 'Activity', 'validateBasket', validateBasketRequestBean={ - 'idBasket': basket['id'], + 'idBasket': post_data['basket_id'], }, ) return {'data': response} @@ -3331,6 +3339,15 @@ class ToulouseMaelis(BaseResource, HTTPResource): return {'data': self.call('Ape', 'addApeBook', request=data)} + @endpoint( + display_category='Facture', + description="Lister les régies", + name='read-regie-list', + perm='can_access', + ) + def read_regie_list(self, request): + return {'data': self.get_referential('Regie')} + class Link(models.Model): resource = models.ForeignKey(ToulouseMaelis, on_delete=models.CASCADE) diff --git a/tests/data/toulouse_maelis/R_get_family_basket.xml b/tests/data/toulouse_maelis/R_get_family_basket.xml index 1bc9180a..1c0cb59b 100644 --- a/tests/data/toulouse_maelis/R_get_family_basket.xml +++ b/tests/data/toulouse_maelis/R_get_family_basket.xml @@ -3,7 +3,8 @@ - + + 105 2023-01-28T23:56:23+01:00 2023-01-28T23:56:23+01:00 0 @@ -28,6 +29,7 @@ ALEX JANY Semaine 1 + 0.0 2014-04-01T00:00:00+02:00 BART @@ -35,6 +37,8 @@ 246711 M + 43.0 + 0.0 2023-01-28T23:50:34+01:00 @@ -55,6 +59,7 @@ Centre Culturel ALBAN MINVILLE Inscription 2ème semestre + 0.0 2014-04-01T00:00:00+02:00 BART @@ -62,6 +67,8 @@ 246711 M + 43.0 + 0.0 2023-01-26T17:39:40+01:00 @@ -89,8 +96,9 @@ 246711 M + 43.0 - + diff --git a/tests/data/toulouse_maelis/R_read_regie_list.xml b/tests/data/toulouse_maelis/R_read_regie_list.xml new file mode 100644 index 00000000..a42e1776 --- /dev/null +++ b/tests/data/toulouse_maelis/R_read_regie_list.xml @@ -0,0 +1,42 @@ + + + + + 102 + CANTINE / CLAE + + + 106 + PARCOURS EDUCATIFS + + + 105 + ENFANCE LOISIRS + + + 109 + SPORT + + + 108 + SENIORS + + + 101 + ACTIONS SOCIO CULTURELLES + + + 103 + CCAS + + + 104 + DSCS + + + 107 + REMBOURSEMENT + + + + diff --git a/tests/test_toulouse_maelis.py b/tests/test_toulouse_maelis.py index b27645da..03fd712c 100644 --- a/tests/test_toulouse_maelis.py +++ b/tests/test_toulouse_maelis.py @@ -165,6 +165,12 @@ def django_db_setup(django_db_setup, django_db_blocker): settings=Settings(strict=False, xsd_ignore_sequence_order=True), ) + invoice_service = ResponsesSoap( + wsdl_url='https://example.org/InvoiceService?wsdl', + wsdl_content=get_xml_file('InvoiceService.wsdl'), + settings=Settings(strict=False, xsd_ignore_sequence_order=True), + ) + with family_service() as soap_mock: soap_mock.add_soap_response('readCategoryList', get_xml_file('R_read_category_list.xml')) soap_mock.add_soap_response( @@ -217,6 +223,10 @@ def django_db_setup(django_db_setup, django_db_blocker): ape_mock.add_soap_response('readNurseryList', get_xml_file('R_read_nursery_list.xml')) con.update_ape_referentials() + with invoice_service() as invoice_mock: + invoice_mock.add_soap_response('readRegieList', get_xml_file('R_read_regie_list.xml')) + con.update_invoice_referentials() + # reset change in zeep private interface to bypass clear_cache fixture from zeep.cache import InMemoryCache @@ -423,6 +433,7 @@ def test_cron(db): 'Quality', 'Quotient', 'RLIndicator', + 'Regie', 'Sex', 'Situation', 'Street', @@ -733,6 +744,24 @@ def test_read_quotient_list(con, app): ] +def test_read_regie_list(con, app): + url = get_endpoint('read-regie-list') + resp = app.get(url) + assert resp.json['err'] == 0 + assert len(resp.json['data']) == 9 + assert resp.json['data'] == [ + {'id': 101, 'code': 101, 'text': 'ACTIONS SOCIO CULTURELLES', 'libelle': 'ACTIONS SOCIO CULTURELLES'}, + {'id': 102, 'code': 102, 'text': 'CANTINE / CLAE', 'libelle': 'CANTINE / CLAE'}, + {'id': 103, 'code': 103, 'text': 'CCAS', 'libelle': 'CCAS'}, + {'id': 104, 'code': 104, 'text': 'DSCS', 'libelle': 'DSCS'}, + {'id': 105, 'code': 105, 'text': 'ENFANCE LOISIRS', 'libelle': 'ENFANCE LOISIRS'}, + {'id': 106, 'code': 106, 'text': 'PARCOURS EDUCATIFS', 'libelle': 'PARCOURS EDUCATIFS'}, + {'id': 107, 'code': 107, 'text': 'REMBOURSEMENT', 'libelle': 'REMBOURSEMENT'}, + {'id': 108, 'code': 108, 'text': 'SENIORS', 'libelle': 'SENIORS'}, + {'id': 109, 'code': 109, 'text': 'SPORT', 'libelle': 'SPORT'}, + ] + + def test_read_rl_indicator_list(con, app): url = get_endpoint('read-rl-indicator-list') resp = app.get(url) @@ -7298,10 +7327,9 @@ def test_update_activity_agenda_date_error(con, app): assert resp.json['err_desc'] == 'start_date and end_date are in different reference year (2022 != 2023)' -@pytest.mark.xfail(run=False) -def test_get_basket(activity_service, con, app): +def test_get_baskets(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) - url = get_endpoint('get-basket') + url = get_endpoint('get-baskets') resp = app.get(url + '?family_id=311352') assert resp.json['err'] == 0 @@ -7310,14 +7338,16 @@ def test_get_basket(activity_service, con, app): resp = app.get(url + '?NameID=local') assert resp.json['err'] == 0 data = resp.json['data'] - assert len(data['lignes']) == 3 - del data['lignes'][2] - del data['lignes'][1] - assert data == { + assert len(data[0]['lignes']) == 3 + del data[0]['lignes'][2] + del data[0]['lignes'][1] + assert data[0] == { + 'id': 'S10053200723', + 'text': 'ENFANCE LOISIRS', + 'codeRegie': 105, 'dateAdd': '2023-01-28T23:56:23+01:00', 'dateMaj': '2023-01-28T23:56:23+01:00', 'delai': 0, - 'id': 'S10053200723', 'idFam': 'S10053183425', 'lignes': [ { @@ -7340,7 +7370,7 @@ def test_get_basket(activity_service, con, app): 'libLieu': 'ALEX JANY', 'libUnit': 'Semaine 1', }, - 'montant': None, + 'montant': 0.0, 'personneInfo': { 'dateBirth': '2014-04-01T00:00:00+02:00', 'firstname': 'BART', @@ -7348,58 +7378,76 @@ def test_get_basket(activity_service, con, app): 'numPerson': 246711, 'sexe': 'M', }, - 'prixUnit': None, - 'qte': None, + 'prixUnit': 43.0, + 'qte': 0.0, }, ], } -def test_get_basket_not_linked_error(con, app): - url = get_endpoint('get-basket') +def test_get_baskets_not_linked_error(con, app): + url = get_endpoint('get-baskets') resp = app.get(url + '?NameID=local') assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'User not linked to family' -@pytest.mark.xfail(run=False) +def test_get_baskets_no_basket(activity_service, con, app): + activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml')) + url = get_endpoint('get-baskets') + resp = app.get(url + '?family_id=311352') + assert resp.json['err'] == 0 + assert resp.json['data'] == [] + + def test_update_basket_time(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) activity_service.add_soap_response('updateBasketTime', get_xml_file('R_update_basket_time.xml')) url = get_endpoint('update-basket-time') + params = {'basket_id': 'S10053200723'} - resp = app.post_json(url + '?family_id=311352') + resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 0 Link.objects.create(resource=con, family_id='311352', name_id='local') - resp = app.post_json(url + '?NameID=local') + resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 0 assert resp.json['data'] == 'ok' def test_update_basket_time_not_linked_error(con, app): url = get_endpoint('update-basket-time') - resp = app.post_json(url + '?NameID=local') + params = {'basket_id': 'S10053200723'} + resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'User not linked to family' -def test_update_basket_time_basket_not_found(activity_service, con, app): +def test_update_basket_time_no_basket(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml')) url = get_endpoint('update-basket-time') - resp = app.post_json(url + '?family_id=311352') + params = {'basket_id': 'S10053200723'} + resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 1 - assert resp.json['err_desc'] == "no basket on '311352' family" + assert resp.json['err_desc'] == "no 'S10053200723' basket on family" + + +def test_update_basket_time_basket_not_found(activity_service, con, app): + activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) + url = get_endpoint('update-basket-time') + params = {'basket_id': 'plop'} + resp = app.post_json(url + '?family_id=311352', params=params) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == "no 'plop' basket on family" -@pytest.mark.xfail(run=False) def test_delete_basket_line(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) activity_service.add_soap_response( 'deletePersonUnitBasket', get_xml_file('R_delete_person_unit_basket.xml') ) url = get_endpoint('delete-basket-line') - params = {'line_id': 'S10053203120'} + params = {'basket_id': 'S10053200723', 'line_id': 'S10053203120'} resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 0 @@ -7407,39 +7455,45 @@ def test_delete_basket_line(activity_service, con, app): resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 0 - assert len(resp.json['data']['lignes']) == 2 + assert len(resp.json['data']['lignes']) == 2 # return the basket assert 'S10053203120' not in [x['id'] for x in resp.json['data']['lignes']] def test_delete_basket_line_not_linked_error(con, app): url = get_endpoint('delete-basket-line') - params = {'line_id': 'S10053203120'} + params = {'basket_id': 'S10053200723', 'line_id': 'S10053203120'} resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'User not linked to family' -@pytest.mark.xfail(run=False) -def test_delete_basket_line_basket_not_found(activity_service, con, app): +def test_delete_basket_line_no_basket(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml')) url = get_endpoint('delete-basket-line') - params = {'line_id': 'S10053203120'} + params = {'basket_id': 'S10053200723', 'line_id': 'S10053203120'} resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 1 - assert resp.json['err_desc'] == "no basket on '311352' family" + assert resp.json['err_desc'] == "no 'S10053200723' basket on family" + + +def test_delete_basket_line_basket_not_found(activity_service, con, app): + activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) + url = get_endpoint('delete-basket-line') + params = {'basket_id': 'plop', 'line_id': 'S10053203120'} + resp = app.post_json(url + '?family_id=311352', params=params) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == "no 'plop' basket on family" -@pytest.mark.xfail(run=False) def test_delete_basket_line_line_not_found(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) url = get_endpoint('delete-basket-line') - params = {'line_id': 'plop'} + params = {'basket_id': 'S10053200723', 'line_id': 'plop'} resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 1 - assert resp.json['err_desc'] == "no 'plop' basket line on '311352' family" + assert resp.json['err_desc'] == "no 'plop' basket line on basket" -@pytest.mark.xfail(run=False) def test_delete_basket(activity_service, con, app): def request_check(request): assert request.idUtilisat in ('local', 'Middle-office') @@ -7451,64 +7505,86 @@ def test_delete_basket(activity_service, con, app): request_check=request_check, ) url = get_endpoint('delete-basket') + params = {'basket_id': 'S10053200723'} - resp = app.post_json(url + '?family_id=311352') + resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 0 Link.objects.create(resource=con, family_id='311352', name_id='local') - resp = app.post_json(url + '?NameID=local') + resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 0 assert resp.json['data'] == 'ok' def test_delete_basket_not_linked_error(con, app): url = get_endpoint('delete-basket') - resp = app.post_json(url + '?NameID=local') + params = {'basket_id': 'S10053200723'} + resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'User not linked to family' -def test_delete_basket_not_found(activity_service, con, app): +def test_delete_basket_no_basket(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml')) url = get_endpoint('delete-basket') - resp = app.post_json(url + '?family_id=311352') + params = {'basket_id': 'S10053200723'} + resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 1 - assert resp.json['err_desc'] == "no basket on '311352' family" + assert resp.json['err_desc'] == "no 'S10053200723' basket on family" + + +def test_delete_basket_not_found(activity_service, con, app): + activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) + url = get_endpoint('delete-basket') + params = {'basket_id': 'plop'} + resp = app.post_json(url + '?family_id=311352', params=params) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == "no 'plop' basket on family" -@pytest.mark.xfail(run=False) def test_validate_basket(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) activity_service.add_soap_response('validateBasket', get_xml_file('R_validate_basket.xml')) url = get_endpoint('validate-basket') + params = {'basket_id': 'S10053200723'} - resp = app.post_json(url + '?family_id=311352') + resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 0 Link.objects.create(resource=con, family_id='311352', name_id='local') - resp = app.post_json(url + '?NameID=local') + resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 0 assert resp.json['data'] == { 'idFam': 'S10053183425', - 'idFactureLst': [], + 'factureLst': [], 'idInsLst': ['S10053203103', 'S10053200721'], - 'paramUrlReglement': [], } def test_validate_basket_not_linked_error(con, app): url = get_endpoint('validate-basket') - resp = app.post_json(url + '?NameID=local') + params = {'basket_id': 'S10053200723'} + resp = app.post_json(url + '?NameID=local', params=params) assert resp.json['err'] == 1 assert resp.json['err_desc'] == 'User not linked to family' -def test_validate_basket_not_found(activity_service, con, app): +def test_validate_basket_no_basket(activity_service, con, app): activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket_empty.xml')) url = get_endpoint('validate-basket') - resp = app.post_json(url + '?family_id=311352') + params = {'basket_id': 'S10053200723'} + resp = app.post_json(url + '?family_id=311352', params=params) assert resp.json['err'] == 1 - assert resp.json['err_desc'] == "no basket on '311352' family" + assert resp.json['err_desc'] == "no 'S10053200723' basket on family" + + +def test_validate_basket_not_found(activity_service, con, app): + activity_service.add_soap_response('getFamilyBasket', get_xml_file('R_get_family_basket.xml')) + url = get_endpoint('validate-basket') + params = {'basket_id': 'plop'} + resp = app.post_json(url + '?family_id=311352', params=params) + assert resp.json['err'] == 1 + assert resp.json['err_desc'] == "no 'plop' basket on family" def test_read_nursery_list(con, app):