ccs-digitalmarketplace-frameworks 3.3.1 → 3.5.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 +12 -0
  4. package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service_as_admin.yml +12 -0
  5. package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_submission.yml +12 -0
  6. package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqTradeOrIndustryBodies.yml +17 -0
  7. package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodies.yml +33 -0
  8. package/frameworks/digital-outcomes-and-specialists-7/questions/services/tradeOrIndustryBodiesOther.yml +19 -0
  9. package/frameworks/digital-outcomes-and-specialists-7/questions/services/virtualMinimumLeadTime.yml +29 -0
  10. package/frameworks/digital-outcomes-and-specialists-7/questions/services/virtualMinimumStudioTime.yml +28 -0
  11. package/frameworks/digital-outcomes-and-specialists-7/questions/services/virtualStudioBookingCost.yml +37 -0
  12. package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatTypesOfUserResearchStudio.yml +16 -0
  13. package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatUserResearchServices.yml +12 -0
  14. package/frameworks/g-cloud-14/metadata/following_framework.yml +1 -1
  15. package/package.json +1 -1
  16. package/requirements-dev.txt +1 -1
  17. package/schema_generator/filter.py +151 -0
  18. package/schema_generator/validation.py +14 -4
  19. package/schemas/questions.json +36 -0
  20. package/scripts/generate-filter-schemas.py +47 -0
  21. package/tests/test_content.py +47 -9
  22. package/tests/test_generate_validation_schema.py +99 -2
@@ -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,15 @@
240
240
  editable: False
241
241
  questions:
242
242
  - whatTypesOfUserResearchStudio
243
+
244
+ - name: User research studios
245
+ editable: False
246
+ questions:
247
+ - multiqTradeOrIndustryBodies
248
+
249
+ - name: Virtual user research studio - Booking
250
+ editable: False
251
+ questions:
252
+ - virtualMinimumStudioTime
253
+ - virtualStudioBookingCost
254
+ - virtualMinimumLeadTime
@@ -268,3 +268,15 @@
268
268
  editable: True
269
269
  questions:
270
270
  - whatTypesOfUserResearchStudio
271
+
272
+ - name: User research studios
273
+ editable: True
274
+ questions:
275
+ - multiqTradeOrIndustryBodies
276
+
277
+ - name: Virtual user research studio - Booking
278
+ editable: True
279
+ questions:
280
+ - virtualMinimumStudioTime
281
+ - virtualStudioBookingCost
282
+ - virtualMinimumLeadTime
@@ -312,3 +312,15 @@
312
312
  editable: True
313
313
  questions:
314
314
  - whatTypesOfUserResearchStudio
315
+
316
+ - name: User research studios
317
+ editable: True
318
+ questions:
319
+ - multiqTradeOrIndustryBodies
320
+
321
+ - name: Virtual user research studio - Booking
322
+ editable: True
323
+ questions:
324
+ - virtualMinimumStudioTime
325
+ - virtualStudioBookingCost
326
+ - virtualMinimumLeadTime
@@ -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
@@ -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"
@@ -0,0 +1,29 @@
1
+ name: Minimum lead time you need to make a studio booking
2
+ question: What is the minimum lead time you need to make a studio booking?
3
+
4
+ depends:
5
+ - "on": lot
6
+ being:
7
+ - user-research-studios-and-participants
8
+
9
+ type: radios
10
+
11
+ options:
12
+ - label: "1 day"
13
+ - label: "2 days"
14
+ - label: "3 to 5 days"
15
+ - label: "1 week"
16
+ - label: "2 weeks"
17
+ - label: "3 weeks"
18
+ - label: "1 month"
19
+ - label: "6 weeks"
20
+ - label: "More than 6 weeks"
21
+
22
+ depends_on_follow_up:
23
+ whatTypesOfUserResearchStudio:
24
+ - virtual-studio
25
+
26
+ validations:
27
+ -
28
+ name: answer_required
29
+ message: Select the minimum lead time you need to make a studio booking
@@ -0,0 +1,28 @@
1
+ name: Minimum amount of time a studio can be booked for
2
+ question: What is the minimum amount of time a studio can be booked for?
3
+
4
+ depends:
5
+ - "on": lot
6
+ being:
7
+ - user-research-studios-and-participants
8
+
9
+ type: radios
10
+
11
+ options:
12
+ - label: "1 hour"
13
+ - label: "2 hours"
14
+ - label: "3 hours"
15
+ - label: "4 hours"
16
+ - label: "5 hours"
17
+ - label: "6 hours"
18
+ - label: "7 hours"
19
+ - label: "8 hours"
20
+
21
+ depends_on_follow_up:
22
+ whatTypesOfUserResearchStudio:
23
+ - virtual-studio
24
+
25
+ validations:
26
+ -
27
+ name: answer_required
28
+ message: Select the minimum amount of time a studio can be booked for
@@ -0,0 +1,37 @@
1
+ name: Cost of booking a studio
2
+ question: How much does it cost (excluding VAT) to book a studio?
3
+
4
+ depends:
5
+ - "on": lot
6
+ being:
7
+ - user-research-studios-and-participants
8
+
9
+ type: pricing
10
+ fields:
11
+ price: virtualStudioBookingCostPrice
12
+ price_unit: virtualStudioBookingCostUnit
13
+
14
+ units:
15
+ - label: an hour
16
+ value: Hour
17
+ - label: a half day
18
+ value: Half day
19
+ - label: a day
20
+ value: Day
21
+ - label: a week
22
+ value: Week
23
+
24
+ depends_on_follow_up:
25
+ whatTypesOfUserResearchStudio:
26
+ - virtual-studio
27
+
28
+ validations:
29
+ - name: answer_required
30
+ field: virtualStudioBookingCostPrice
31
+ message: Enter how much it costs to book a studio
32
+ - name: not_money_format
33
+ field: virtualStudioBookingCostPrice
34
+ message: Price must be a number, without units, eg 99.95
35
+ - name: answer_required
36
+ field: virtualStudioBookingCostUnit
37
+ message: You must select the unit
@@ -18,6 +18,22 @@ depends_on_follow_up:
18
18
  whatUserResearchServices:
19
19
  - user-research-studios
20
20
 
21
+ dependent_follow_up:
22
+ tradeOrIndustryBodies:
23
+ - virtual-studio
24
+ virtualMinimumStudioTime:
25
+ - virtual-studio
26
+ virtualStudioBookingCostPrice:
27
+ - virtual-studio
28
+ virtualStudioBookingCostUnit:
29
+ - virtual-studio
30
+ virtualMinimumLeadTime:
31
+ - virtual-studio
32
+
33
+ dependent_follow_up_followup:
34
+ tradeOrIndustryBodiesOther:
35
+ - virtual-studio
36
+
21
37
  validations:
22
38
  -
23
39
  name: answer_required
@@ -33,12 +33,24 @@ dependent_follow_up:
33
33
  - user-research-participants
34
34
  whatTypesOfUserResearchStudio:
35
35
  - user-research-studios
36
+ tradeOrIndustryBodies:
37
+ - user-research-studios
38
+ virtualMinimumStudioTime:
39
+ - user-research-studios
40
+ virtualStudioBookingCostPrice:
41
+ - user-research-studios
42
+ virtualStudioBookingCostUnit:
43
+ - user-research-studios
44
+ virtualMinimumLeadTime:
45
+ - user-research-studios
36
46
 
37
47
  dependent_follow_up_followup:
38
48
  otherTradeOrIndustryBodiesOther:
39
49
  - user-research-participants
40
50
  icoRegistrationReference:
41
51
  - user-research-participants
52
+ tradeOrIndustryBodiesOther:
53
+ - user-research-studios
42
54
 
43
55
  validations:
44
56
  -
@@ -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.1",
3
+ "version": "3.5.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)
@@ -501,8 +501,10 @@ def pricing_property(question):
501
501
  )
502
502
 
503
503
  if 'price_unit' in question.fields:
504
- pricing[question.fields['price_unit']] = {
505
- "enum": [
504
+ if question.get('units'):
505
+ units = [unit.get('value', unit.get('label')) for unit in question.get('units')]
506
+ else:
507
+ units = [
506
508
  "Unit",
507
509
  "Person",
508
510
  "Licence",
@@ -516,13 +518,18 @@ def pricing_property(question):
516
518
  "Gigabyte",
517
519
  "Terabyte"
518
520
  ]
521
+
522
+ pricing[question.fields['price_unit']] = {
523
+ "enum": units
519
524
  }
520
525
  if 'price_unit' in question.get('optional_fields', []):
521
526
  pricing[question.fields['price_unit']]['enum'].insert(0, "")
522
527
 
523
528
  if 'price_interval' in question.fields:
524
- pricing[question.fields['price_interval']] = {
525
- "enum": [
529
+ if question.get('intervals'):
530
+ intervals = [interval.get('value', interval.get('label')) for interval in question.get('intervals')]
531
+ else:
532
+ intervals = [
526
533
  "Second",
527
534
  "Minute",
528
535
  "Hour",
@@ -533,6 +540,9 @@ def pricing_property(question):
533
540
  "6 months",
534
541
  "Year"
535
542
  ]
543
+
544
+ pricing[question.fields['price_interval']] = {
545
+ "enum": intervals
536
546
  }
537
547
  if 'price_interval' in question.get('optional_fields', []):
538
548
  pricing[question.fields['price_interval']]['enum'].insert(0, "")
@@ -367,6 +367,42 @@
367
367
  "required": [
368
368
  "passIfIn"
369
369
  ]
370
+ },
371
+ "units": {
372
+ "type": "array",
373
+ "items": {
374
+ "type": "object",
375
+ "properties": {
376
+ "label": {
377
+ "type": "string"
378
+ },
379
+ "value": {
380
+ "type": "string"
381
+ }
382
+ },
383
+ "additionalProperties": false,
384
+ "required": [
385
+ "label"
386
+ ]
387
+ }
388
+ },
389
+ "intervals": {
390
+ "type": "array",
391
+ "items": {
392
+ "type": "object",
393
+ "properties": {
394
+ "label": {
395
+ "type": "string"
396
+ },
397
+ "value": {
398
+ "type": "string"
399
+ }
400
+ },
401
+ "additionalProperties": false,
402
+ "required": [
403
+ "label"
404
+ ]
405
+ }
370
406
  }
371
407
  },
372
408
  "additionalProperties": false,
@@ -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
 
@@ -558,8 +558,31 @@ def test_pricing_property_price_unit_and_interval():
558
558
  }
559
559
  cq = ContentQuestion(manifest)
560
560
  result = pricing_property(cq)
561
- assert isinstance(result['priceInterval']['enum'], list)
562
- assert isinstance(result['priceUnit']['enum'], list)
561
+ assert result['priceUnit']['enum'] == [
562
+ "Unit",
563
+ "Person",
564
+ "Licence",
565
+ "User",
566
+ "Device",
567
+ "Instance",
568
+ "Server",
569
+ "Virtual machine",
570
+ "Transaction",
571
+ "Megabyte",
572
+ "Gigabyte",
573
+ "Terabyte"
574
+ ]
575
+ assert result['priceInterval']['enum'] == [
576
+ "Second",
577
+ "Minute",
578
+ "Hour",
579
+ "Day",
580
+ "Week",
581
+ "Month",
582
+ "Quarter",
583
+ "6 months",
584
+ "Year"
585
+ ]
563
586
 
564
587
 
565
588
  def test_pricing_property_price_unit_and_interval_optional():
@@ -633,6 +656,80 @@ def test_pricing_property_digit_limits_decimal_place_restriction():
633
656
  assert '{1,14}' in result['priceMax']['pattern']
634
657
 
635
658
 
659
+ def test_pricing_property_price():
660
+ manifest = {
661
+ "id": "Test",
662
+ "type": "pricing",
663
+ "fields": {
664
+ "price": "price"
665
+ }
666
+ }
667
+ cq = ContentQuestion(manifest)
668
+ result = pricing_property(cq)
669
+ assert not result['price']['pattern'].startswith("^$|")
670
+
671
+
672
+ def test_pricing_property_custom_units():
673
+ manifest = {
674
+ "id": "Test",
675
+ "type": "pricing",
676
+ "fields": {
677
+ "price": "price",
678
+ "price_unit": "priceUnit"
679
+ },
680
+ "units": [
681
+ {
682
+ "label": "Option 1"
683
+ },
684
+ {
685
+ "label": "Option 2",
686
+ "value": "option-2"
687
+ },
688
+ {
689
+ "label": "Option 3",
690
+ "value": "option-3"
691
+ }
692
+ ]
693
+ }
694
+ cq = ContentQuestion(manifest)
695
+ result = pricing_property(cq)
696
+ assert result['priceUnit']['enum'] == [
697
+ "Option 1",
698
+ "option-2",
699
+ "option-3"
700
+ ]
701
+
702
+
703
+ def test_pricing_property_custom_intervals():
704
+ manifest = {
705
+ "id": "Test",
706
+ "type": "pricing",
707
+ "fields": {
708
+ "price": "price",
709
+ "price_interval": "priceInterval"
710
+ },
711
+ "intervals": [
712
+ {
713
+ "label": "Option 1",
714
+ "value": "option-1"
715
+ },
716
+ {
717
+ "label": "Option 2"
718
+ },
719
+ {
720
+ "label": "Option 3"
721
+ }
722
+ ]
723
+ }
724
+ cq = ContentQuestion(manifest)
725
+ result = pricing_property(cq)
726
+ assert result['priceInterval']['enum'] == [
727
+ "option-1",
728
+ "Option 2",
729
+ "Option 3"
730
+ ]
731
+
732
+
636
733
  @given(st.text())
637
734
  def test_number_property(id):
638
735
  actual = number_property({'id': id, 'type': 'number'})