caluire-axel: new endpoint set_agenda_apply_changes (#67280)

This commit is contained in:
Lauréline Guérin 2022-07-12 15:47:49 +02:00
parent a02ffc773d
commit a0d07da11a
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 657 additions and 0 deletions

View File

@ -966,6 +966,85 @@ class CaluireAxel(BaseResource):
@endpoint(
display_category=_('Schooling'),
display_order=9,
description=_("Set agenda for a child, from changes applied to another child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.CHANGES_SCHEMA,
}
}
},
)
def set_agenda_apply_changes(self, request, NameID, post_data):
link = self.get_link(NameID)
child_id = post_data['child_id']
child_data = self.get_child_data(link.family_id, child_id)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
start_date, end_date, reference_year = self.get_start_and_end_dates(
post_data['start_date'], post_data['end_date']
)
# get current agenda
activities_data = self.get_child_activities(child_id, reference_year)
agenda = {}
current_child_cantine_activity_id = None
current_child_cantine_activity_ids = []
current_child_activity_ids = set()
for activity in activities_data.get('ACTIVITE', []):
activity_id = activity['IDENTACTIVITE']
if activity_id.startswith('CJ'):
# mercredi or vacances: not bookable
continue
if self.get_activity_type(activity_id) == 'midi':
current_child_cantine_activity_ids.append(activity_id)
current_child_activity_ids.add(activity_id)
agenda[activity_id] = self.get_bookings(child_id, activity_id, start_date, end_date)
if len(current_child_cantine_activity_ids) > 1:
raise APIError(
'more than one activity cantine found for this child (%s)'
% ', '.join(current_child_cantine_activity_ids),
err_code='bad-request',
http_status=400,
)
if current_child_cantine_activity_ids:
current_child_cantine_activity_id = current_child_cantine_activity_ids[0]
# create booking_list from current_agenda and changes
changes = {}
for change in post_data['changes']:
activity_id = change['activity_id']
if activity_id.startswith('CJ'):
# mercredi or vacances: not bookable
continue
# activity_id can be different for cantine activity
if self.get_activity_type(activity_id) == 'midi':
activity_id = current_child_cantine_activity_id
if activity_id not in current_child_activity_ids:
# current child is not registered for this activity, skip
continue
changes['%s:%s:%s' % (child_id, activity_id, change['day'])] = change['booked']
booking_list = []
for activity_id, bookings in agenda.items():
for booking in bookings:
bid = booking['id']
prefill = booking['prefill']
if bid not in changes and prefill is True:
# no change, but already booked
booking_list.append(bid)
elif bid in changes and changes[bid] is True:
# change, should be booked
booking_list.append(bid)
return self._set_agenda(child_id, start_date, end_date, activities_data, agenda, booking_list)
@endpoint(
display_category=_('Schooling'),
display_order=10,
description=_("Set activity agenda for a child with a typical week"),
perm='can_access',
parameters={

View File

@ -151,6 +151,50 @@ BOOKING_SCHEMA = {
],
}
CHANGES_SCHEMA = {
'type': 'object',
'properties': {
'child_id': {
'type': 'string',
'minLength': 1,
'maxLength': 8,
},
'changes': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'activity_id': {
'type': 'string',
'minLength': 1,
'maxLength': 10,
},
'activity_label': {
'type': 'string',
'minLength': 0,
'maxLength': 100,
},
'day': copy.deepcopy(axel.date_type),
'booked': copy.deepcopy(boolean_type),
},
'required': [
'activity_label',
'day',
'booked',
],
},
},
'start_date': copy.deepcopy(axel.date_type),
'end_date': copy.deepcopy(axel.date_type),
},
'required': [
'child_id',
'changes',
'start_date',
'end_date',
],
}
TYPICAL_WEEK_BOOKING_SCHEMA = {
'type': 'object',
'properties': {

View File

@ -147,6 +147,16 @@ def booking_params():
}
@pytest.fixture
def changes_params():
return {
'child_id': '50632',
'changes': [],
'start_date': '2020-09-01',
'end_date': '2021-08-31',
}
@pytest.fixture
def week_booking_params():
return {
@ -2455,6 +2465,530 @@ def test_set_agenda_endpoint_wrong_code(app, resource, family_data, activities,
assert resp.json['err'] == 'agenda-code-error-%s' % code
@freezegun.freeze_time('2020-09-01')
def test_set_agenda_apply_changes_endpoint_axel_error(app, resource, family_data, changes_params):
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
changes_params['changes'] = [
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-10-30', 'booked': True},
]
with mock.patch('passerelle.contrib.caluire_axel.schemas.get_famille_individus') as operation:
operation.side_effect = AxelError('FooBar')
resp = app.post_json('/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params)
assert resp.json['err_desc'] == "Axel error: FooBar"
assert resp.json['err'] == 'error'
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
with open(filepath) as xml:
content = xml.read()
with mock_data(content, 'GetFamilleIndividus'):
with mock.patch('passerelle.contrib.caluire_axel.schemas.get_list_activites') as operation:
operation.side_effect = AxelError('FooBar')
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params
)
assert resp.json['err_desc'] == "Axel error: FooBar"
assert resp.json['err'] == 'error'
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/activities_info.xml')
with open(filepath) as xml:
content = xml.read()
with mock_data(content, 'GetListActivites'):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.get_agenda') as operation:
operation.side_effect = AxelError('FooBar')
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params
)
assert resp.json['err_desc'] == "Axel error: FooBar"
assert resp.json['err'] == 'error'
activities = [
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ACCMAT',
'LIBELLEACTIVITE': 'Matin',
},
]
content = '''<PORTAIL>
<GETAGENDA>
<CODE>1</CODE>
<JOUR>
<JOURDATE>30/10/2020</JOURDATE>
<MATIN>.</MATIN>
<MIDI></MIDI>
<APRESMIDI></APRESMIDI>
<FERME>N</FERME>
</JOUR>
</GETAGENDA>
</PORTAIL>'''
with mock_data(content, 'GetAgenda'):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value={'ACTIVITE': activities},
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_agenda') as operation:
operation.side_effect = AxelError('FooBar')
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params
)
assert resp.json['err_desc'] == "Axel error: FooBar"
assert resp.json['err'] == 'error'
def test_set_agenda_apply_changes_endpoint_no_result(app, resource, changes_params):
changes_params['child_id'] = 'zzz'
resp = app.post_json('/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params)
assert resp.json['err_desc'] == "Person not found"
assert resp.json['err'] == 'not-found'
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
with open(filepath) as xml:
content = xml.read()
with mock_data(content, 'GetFamilleIndividus'):
resp = app.post_json('/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params)
assert resp.json['err_desc'] == "Child not found"
assert resp.json['err'] == 'not-found'
def test_set_agenda_apply_changes_endpoint_date_error(app, resource, changes_params):
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
filepath = os.path.join(os.path.dirname(__file__), 'data/caluire_axel/family_info.xml')
with open(filepath) as xml:
content = xml.read()
with mock_data(content, 'GetFamilleIndividus'):
changes_params['start_date'] = '2021-09-01'
changes_params['end_date'] = '2021-08-31'
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params, status=400
)
assert resp.json['err_desc'] == "start_date should be before end_date"
assert resp.json['err'] == 'bad-request'
changes_params['end_date'] = '2022-09-01'
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy', params=changes_params, status=400
)
assert (
resp.json['err_desc'] == "start_date and end_date are in different reference year (2021 != 2022)"
)
assert resp.json['err'] == 'bad-request'
def test_set_agenda_apply_changes_endpoint(app, resource, family_data, activities, changes_params):
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
changes_params['end_date'] = '2020-09-01'
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value=activities,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_bookings',
return_value=[],
):
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy',
params=changes_params,
)
assert resp.json['err'] == 0
assert resp.json['updated'] is True
assert resp.json['count'] == 0
@freezegun.freeze_time('2020-08-01')
def test_set_agenda_apply_changes_endpoint_multi(app, resource, family_data, changes_params):
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
activities = [
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ACCMAT',
'LIBELLEACTIVITE': 'Matin',
},
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ECOLELEM',
'LIBELLEACTIVITE': 'Cantine',
},
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'CJ MER',
'LIBELLEACTIVITE': 'FOOBAR',
},
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'CJVACANCES',
'LIBELLEACTIVITE': 'FOOBAR',
},
]
changes_params['end_date'] = '2020-09-11'
changes_params['changes'] = [
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-09-07', 'booked': False},
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-09-08', 'booked': True},
{
'activity_id': 'ECOLELEM2',
'activity_label': 'Cantine',
'day': '2020-09-07',
'booked': True,
}, # cantine, not the same id
{
'activity_id': 'ECOLELEM2',
'activity_label': 'Cantine',
'day': '2020-09-10',
'booked': False,
}, # cantine, not the same id
{'activity_id': 'CJ MER', 'activity_label': 'FOOBAR', 'day': '2020-09-08', 'booked': True}, # ignored
{
'activity_id': 'CJVACANCES',
'activity_label': 'FOOBAR',
'day': '2020-09-08',
'booked': True,
}, # ignored
]
bookings = []
for activity_id in ['ACCMAT', 'ECOLELEM']:
for day in ['2020-09-07', '2020-09-08']:
bookings.append(
{
'id': '50632:%s:%s' % (activity_id, day),
'disabled': False,
'prefill': False,
}
)
for day in ['2020-09-10', '2020-09-11']:
bookings.append(
{
'id': '50632:%s:%s' % (activity_id, day),
'disabled': False,
'prefill': True,
}
)
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value={'ACTIVITE': activities},
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_bookings',
return_value=bookings,
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_agenda') as operation:
operation.return_value = OperationResult(
json_response={'DATA': {'PORTAIL': {'SETAGENDA': {'CODE': 0}}}},
xml_request='',
xml_response='',
)
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy',
params=changes_params,
)
assert resp.json['err'] == 0
assert resp.json['updated'] is True
assert resp.json['count'] == 3
assert resp.json['changes'] == [
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-09-08', 'booked': True},
{'activity_id': 'ECOLELEM', 'activity_label': 'Cantine', 'day': '2020-09-07', 'booked': True},
{'activity_id': 'ECOLELEM', 'activity_label': 'Cantine', 'day': '2020-09-10', 'booked': False},
]
assert len(operation.call_args_list) == 2
assert operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['IDENTINDIVIDU'] == '50632'
assert operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['IDENTACTIVITE'] == 'ACCMAT'
assert len(operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR']) == 1
assert (
operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][0]['JOURDATE']
== '2020-09-08'
)
assert operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][0]['MATIN'] == 'X'
assert operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['IDENTINDIVIDU'] == '50632'
assert (
operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['IDENTACTIVITE'] == 'ECOLELEM'
)
assert len(operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR']) == 2
assert (
operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][0]['JOURDATE']
== '2020-09-07'
)
assert operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][0]['MATIN'] == 'X'
assert (
operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][1]['JOURDATE']
== '2020-09-10'
)
assert operation.call_args_list[1][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][1]['MATIN'] == 'D'
@freezegun.freeze_time('2020-08-01')
def test_set_agenda_apply_changes_endpoint_multi_too_many_cantine_activities(
app, resource, family_data, changes_params
):
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
# more than one activity "midi"
activities = [
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ECOLELEM',
'LIBELLEACTIVITE': 'Cantine',
},
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ECOLELEM2',
'LIBELLEACTIVITE': 'Cantine',
},
]
changes_params['end_date'] = '2020-09-11'
changes_params['changes'] = [
{
'activity_id': 'ECOLELEM2',
'activity_label': 'Cantine',
'day': '2020-09-07',
'booked': True,
},
]
bookings = [
{
'id': '50632:MIDI:2020-09-07',
'disabled': False,
'prefill': False,
},
]
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value={'ACTIVITE': activities},
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_bookings',
return_value=bookings,
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_agenda') as operation:
operation.return_value = OperationResult(
json_response={'DATA': {'PORTAIL': {'SETAGENDA': {'CODE': 0}}}},
xml_request='',
xml_response='',
)
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy',
params=changes_params,
status=400,
)
assert (
resp.json['err_desc']
== "more than one activity cantine found for this child (ECOLELEM, ECOLELEM2)"
)
assert resp.json['err'] == 'bad-request'
@freezegun.freeze_time('2020-08-01')
def test_set_agenda_apply_changes_endpoint_multi_unkown_activity(app, resource, family_data, changes_params):
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
activities = [
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ACCMAT',
'LIBELLEACTIVITE': 'Matin',
},
]
changes_params['end_date'] = '2020-09-11'
changes_params['changes'] = [
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-09-07', 'booked': False},
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-09-08', 'booked': True},
{
'activity_id': 'ECOLELEM2',
'activity_label': 'Cantine',
'day': '2020-09-07',
'booked': True,
}, # cantine, child is not registered
{
'activity_id': 'ECOLELEM2',
'activity_label': 'Cantine',
'day': '2020-09-10',
'booked': False,
}, # cantine, child is not registered
]
bookings = []
for day in ['2020-09-07', '2020-09-08']:
bookings.append(
{
'id': '50632:ACCMAT:%s' % day,
'disabled': False,
'prefill': False,
}
)
for day in ['2020-09-10', '2020-09-11']:
bookings.append(
{
'id': '50632:ACCMAT:%s' % day,
'disabled': False,
'prefill': True,
}
)
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value={'ACTIVITE': activities},
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_bookings',
return_value=bookings,
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_agenda') as operation:
operation.return_value = OperationResult(
json_response={'DATA': {'PORTAIL': {'SETAGENDA': {'CODE': 0}}}},
xml_request='',
xml_response='',
)
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy',
params=changes_params,
)
assert resp.json['err'] == 0
assert resp.json['updated'] is True
assert resp.json['count'] == 1
assert resp.json['changes'] == [
{'activity_id': 'ACCMAT', 'activity_label': 'Matin', 'day': '2020-09-08', 'booked': True},
]
assert len(operation.call_args_list) == 1
assert operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['IDENTINDIVIDU'] == '50632'
assert operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['IDENTACTIVITE'] == 'ACCMAT'
assert len(operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR']) == 1
assert (
operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][0]['JOURDATE']
== '2020-09-08'
)
assert operation.call_args_list[0][0][1]['PORTAIL']['SETAGENDA']['ACTIVITE']['JOUR'][0]['MATIN'] == 'X'
@freezegun.freeze_time('2020-08-01')
def test_set_agenda_apply_changes_endpoint_multi_soir(app, resource, family_data, changes_params):
# just a little check, booking exclusivity is handled by _set_agenda method,
# already tested for set_agenda endpoint
Link.objects.create(resource=resource, name_id='yyy', family_id='XXX', person_id='42')
activities = [
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'GARDERIES',
'LIBELLEACTIVITE': 'Garderie',
},
{
'ENTREE': '2020-09-02',
'SORTIE': '2021-08-31',
'IDENTACTIVITE': 'ETUDES',
'LIBELLEACTIVITE': 'Etudes',
},
]
changes_params['end_date'] = '2020-09-11'
changes_params['changes'] = [
{'activity_id': 'GARDERIES', 'activity_label': 'Garderie', 'day': '2020-09-07', 'booked': True},
]
bookings = [
{
'id': '50632:GARDERIES:2020-09-07',
'disabled': False,
'prefill': False,
},
{
'id': '50632:ETUDES:2020-09-07',
'disabled': False,
'prefill': True,
},
]
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value={'ACTIVITE': activities},
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_bookings',
return_value=bookings,
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_agenda') as operation:
operation.return_value = OperationResult(
json_response={'DATA': {'PORTAIL': {'SETAGENDA': {'CODE': 0}}}},
xml_request='',
xml_response='',
)
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy',
params=changes_params,
status=400,
)
assert (
resp.json['err_desc']
== "not possible to book 50632:ETUDES:2020-09-07 and 50632:GARDERIES:2020-09-07 the same day"
)
assert resp.json['err'] == 'bad-request'
# already booked, no error
bookings = [
{
'id': '50632:GARDERIES:2020-09-07',
'disabled': False,
'prefill': True,
},
{
'id': '50632:ETUDES:2020-09-07',
'disabled': False,
'prefill': True,
},
]
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_family_data',
return_value=family_data,
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_child_activities',
return_value={'ACTIVITE': activities},
):
with mock.patch(
'passerelle.contrib.caluire_axel.models.CaluireAxel.get_bookings',
return_value=bookings,
):
with mock.patch('passerelle.contrib.caluire_axel.schemas.set_agenda') as operation:
operation.return_value = OperationResult(
json_response={'DATA': {'PORTAIL': {'SETAGENDA': {'CODE': 0}}}},
xml_request='',
xml_response='',
)
resp = app.post_json(
'/caluire-axel/test/set_agenda_apply_changes?NameID=yyy',
params=changes_params,
)
assert resp.json['err'] == 0
@freezegun.freeze_time('2020-08-01')
def test_set_activity_agenda_typical_week_endpoint_axel_error(
app, resource, family_data, activities, week_booking_params