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.
Files changed (22) hide show
  1. package/.github/workflows/test.yml +1 -0
  2. package/README.md +3 -0
  3. package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service.yml +5 -0
  4. package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service_as_admin.yml +5 -0
  5. package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_submission.yml +5 -0
  6. package/frameworks/digital-outcomes-and-specialists-7/questions/services/icoRegistered.yml +1 -0
  7. package/frameworks/digital-outcomes-and-specialists-7/questions/services/icoRegistrationReference.yml +2 -1
  8. package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqIcoRegistered.yml +1 -1
  9. package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqOtherTradeOrIndustryBodies.yml +1 -1
  10. package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqTradeOrIndustryBodies.yml +17 -0
  11. package/frameworks/digital-outcomes-and-specialists-7/questions/services/otherTradeOrIndustryBodies.yml +2 -0
  12. package/frameworks/digital-outcomes-and-specialists-7/questions/services/otherTradeOrIndustryBodiesOther.yml +4 -3
  13. package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodies.yml +33 -0
  14. package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodiesOther.yml +19 -0
  15. package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatTypesOfUserResearchStudio.yml +8 -0
  16. package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatUserResearchServices.yml +4 -0
  17. package/frameworks/g-cloud-14/metadata/following_framework.yml +1 -1
  18. package/package.json +1 -1
  19. package/requirements-dev.txt +1 -1
  20. package/schema_generator/filter.py +151 -0
  21. package/scripts/generate-filter-schemas.py +47 -0
  22. 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
 
@@ -240,3 +240,8 @@
240
240
  editable: False
241
241
  questions:
242
242
  - whatTypesOfUserResearchStudio
243
+
244
+ - name: User research studios
245
+ editable: False
246
+ questions:
247
+ - multiqTradeOrIndustryBodies
@@ -268,3 +268,8 @@
268
268
  editable: True
269
269
  questions:
270
270
  - whatTypesOfUserResearchStudio
271
+
272
+ - name: User research studios
273
+ editable: True
274
+ questions:
275
+ - multiqTradeOrIndustryBodies
@@ -312,3 +312,8 @@
312
312
  editable: True
313
313
  questions:
314
314
  - whatTypesOfUserResearchStudio
315
+
316
+ - name: User research studios
317
+ editable: True
318
+ questions:
319
+ - multiqTradeOrIndustryBodies
@@ -1,3 +1,4 @@
1
+ name: Registered with the Information Commissioner's Office (ICO)
1
2
  question: Are you registered with the Information Commissioner's Office (ICO)?
2
3
 
3
4
  depends:
@@ -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: under_word_limit
16
+ - name: under_10_words
16
17
  message: "Your answer must be no longer than 10 words"
@@ -1,5 +1,5 @@
1
1
  name: Registered with the Information Commissioner's Office (ICO)
2
- question: Registered with the Information Commissioner's Office (ICO)
2
+ question: ""
3
3
 
4
4
  depends:
5
5
  - "on": lot
@@ -1,5 +1,5 @@
1
1
  name: Member of any other trade or industry bodies
2
- question: Member of any other trade or industry bodies
2
+ question: ""
3
3
 
4
4
  depends:
5
5
  - "on": lot
@@ -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,6 @@
1
+ name: Member of any other trade or industry bodies
1
2
  question: Are you a member of any other trade or industry bodies?
3
+ hint: Select all that apply
2
4
 
3
5
  depends:
4
6
  - "on": lot
@@ -1,4 +1,5 @@
1
- question: Specify any other trade or industry bodies you a member of
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: under_word_limit
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"
@@ -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
  -
@@ -1,4 +1,4 @@
1
1
  framework:
2
- coming: '2024'
2
+ coming: '2025'
3
3
  name: G-Cloud 15
4
4
  slug: g-cloud-15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccs-digitalmarketplace-frameworks",
3
- "version": "3.3.0",
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"
@@ -115,7 +115,7 @@ govuk-country-register==0.5.0
115
115
  # via
116
116
  # -r requirements.txt
117
117
  # ccs-digitalmarketplace-utils
118
- hypothesis==6.115.0
118
+ hypothesis==6.115.3
119
119
  # via -r requirements-dev.in
120
120
  idna==3.7
121
121
  # via
@@ -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')
@@ -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
- for fllowup_question_id in question.get('followup', {}).keys():
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[question_id][fllowup_question_id] = \
170
- question_values
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