ccs-digitalmarketplace-frameworks 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +1 -0
- package/README.md +3 -0
- package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service.yml +5 -0
- package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service_as_admin.yml +5 -0
- package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_submission.yml +5 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/icoRegistered.yml +1 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/icoRegistrationReference.yml +2 -1
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqIcoRegistered.yml +1 -1
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqOtherTradeOrIndustryBodies.yml +1 -1
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqTradeOrIndustryBodies.yml +17 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/otherTradeOrIndustryBodies.yml +2 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/otherTradeOrIndustryBodiesOther.yml +4 -3
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodies.yml +33 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodiesOther.yml +19 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatTypesOfUserResearchStudio.yml +8 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatUserResearchServices.yml +4 -0
- package/frameworks/g-cloud-14/metadata/following_framework.yml +1 -1
- package/package.json +1 -1
- package/requirements-dev.txt +1 -1
- package/schema_generator/filter.py +151 -0
- package/scripts/generate-filter-schemas.py +47 -0
- package/tests/test_content.py +47 -9
|
@@ -40,6 +40,7 @@ jobs:
|
|
|
40
40
|
- scripts/generate-assessment-schema.py g-cloud-14
|
|
41
41
|
- scripts/generate-search-config.py g-cloud-14 services --output-path=/tmp
|
|
42
42
|
- scripts/generate-validation-schemas.py --output-path=/tmp
|
|
43
|
+
- scripts/generate-filter-schemas.py --output-path=/tmp
|
|
43
44
|
|
|
44
45
|
steps:
|
|
45
46
|
- uses: actions/checkout@v4
|
package/README.md
CHANGED
|
@@ -186,6 +186,9 @@ Don't forget that you may also need to generate schemas into the API's `json_sch
|
|
|
186
186
|
`scripts/generate-validation-schemas.py` script in this repo, or similarly update the Search API using the
|
|
187
187
|
`scripts/generate-search-config.py` script.
|
|
188
188
|
|
|
189
|
+
You may also need to generate filter schemas into the Supplier API's `filters` directory using the
|
|
190
|
+
`scripts/generate-filter-schemas.py` script in this repo.
|
|
191
|
+
|
|
189
192
|
Running the tests
|
|
190
193
|
-----------------
|
|
191
194
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
name: Registration reference
|
|
1
2
|
question: Enter your registration reference
|
|
2
3
|
|
|
3
4
|
depends:
|
|
@@ -12,5 +13,5 @@ hidden: true
|
|
|
12
13
|
validations:
|
|
13
14
|
- name: answer_required
|
|
14
15
|
message: Enter your registration reference
|
|
15
|
-
- name:
|
|
16
|
+
- name: under_10_words
|
|
16
17
|
message: "Your answer must be no longer than 10 words"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Member of any trade or industry bodies
|
|
2
|
+
question: ""
|
|
3
|
+
|
|
4
|
+
depends:
|
|
5
|
+
- "on": lot
|
|
6
|
+
being:
|
|
7
|
+
- user-research-studios-and-participants
|
|
8
|
+
|
|
9
|
+
depends_on_follow_up:
|
|
10
|
+
whatTypesOfUserResearchStudio:
|
|
11
|
+
- virtual-studio
|
|
12
|
+
|
|
13
|
+
type: multiquestion
|
|
14
|
+
|
|
15
|
+
questions:
|
|
16
|
+
- tradeOrIndustryBodies
|
|
17
|
+
- tradeOrIndustryBodiesOther
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
name: Other trade or industry bodies you are a member of
|
|
2
|
+
question: Specify any other trade or industry bodies you are a member of
|
|
2
3
|
|
|
3
4
|
depends:
|
|
4
5
|
- "on": lot
|
|
@@ -11,8 +12,8 @@ hidden: true
|
|
|
11
12
|
|
|
12
13
|
validations:
|
|
13
14
|
- name: answer_required
|
|
14
|
-
message: Enter any other trade or industry bodies you a member of
|
|
15
|
-
- name:
|
|
15
|
+
message: Enter any other trade or industry bodies you are a member of
|
|
16
|
+
- name: under_200_words
|
|
16
17
|
message: "Your answer must be no more than 200 words"
|
|
17
18
|
- name: under_character_limit
|
|
18
19
|
message: "Your answer must be no more than 2000 characters"
|
package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodies.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Member of any trade or industry bodies
|
|
2
|
+
question: Are you a member of any trade or industry bodies?
|
|
3
|
+
hint: Select all that apply
|
|
4
|
+
|
|
5
|
+
depends:
|
|
6
|
+
- "on": lot
|
|
7
|
+
being:
|
|
8
|
+
- user-research-studios-and-participants
|
|
9
|
+
|
|
10
|
+
depends_on_follow_up:
|
|
11
|
+
whatTypesOfUserResearchStudio:
|
|
12
|
+
- virtual-studio
|
|
13
|
+
|
|
14
|
+
type: checkboxes
|
|
15
|
+
|
|
16
|
+
options:
|
|
17
|
+
- label: Association of Qualitative Research (AQR)
|
|
18
|
+
value: aqr
|
|
19
|
+
- label: Market Research Society (MRS)
|
|
20
|
+
value: mrs
|
|
21
|
+
- label: Viewing Facilities Association (VFA)
|
|
22
|
+
value: vfa
|
|
23
|
+
- label: Other
|
|
24
|
+
value: other
|
|
25
|
+
|
|
26
|
+
followup:
|
|
27
|
+
tradeOrIndustryBodiesOther:
|
|
28
|
+
- other
|
|
29
|
+
|
|
30
|
+
validations:
|
|
31
|
+
-
|
|
32
|
+
name: answer_required
|
|
33
|
+
message: Select if you are a member of any trade or industry bodies
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Other trade or industry bodies you are a member of
|
|
2
|
+
question: Specify any other trade or industry bodies you are a member of
|
|
3
|
+
|
|
4
|
+
depends:
|
|
5
|
+
- "on": lot
|
|
6
|
+
being:
|
|
7
|
+
- user-research-studios-and-participants
|
|
8
|
+
|
|
9
|
+
type: textbox_large
|
|
10
|
+
max_length_in_words: 200
|
|
11
|
+
hidden: true
|
|
12
|
+
|
|
13
|
+
validations:
|
|
14
|
+
- name: answer_required
|
|
15
|
+
message: Enter any other trade or industry bodies you are a member of
|
|
16
|
+
- name: under_200_words
|
|
17
|
+
message: "Your answer must be no more than 200 words"
|
|
18
|
+
- name: under_character_limit
|
|
19
|
+
message: "Your answer must be no more than 2000 characters"
|
|
@@ -18,6 +18,14 @@ depends_on_follow_up:
|
|
|
18
18
|
whatUserResearchServices:
|
|
19
19
|
- user-research-studios
|
|
20
20
|
|
|
21
|
+
dependent_follow_up:
|
|
22
|
+
tradeOrIndustryBodies:
|
|
23
|
+
- virtual-studio
|
|
24
|
+
|
|
25
|
+
dependent_follow_up_followup:
|
|
26
|
+
tradeOrIndustryBodiesOther:
|
|
27
|
+
- virtual-studio
|
|
28
|
+
|
|
21
29
|
validations:
|
|
22
30
|
-
|
|
23
31
|
name: answer_required
|
|
@@ -33,12 +33,16 @@ dependent_follow_up:
|
|
|
33
33
|
- user-research-participants
|
|
34
34
|
whatTypesOfUserResearchStudio:
|
|
35
35
|
- user-research-studios
|
|
36
|
+
tradeOrIndustryBodies:
|
|
37
|
+
- user-research-studios
|
|
36
38
|
|
|
37
39
|
dependent_follow_up_followup:
|
|
38
40
|
otherTradeOrIndustryBodiesOther:
|
|
39
41
|
- user-research-participants
|
|
40
42
|
icoRegistrationReference:
|
|
41
43
|
- user-research-participants
|
|
44
|
+
tradeOrIndustryBodiesOther:
|
|
45
|
+
- user-research-studios
|
|
42
46
|
|
|
43
47
|
validations:
|
|
44
48
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccs-digitalmarketplace-frameworks",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Data files for Digital Marketplace’s procurement frameworks",
|
|
5
5
|
"repository": "git@github.com:Crown-Commercial-Service/ccs-digitalmarketplace-frameworks",
|
|
6
6
|
"author": "enquiries@digitalmarketplace.service.gov.uk"
|
package/requirements-dev.txt
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from dmcontent import ContentLoader
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
|
|
7
|
+
G_CLOUD_LOTS = [
|
|
8
|
+
('cloud-hosting', 'Cloud Hosting'),
|
|
9
|
+
('cloud-software', 'Cloud Software'),
|
|
10
|
+
('cloud-support', 'Cloud Support')
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
FRAMEWORKS_AND_LOTS = {
|
|
14
|
+
"g-cloud": [
|
|
15
|
+
{
|
|
16
|
+
'framework_slug': 'g-cloud-12',
|
|
17
|
+
'framework_name': "G-Cloud 12",
|
|
18
|
+
'lots': G_CLOUD_LOTS
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
'framework_slug': 'g-cloud-13',
|
|
22
|
+
'framework_name': "G-Cloud 13",
|
|
23
|
+
'lots': G_CLOUD_LOTS,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
'framework_slug': 'g-cloud-14',
|
|
27
|
+
'framework_name': "G-Cloud 14",
|
|
28
|
+
'lots': G_CLOUD_LOTS,
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_schema_title_and_slugs():
|
|
35
|
+
titles_and_slugs = []
|
|
36
|
+
for _family, framework_list in FRAMEWORKS_AND_LOTS.items():
|
|
37
|
+
for framework in framework_list:
|
|
38
|
+
for lot_slug, _lot_name in framework['lots']:
|
|
39
|
+
titles_and_slugs.append(
|
|
40
|
+
(
|
|
41
|
+
framework['framework_slug'],
|
|
42
|
+
lot_slug
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return titles_and_slugs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
SCHEMAS = _get_schema_title_and_slugs()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_filter_value_from_question_option(option):
|
|
52
|
+
"""
|
|
53
|
+
Processes `option` into a search term value suitable for the Search API.
|
|
54
|
+
:return: normalized string value
|
|
55
|
+
"""
|
|
56
|
+
return (option.get('value') or option.get('label', '')).lower().replace(',', '')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sections_for_lot(lot, builder, all_lots=None):
|
|
60
|
+
if all_lots is None:
|
|
61
|
+
all_lots = []
|
|
62
|
+
if lot is None or lot == 'all':
|
|
63
|
+
for lot_slug in [x['slug'] for x in all_lots]:
|
|
64
|
+
builder = builder.filter({'lot': lot_slug})
|
|
65
|
+
else:
|
|
66
|
+
builder = builder.filter({'lot': lot})
|
|
67
|
+
|
|
68
|
+
return builder.sections
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _filters_for_lot(lot, builder, all_lots=None):
|
|
72
|
+
if all_lots is None:
|
|
73
|
+
all_lots = []
|
|
74
|
+
sections = _sections_for_lot(lot, builder, all_lots=all_lots)
|
|
75
|
+
lot_filters = OrderedDict()
|
|
76
|
+
|
|
77
|
+
for section in sections:
|
|
78
|
+
section_filter = {
|
|
79
|
+
"label": section["name"],
|
|
80
|
+
"slug": section["slug"],
|
|
81
|
+
"filters": [],
|
|
82
|
+
}
|
|
83
|
+
for question in section["questions"]:
|
|
84
|
+
section_filter["filters"].extend(
|
|
85
|
+
_filters_for_question(question)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
lot_filters[section.slug] = section_filter
|
|
89
|
+
|
|
90
|
+
return lot_filters
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _filters_for_question(question):
|
|
94
|
+
question_filters = []
|
|
95
|
+
if question['type'] == 'boolean':
|
|
96
|
+
question_filters.append({
|
|
97
|
+
'label': question.get('filter_label') or question.get('name') or question['question'],
|
|
98
|
+
'name': question['id'],
|
|
99
|
+
'id': question['id'],
|
|
100
|
+
'value': 'true',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
elif question['type'] in ['checkboxes', 'radios', 'checkbox_tree']:
|
|
104
|
+
_recursive_add_option_filters(question, question['options'], question_filters)
|
|
105
|
+
|
|
106
|
+
return question_filters
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _recursive_add_option_filters(question, options_list, filters_list):
|
|
110
|
+
for option in options_list:
|
|
111
|
+
if not option.get('filter_ignore'):
|
|
112
|
+
value = _get_filter_value_from_question_option(option)
|
|
113
|
+
presented_filter = {
|
|
114
|
+
'label': option.get('filter_label') or option['label'],
|
|
115
|
+
'name': question['id'],
|
|
116
|
+
'id': '{}-{}'.format(
|
|
117
|
+
question['id'],
|
|
118
|
+
value.replace(' ', '-')),
|
|
119
|
+
'value': value,
|
|
120
|
+
}
|
|
121
|
+
if option.get('options'):
|
|
122
|
+
presented_filter['children'] = []
|
|
123
|
+
_recursive_add_option_filters(question, option.get('options', []), presented_filter['children'])
|
|
124
|
+
|
|
125
|
+
filters_list.append(presented_filter)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def generate_filter(framework_slug, lot_slug):
|
|
129
|
+
loader = ContentLoader('./')
|
|
130
|
+
|
|
131
|
+
loader.load_manifest(framework_slug, 'services', 'services_search_filters')
|
|
132
|
+
manifest = loader.get_manifest(framework_slug, 'services_search_filters')
|
|
133
|
+
|
|
134
|
+
return _filters_for_lot(
|
|
135
|
+
lot_slug,
|
|
136
|
+
manifest,
|
|
137
|
+
all_lots=[lot_slug for _lot_name, lot_slug in G_CLOUD_LOTS]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def generate_filter_todir(dir_path, framework_slug, lot_slug):
|
|
142
|
+
start_t = time.perf_counter()
|
|
143
|
+
schema = generate_filter(framework_slug, lot_slug)
|
|
144
|
+
|
|
145
|
+
with open(os.path.join(dir_path, 'filters-{}-{}.json'.format(framework_slug, lot_slug)), 'w') as f:
|
|
146
|
+
json.dump(schema, f, sort_keys=True, indent=2, separators=(',', ': '))
|
|
147
|
+
f.write(os.linesep)
|
|
148
|
+
|
|
149
|
+
end_t = time.perf_counter()
|
|
150
|
+
|
|
151
|
+
return ((framework_slug, lot_slug), end_t - start_t)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Generate filters JSON schemas from the frameworks questions content.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
generate-filters-schemas.py --output-path=<output_path> [--framework=<framework_slug>] [--lot=<lot_slug>]
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
sys.path.insert(0, '.')
|
|
11
|
+
|
|
12
|
+
from docopt import docopt
|
|
13
|
+
|
|
14
|
+
from multiprocessing import Pool
|
|
15
|
+
from functools import partial
|
|
16
|
+
|
|
17
|
+
from schema_generator.filter import generate_filter_todir, SCHEMAS
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == '__main__':
|
|
21
|
+
arguments = docopt(__doc__)
|
|
22
|
+
OUTPUT_DIR = arguments['--output-path']
|
|
23
|
+
FRAMEWORK_SLUG = arguments.get('--framework')
|
|
24
|
+
LOT_SLUG = arguments.get('--lot')
|
|
25
|
+
if not os.path.exists(OUTPUT_DIR):
|
|
26
|
+
print("Creating {} directory".format(OUTPUT_DIR))
|
|
27
|
+
os.makedirs(OUTPUT_DIR)
|
|
28
|
+
|
|
29
|
+
schemas = SCHEMAS
|
|
30
|
+
|
|
31
|
+
if FRAMEWORK_SLUG:
|
|
32
|
+
schemas = [schema for schema in schemas if schema[0] == FRAMEWORK_SLUG]
|
|
33
|
+
|
|
34
|
+
if LOT_SLUG:
|
|
35
|
+
schemas = [schema for schema in schemas if schema[1] == LOT_SLUG]
|
|
36
|
+
|
|
37
|
+
with Pool(processes=5) as pool:
|
|
38
|
+
results = pool.starmap(
|
|
39
|
+
partial(
|
|
40
|
+
generate_filter_todir,
|
|
41
|
+
OUTPUT_DIR,
|
|
42
|
+
),
|
|
43
|
+
schemas
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
for schema, duration in results:
|
|
47
|
+
print(f'{schema} completed in {duration:.2f}s')
|
package/tests/test_content.py
CHANGED
|
@@ -137,9 +137,7 @@ def test_dependent_follow_ups(framework, question_set, manifest):
|
|
|
137
137
|
loaded_content = content.get_manifest(framework, manifest)
|
|
138
138
|
|
|
139
139
|
questions_with_dependent_follow_up = {}
|
|
140
|
-
questions_with_dependent_follow_up_followup = {}
|
|
141
140
|
expected_questions_with_dependent_follow_up = {}
|
|
142
|
-
expected_questions_with_dependent_follow_up_followup = {}
|
|
143
141
|
|
|
144
142
|
for section in loaded_content.sections:
|
|
145
143
|
for section_question in section.questions:
|
|
@@ -151,25 +149,65 @@ def test_dependent_follow_ups(framework, question_set, manifest):
|
|
|
151
149
|
for question in questions:
|
|
152
150
|
if question.get("dependent_follow_up"):
|
|
153
151
|
questions_with_dependent_follow_up[question.id] = question.get("dependent_follow_up")
|
|
154
|
-
if question.get("dependent_follow_up_followup"):
|
|
155
|
-
questions_with_dependent_follow_up_followup[question.id] = \
|
|
156
|
-
question.get("dependent_follow_up_followup")
|
|
157
152
|
|
|
158
153
|
for question_id, question_values in question.get("depends_on_follow_up", {}).items():
|
|
159
154
|
if question_id not in expected_questions_with_dependent_follow_up:
|
|
160
155
|
expected_questions_with_dependent_follow_up[question_id] = {}
|
|
161
156
|
|
|
157
|
+
for dependent_follow_up_id in question.get("dependent_follow_up", {}).keys():
|
|
158
|
+
expected_questions_with_dependent_follow_up[
|
|
159
|
+
question_id
|
|
160
|
+
][
|
|
161
|
+
dependent_follow_up_id
|
|
162
|
+
] = question_values
|
|
163
|
+
|
|
162
164
|
for field in question.form_fields:
|
|
163
165
|
expected_questions_with_dependent_follow_up[question_id][field] = question_values
|
|
164
166
|
|
|
165
|
-
|
|
167
|
+
assert questions_with_dependent_follow_up == expected_questions_with_dependent_follow_up
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize("framework, question_set, manifest", get_manifests())
|
|
171
|
+
def test_dependent_follow_up_followup(framework, question_set, manifest):
|
|
172
|
+
content.load_manifest(framework, question_set, manifest)
|
|
173
|
+
loaded_content = content.get_manifest(framework, manifest)
|
|
174
|
+
|
|
175
|
+
questions_with_dependent_follow_up_followup = {}
|
|
176
|
+
expected_questions_with_dependent_follow_up_followup = {}
|
|
177
|
+
|
|
178
|
+
for section in loaded_content.sections:
|
|
179
|
+
for section_question in section.questions:
|
|
180
|
+
if isinstance(section_question, Multiquestion):
|
|
181
|
+
questions = section_question.get_questions_flat()
|
|
182
|
+
else:
|
|
183
|
+
questions = [section_question]
|
|
184
|
+
|
|
185
|
+
for question in questions:
|
|
186
|
+
if question.get("dependent_follow_up_followup"):
|
|
187
|
+
questions_with_dependent_follow_up_followup[question.id] = \
|
|
188
|
+
question.get("dependent_follow_up_followup")
|
|
189
|
+
|
|
190
|
+
for question_id, question_values in question.get("depends_on_follow_up", {}).items():
|
|
191
|
+
for followup_question_id in question.get('followup', {}).keys():
|
|
166
192
|
if question_id not in expected_questions_with_dependent_follow_up_followup:
|
|
167
193
|
expected_questions_with_dependent_follow_up_followup[question_id] = {}
|
|
168
194
|
|
|
169
|
-
expected_questions_with_dependent_follow_up_followup[
|
|
170
|
-
|
|
195
|
+
expected_questions_with_dependent_follow_up_followup[
|
|
196
|
+
question_id
|
|
197
|
+
][
|
|
198
|
+
followup_question_id
|
|
199
|
+
] = question_values
|
|
200
|
+
|
|
201
|
+
for dependent_follow_up_followup_id in question.get("dependent_follow_up_followup", {}).keys():
|
|
202
|
+
if question_id not in expected_questions_with_dependent_follow_up_followup:
|
|
203
|
+
expected_questions_with_dependent_follow_up_followup[question_id] = {}
|
|
204
|
+
|
|
205
|
+
expected_questions_with_dependent_follow_up_followup[
|
|
206
|
+
question_id
|
|
207
|
+
][
|
|
208
|
+
dependent_follow_up_followup_id
|
|
209
|
+
] = question_values
|
|
171
210
|
|
|
172
|
-
assert questions_with_dependent_follow_up == expected_questions_with_dependent_follow_up
|
|
173
211
|
assert questions_with_dependent_follow_up_followup == expected_questions_with_dependent_follow_up_followup
|
|
174
212
|
|
|
175
213
|
|