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.
- package/.github/workflows/test.yml +1 -0
- package/README.md +3 -0
- package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service.yml +12 -0
- package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_service_as_admin.yml +12 -0
- package/frameworks/digital-outcomes-and-specialists-7/manifests/edit_submission.yml +12 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/multiqTradeOrIndustryBodies.yml +17 -0
- 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/virtualMinimumLeadTime.yml +29 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/virtualMinimumStudioTime.yml +28 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/virtualStudioBookingCost.yml +37 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatTypesOfUserResearchStudio.yml +16 -0
- package/frameworks/digital-outcomes-and-specialists-7/questions/services/whatUserResearchServices.yml +12 -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/schema_generator/validation.py +14 -4
- package/schemas/questions.json +36 -0
- package/scripts/generate-filter-schemas.py +47 -0
- package/tests/test_content.py +47 -9
- 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
|
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"
|
package/frameworks/digital-outcomes-and-specialists-7/questions/services/virtualMinimumLeadTime.yml
ADDED
|
@@ -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
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccs-digitalmarketplace-frameworks",
|
|
3
|
-
"version": "3.
|
|
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"
|
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)
|
|
@@ -501,8 +501,10 @@ def pricing_property(question):
|
|
|
501
501
|
)
|
|
502
502
|
|
|
503
503
|
if 'price_unit' in question.fields:
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
525
|
-
|
|
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, "")
|
package/schemas/questions.json
CHANGED
|
@@ -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')
|
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
|
|
|
@@ -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
|
|
562
|
-
|
|
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'})
|