smartdown 0.11.2 → 0.11.3
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.
- data/README.md +87 -400
- data/lib/smartdown/api/postcode_question.rb +8 -0
- data/lib/smartdown/api/previous_question.rb +2 -0
- data/lib/smartdown/api/question_page.rb +3 -0
- data/lib/smartdown/model/answer/postcode.rb +33 -0
- data/lib/smartdown/model/element/question/postcode.rb +15 -0
- data/lib/smartdown/parser/directory_input.rb +1 -1
- data/lib/smartdown/parser/element/postcode_question.rb +20 -0
- data/lib/smartdown/parser/node_interpreter.rb +5 -4
- data/lib/smartdown/parser/node_parser.rb +2 -0
- data/lib/smartdown/parser/node_transform.rb +9 -41
- data/lib/smartdown/version.rb +1 -1
- data/spec/fixtures/directory_input/outcomes/nested/o1.txt +1 -0
- data/spec/model/answer/postcode_spec.rb +31 -0
- data/spec/parser/directory_input_spec.rb +17 -9
- data/spec/parser/element/postcode_question_spec.rb +57 -0
- metadata +37 -16
data/README.md
CHANGED
@@ -1,444 +1,131 @@
|
|
1
1
|
# Smartdown [](https://travis-ci.org/alphagov/smartdown)
|
2
2
|
|
3
|
-
Smartdown is
|
4
|
-
DSL](http://www.martinfowler.com/bliki/DomainSpecificLanguage.html) for
|
5
|
-
representing a series of questions and logical rules which determine the order
|
6
|
-
in which the questions are asked, according to user input.
|
3
|
+
Smartdown is a [custom formatting language](http://www.martinfowler.com/bliki/DomainSpecificLanguage.html) designed to generate HTML formatted questions. These questions can then be joined in a manner that articulates a full user journey.
|
7
4
|
|
8
|
-
|
9
|
-
[Markdown](http://daringfireball.net/projects/markdown/), with some extensions
|
10
|
-
to allow expression of logical rules, questions and conditional blocks of
|
11
|
-
text.
|
5
|
+
Implementation details for each kind of question can be found in the [documentation directory](doc).
|
12
6
|
|
13
|
-
|
14
|
-
|
15
|
-
A single smartdown flow has a cover sheet, a set of questions, a set of
|
16
|
-
outcomes and a set of test scenarios. Cover sheets, questions and outcomes are
|
17
|
-
all types of node. A node represents a single user interaction (normally a web
|
18
|
-
page, but in other media may be presented differently).
|
19
|
-
|
20
|
-
Each question and outcome is held in a separate file. The name of the files
|
21
|
-
are used to identify each question. Here's an example of the check-uk-visa
|
22
|
-
flow:
|
23
|
-
|
24
|
-
```
|
25
|
-
-- check-uk-visa
|
26
|
-
|-- outcomes
|
27
|
-
| |-- general_y.txt
|
28
|
-
| |-- joining_family_m.txt
|
29
|
-
| |-- joining_family_y.txt
|
30
|
-
| |-- marriage.txt
|
31
|
-
| |-- medical_n.txt
|
32
|
-
| |-- medical_y.txt
|
33
|
-
| `-- ...
|
34
|
-
|-- scenarios
|
35
|
-
| |-- 1.txt
|
36
|
-
| |-- 2.txt
|
37
|
-
| `-- 3.txt
|
38
|
-
|-- questions
|
39
|
-
| |-- planning_to_leave_airport.txt
|
40
|
-
| |-- purpose_of_visit.txt
|
41
|
-
| |-- staying_for_how_long.txt
|
42
|
-
| |-- what_passport_do_you_have.txt
|
43
|
-
`-- check-uk-visa.txt
|
44
|
-
```
|
45
|
-
|
46
|
-
## General node file syntax
|
47
|
-
|
48
|
-
Each file has three parts: front-matter, a model definition, rules/logic. Only the model definition is required.
|
49
|
-
|
50
|
-
* **front-matter** defines metadata in the form `property: value`. Note: this
|
51
|
-
does not support full YAML syntax.
|
52
|
-
* the **model definition* is a markdown-like block which defines a flow,
|
53
|
-
question or outcome.
|
54
|
-
* **rules/logic** defines 'next node' transition rules or other
|
55
|
-
logic/predicate definitions
|
56
|
-
|
57
|
-
## Cover sheet node
|
58
|
-
|
59
|
-
The cover sheet starts the flow off, its filename should match the flow name,
|
60
|
-
e.g. 'check-uk-visa.txt'.
|
61
|
-
|
62
|
-
It has initial 'front matter' which defines metadata for the flow. It then
|
63
|
-
defines the copy for the cover sheet in markdown format. The h1 title is
|
64
|
-
compulsory and used as the title for the smart answer.
|
65
|
-
|
66
|
-
A start button determines which question node is presented first.
|
67
|
-
|
68
|
-
```
|
69
|
-
meta_description: You may need a visa to come to the UK to visit, study or work.
|
70
|
-
satisfies_need: 100982
|
71
|
-
|
72
|
-
# Check if you need a UK visa
|
73
|
-
|
74
|
-
You may need a visa to come to the UK to visit, study or work.
|
75
|
-
|
76
|
-
[start_button: what_passport_do_you_have]
|
77
|
-
```
|
78
|
-
|
79
|
-
## Question nodes
|
80
|
-
|
81
|
-
Question nodes follow the same standard structure outlined above.
|
82
|
-
|
83
|
-
Smartdown currently allows multiple questions to be defined per node, but this
|
84
|
-
feature has only [recently been introduced](CHANGELOG.md#010) and may still change.
|
85
|
-
|
86
|
-
The next sections define the various question types available.
|
87
|
-
|
88
|
-
Note that at present only the 'choice' question type has been implemented.
|
89
|
-
Unimplemented question types are marked with **(tbd)** in the heading. For
|
90
|
-
these question types, consider this documentation to be a proposal of how they
|
91
|
-
might work.
|
92
|
-
|
93
|
-
### "Choice" questions (aka. radio buttons)
|
94
|
-
|
95
|
-
A choice question allows the user to select a single option from a list of choices.
|
96
|
-
|
97
|
-
```markdown
|
98
|
-
## Will you pass through UK Border Control?
|
99
|
-
|
100
|
-
You might pass through UK Border Control even if you don't leave the airport -
|
101
|
-
eg your bags aren't checked through and you need to collect them before transferring
|
102
|
-
to your outbound flight.
|
103
|
-
|
104
|
-
[choice: uk_border_control]
|
105
|
-
* yes: Yes
|
106
|
-
* no: No
|
107
|
-
```
|
108
|
-
|
109
|
-
### 'Country' question
|
110
|
-
|
111
|
-
A 'country' question allows the user to select a country from a drop-down list.
|
112
|
-
|
113
|
-
```markdown
|
114
|
-
## Where do you want to get married?
|
115
|
-
|
116
|
-
[country: marriage_country, countries: all_countries]
|
117
|
-
```
|
118
|
-
|
119
|
-
Where `all_countries` is the name of a data-plugin method that will return a hash of
|
120
|
-
country slugs/names.
|
121
|
-
|
122
|
-
### Date
|
123
|
-
|
124
|
-
```markdown
|
125
|
-
## What is the baby’s due date?
|
126
|
-
|
127
|
-
[date: baby_due_date]
|
128
|
-
```
|
129
|
-
|
130
|
-
To control the range of years selected you can supply 2 optional arguments to date questions: `from` and `to`.
|
131
|
-
These can take the form of absolute values, eg.
|
132
|
-
|
133
|
-
```markdown
|
134
|
-
[date: baby_due_date, from: 2010, to: 2015]
|
135
|
-
```
|
136
|
-
|
137
|
-
Or relative values (from the current year), eg.
|
138
|
-
|
139
|
-
```markdown
|
140
|
-
[date: baby_due_date, from: -4, to: 1]
|
141
|
-
```
|
142
|
-
|
143
|
-
The default values for `from` and `to` are relative years: `-1` and `3` respectively.
|
144
|
-
|
145
|
-
### Text
|
146
|
-
|
147
|
-
```markdown
|
148
|
-
[text: text_value]
|
149
|
-
```
|
150
|
-
|
151
|
-
Asks for an arbitrary text input.
|
152
|
-
|
153
|
-
### Salary
|
154
|
-
|
155
|
-
```markdown
|
156
|
-
[salary: salary_value]
|
157
|
-
```
|
158
|
-
|
159
|
-
## Aliases
|
160
|
-
|
161
|
-
An alias lets you referrer to any question identifier by its question intentifer or its alias.
|
162
|
-
|
163
|
-
```markdown
|
164
|
-
## Are you Clark Kent?
|
165
|
-
|
166
|
-
[choice: clark_kent, alias: superman]
|
167
|
-
* yes: Yes
|
168
|
-
* no: No
|
169
|
-
```
|
170
|
-
|
171
|
-
## Next steps
|
172
|
-
|
173
|
-
Markdown to be displayed as part of an outcome to direct the users to other information of potential interest to them.
|
174
|
-
|
175
|
-
```markdown
|
176
|
-
[next_steps]
|
177
|
-
* Any kind of markdown
|
178
|
-
[A link](https://gov.uk/somewhere)
|
179
|
-
[end_next_steps]
|
180
|
-
```
|
181
|
-
|
182
|
-
## Next node rules
|
183
|
-
|
184
|
-
Logical rules for transitioning to the next node are defined in 'Next node' section. This is declared using a markdown h1 'Next node'.
|
185
|
-
|
186
|
-
There are two constructs for defining rules:
|
187
|
-
|
188
|
-
```
|
189
|
-
# Next node
|
190
|
-
|
191
|
-
* predicate => outcome
|
192
|
-
```
|
193
|
-
|
194
|
-
defines a conditional transition
|
195
|
-
|
196
|
-
```
|
197
|
-
# Next node
|
198
|
-
|
199
|
-
* reddish?
|
200
|
-
* yellowish? => orange
|
201
|
-
* blueish? => purple
|
202
|
-
```
|
203
|
-
|
204
|
-
defines nested rules.
|
205
|
-
|
206
|
-
In the example above the node `orange` would be selected if both `reddish?` and `yellowish?` were true.
|
207
|
-
|
208
|
-
## Predicates
|
209
|
-
|
210
|
-
As well as 'named' predicates which might be defined by a plugin or other
|
211
|
-
mechanism, there's also a basic expression language for predicates.
|
212
|
-
|
213
|
-
The currently supported operations are:
|
214
|
-
|
215
|
-
```
|
216
|
-
variable_name is 'string'
|
217
|
-
variable_name in {this that the-other}
|
218
|
-
```
|
219
|
-
|
220
|
-
### Date comparison predicates (tbd)
|
221
|
-
|
222
|
-
```
|
223
|
-
date_variable_name >= '14/07/2014'
|
224
|
-
date_variable_name < '14/07/2014'
|
225
|
-
```
|
226
|
-
|
227
|
-
### Logical connectives
|
228
|
-
|
229
|
-
There are operators that can be used to combine predicates, or invert
|
230
|
-
their value. Namely NOT, OR and AND.
|
231
|
-
|
232
|
-
eg.
|
233
|
-
|
234
|
-
```
|
235
|
-
variable_name is 'string' OR NOT variable name is 'date'
|
236
|
-
```
|
237
|
-
|
238
|
-
`OR` connectives join a sequence of predicates and will return true if
|
239
|
-
any of them evaluate to true, otherwise false.
|
240
|
-
|
241
|
-
`AND` connectives join a sequence of predicates and will return true if
|
242
|
-
all of them evaluate to true, otherwise false.
|
243
|
-
|
244
|
-
`NOT` connectives will invert the return value of a predicate. ie turn
|
245
|
-
true to false and vice versa. They have high precedence so bind to a single
|
246
|
-
predicate in chain eg in:
|
247
|
-
|
248
|
-
```
|
249
|
-
NOT variable_name is 'lovely name' OR variable_name is 'special name'
|
250
|
-
```
|
251
|
-
|
252
|
-
The implied parentheses around the experssion are:
|
253
|
-
|
254
|
-
```
|
255
|
-
(NOT variable_name is 'lovely name') OR variable_name is 'special name'
|
256
|
-
```
|
257
|
-
|
258
|
-
For more information on Logical Connectives see:
|
259
|
-
|
260
|
-
http://en.wikipedia.org/wiki/Logical_connective
|
261
|
-
|
262
|
-
|
263
|
-
## Processing model
|
264
|
-
|
265
|
-
Each response to a question is assigned to a variable which corresponds to the
|
266
|
-
question name (as determined by the filename).
|
267
|
-
|
268
|
-
## Conditional blocks in outcomes
|
269
|
-
|
270
|
-
The syntax is:
|
271
|
-
|
272
|
-
```markdown
|
273
|
-
|
274
|
-
$IF pred?
|
275
|
-
|
276
|
-
Text if true
|
277
|
-
|
278
|
-
more text if you like
|
279
|
-
|
280
|
-
$ENDIF
|
281
|
-
```
|
282
|
-
|
283
|
-
You can also have an else clause:
|
284
|
-
|
285
|
-
```markdown
|
286
|
-
|
287
|
-
$IF pred?
|
288
|
-
|
289
|
-
Text if true
|
290
|
-
|
291
|
-
$ELSE
|
292
|
-
|
293
|
-
Text if false
|
294
|
-
|
295
|
-
$ENDIF
|
296
|
-
```
|
297
|
-
|
298
|
-
It's required to have a blank line between each if statement and the next paragraph of text, in other words this would be **invalid**:
|
299
|
-
|
300
|
-
```markdown
|
301
|
-
|
302
|
-
$IF pred?
|
303
|
-
Text if true
|
304
|
-
$ENDIF
|
305
|
-
```
|
7
|
+
For example:
|
306
8
|
|
307
|
-
Similarly, it is also possible to specify an elseif clause. These can be
|
308
|
-
chained together indefinitely. It is also possible to keep an else
|
309
|
-
clause at the end like so:
|
310
9
|
|
311
10
|
```markdown
|
312
|
-
|
313
|
-
|
314
|
-
Text if pred1 true
|
315
|
-
|
316
|
-
$ELSEIF pred2?
|
317
|
-
|
318
|
-
Text if pred1 false and pred2 true
|
319
|
-
|
320
|
-
$ELSEIF pred3?
|
321
|
-
|
322
|
-
Text if pred1 and pred2 false, and pred3 true
|
323
|
-
|
324
|
-
$ELSE
|
325
|
-
|
326
|
-
Text if pred1, pred2, pred3 are false
|
327
|
-
|
328
|
-
$ENDIF
|
329
|
-
```
|
330
|
-
|
331
|
-
It is also possible to nest if statements: like so.
|
11
|
+
# A Formatting and Logic Language
|
332
12
|
|
333
|
-
|
13
|
+
Smartdown helps GOV.UK users find the information they need without
|
14
|
+
having to search through daunting official documentation.
|
334
15
|
|
335
|
-
|
16
|
+
## Some extra information you need to know before you start
|
336
17
|
|
337
|
-
|
18
|
+
* Like bullet points
|
19
|
+
* Can be used
|
20
|
+
* Throughout this journey
|
338
21
|
|
339
|
-
|
22
|
+
[start: step_1]
|
340
23
|
|
341
|
-
|
24
|
+
## Additional context after a start button
|
342
25
|
|
343
|
-
|
26
|
+
Any more information down here
|
344
27
|
```
|
345
28
|
|
346
|
-
|
29
|
+
Will produce:
|
347
30
|
|
348
|
-
|
31
|
+
```html
|
32
|
+
<div id="js-replaceable">
|
349
33
|
|
350
|
-
|
34
|
+
<header class="page-header group">
|
35
|
+
<div>
|
36
|
+
<h1>
|
37
|
+
A formatting and Logic Language
|
38
|
+
</h1>
|
39
|
+
</div>
|
40
|
+
</header>
|
351
41
|
|
352
|
-
|
42
|
+
<div class="article-container group">
|
43
|
+
<article role="article" class="group">
|
44
|
+
<div class="inner">
|
45
|
+
<div class="intro">
|
46
|
+
<p>Smartdown helps GOV.UK users find the information they need
|
47
|
+
without having to search through daunting official
|
48
|
+
documentation.</p>
|
353
49
|
|
354
|
-
|
50
|
+
<h2 id="some-extra-information-you-need-to-know-before-you-start">
|
51
|
+
Some extra information you need to know before you start
|
52
|
+
</h2>
|
355
53
|
|
356
|
-
|
357
|
-
|
54
|
+
<ul>
|
55
|
+
<li>Like bullet points</li>
|
56
|
+
<li>Can be used</li>
|
57
|
+
<li>Throughout this journey</li>
|
58
|
+
</ul>
|
358
59
|
|
359
|
-
|
60
|
+
<p class="get-started">
|
61
|
+
<a rel="nofollow" href="/step-1/y" class="big button">
|
62
|
+
Start now
|
63
|
+
</a>
|
64
|
+
</p>
|
65
|
+
</div>
|
360
66
|
|
361
|
-
|
362
|
-
|
67
|
+
<h2 id="additional-context-after-a-start-button">
|
68
|
+
Additional context after a start button
|
69
|
+
</h2>
|
70
|
+
<p>Any more information down here</p>
|
363
71
|
|
364
|
-
|
72
|
+
</div>
|
73
|
+
</article>
|
74
|
+
</div>
|
365
75
|
|
76
|
+
</div>
|
366
77
|
```
|
367
|
-
## My header
|
368
|
-
|
369
|
-
Markdown copy..
|
370
78
|
|
371
|
-
|
79
|
+
Which on GOV.UK will look like:
|
372
80
|
|
373
|
-
|
374
|
-
```
|
81
|
+

|
375
82
|
|
376
|
-
Where `snippet_name` is in a `snippets/` directory in the flow root with a `.txt`
|
377
|
-
extension, eg `my-flow-name/snippets/my_snippet.txt`.
|
378
83
|
|
379
|
-
The
|
380
|
-
|
381
|
-
###Snippet Organisation
|
382
|
-
|
383
|
-
You can organise related snippets into a sub-directory of arbitrary depth
|
384
|
-
|
385
|
-
For example:
|
386
|
-
|
387
|
-
```
|
388
|
-
## My header
|
84
|
+
The language is designed to look like [Markdown](http://daringfireball.net/projects/markdown/), but it has been extended to allow you to write in logic rules, questions and conditional blocks of text.
|
389
85
|
|
390
|
-
|
391
|
-
|
392
|
-
{{snippet: my_sub_directory/my_snippet}}
|
393
|
-
|
394
|
-
More copy...
|
395
|
-
```
|
396
|
-
Where `snippet_name` is in a `snippets/` directory in the flow root with a `.txt` extension, eg `my-flow-name/snippets/my_sub_directory/my_snippet.txt`.
|
397
|
-
|
398
|
-
## Scenarios
|
86
|
+
## Overview
|
399
87
|
|
400
|
-
|
401
|
-
|
88
|
+
A single smartdown flow has a [Start Page](doc/start-pages.md), a set of [Questions](doc/questions.md),
|
89
|
+
[Outcomes](doc/outcomes.md) and a set of [Test Scenarios](doc/scenarios.md).
|
402
90
|
|
403
|
-
|
91
|
+
Start Pages, Questions and Outcomes are all a type of 'node'.
|
92
|
+
A node represents a single user interaction (normally a web page, but in other media may be presented differently).
|
404
93
|
|
405
|
-
|
406
|
-
scenario file should contain scenarios written as documented below.
|
94
|
+
Each question and outcome is held in its own file. The name of the files are significant: they are used to identify each question.
|
407
95
|
|
408
|
-
###Format
|
409
96
|
|
410
|
-
|
411
|
-
* a description (optional)
|
412
|
-
* list of questions pages (each question page starts with a -), inside which questions to answers are defined
|
413
|
-
* name of the outcome
|
97
|
+
Here's an example of the check-uk-visa flow:
|
414
98
|
|
415
99
|
```
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
100
|
+
-- check-uk-visa
|
101
|
+
|-- outcomes
|
102
|
+
| |-- general_y.txt
|
103
|
+
| |-- joining_family_m.txt
|
104
|
+
| |-- joining_family_y.txt
|
105
|
+
| |-- marriage.txt
|
106
|
+
| |-- medical_n.txt
|
107
|
+
| |-- medical_y.txt
|
108
|
+
| `-- ...
|
109
|
+
|-- scenarios
|
110
|
+
| |-- 1.txt
|
111
|
+
| |-- 2.txt
|
112
|
+
| `-- 3.txt
|
113
|
+
|-- questions
|
114
|
+
| |-- planning_to_leave_airport.txt
|
115
|
+
| |-- purpose_of_visit.txt
|
116
|
+
| |-- staying_for_how_long.txt
|
117
|
+
| |-- what_passport_do_you_have.txt
|
118
|
+
`-- check-uk-visa.txt
|
428
119
|
```
|
429
120
|
|
430
|
-
##
|
431
|
-
|
432
|
-
####Answers vs responses
|
121
|
+
## Wiki
|
433
122
|
|
434
|
-
|
435
|
-
Both are used to describe an answer to a question, but indicate two different formats:
|
436
|
-
* ```response``` is used for raw string inputs
|
437
|
-
* ```answer``` is used for Model::Answer objects
|
123
|
+
Additional documentation and a [glossary of terms](https://github.com/alphagov/smartdown/wiki/Glossary) and concepts can be found in the [project wiki](https://github.com/alphagov/smartdown/wiki/) or in the [documentation folder](doc)
|
438
124
|
|
439
|
-
|
125
|
+
### Dependencies
|
440
126
|
|
441
|
-
|
127
|
+
Currently smartdown relies on the [Smart Answers](https://github.com/alphagov/smart-answers/) application to run.
|
442
128
|
|
443
|
-
|
129
|
+
### Running
|
444
130
|
|
131
|
+
For GOV.UK developers you can `bowl smartanswers` and navigate to an example flow such as http://smartanswers.dev.gov.uk/animal-example-multiple from within the GOV.UK VM.
|
@@ -18,6 +18,8 @@ module Smartdown
|
|
18
18
|
@question = SalaryQuestion.new(elements)
|
19
19
|
elsif element = elements.find{|element| element.is_a? Smartdown::Model::Element::Question::Text}
|
20
20
|
@question = TextQuestion.new(elements)
|
21
|
+
elsif element = elements.find{|element| element.is_a? Smartdown::Model::Element::Question::Postcode}
|
22
|
+
@question = PostcodeQuestion.new(elements)
|
21
23
|
end
|
22
24
|
@answer = element.answer_type.new(response, element) if element
|
23
25
|
end
|
@@ -3,6 +3,7 @@ require 'smartdown/api/date_question'
|
|
3
3
|
require 'smartdown/api/country_question'
|
4
4
|
require 'smartdown/api/salary_question'
|
5
5
|
require 'smartdown/api/text_question'
|
6
|
+
require 'smartdown/api/postcode_question'
|
6
7
|
|
7
8
|
module Smartdown
|
8
9
|
module Api
|
@@ -21,6 +22,8 @@ module Smartdown
|
|
21
22
|
Smartdown::Api::SalaryQuestion.new(question_element_group)
|
22
23
|
elsif question_element_group.find{|element| element.is_a? Smartdown::Model::Element::Question::Text}
|
23
24
|
Smartdown::Api::TextQuestion.new(question_element_group)
|
25
|
+
elsif question_element_group.find{|element| element.is_a? Smartdown::Model::Element::Question::Postcode}
|
26
|
+
Smartdown::Api::PostcodeQuestion.new(question_element_group)
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative "base"
|
3
|
+
require "uk_postcode"
|
4
|
+
|
5
|
+
module Smartdown
|
6
|
+
module Model
|
7
|
+
module Answer
|
8
|
+
class Postcode < Base
|
9
|
+
|
10
|
+
def value_type
|
11
|
+
::String
|
12
|
+
end
|
13
|
+
|
14
|
+
def humanize
|
15
|
+
value
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def parse_value(value)
|
20
|
+
postcode = UKPostcode.new(value)
|
21
|
+
if !postcode.valid?
|
22
|
+
@error = "Invalid postcode"
|
23
|
+
return
|
24
|
+
elsif !postcode.full?
|
25
|
+
@error = "Please enter a full postcode"
|
26
|
+
return
|
27
|
+
end
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'smartdown/model/answer/postcode'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Model
|
5
|
+
module Element
|
6
|
+
module Question
|
7
|
+
class Postcode < Struct.new(:name, :alias)
|
8
|
+
def answer_type
|
9
|
+
Smartdown::Model::Answer::Postcode
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'smartdown/parser/question'
|
2
|
+
|
3
|
+
module Smartdown
|
4
|
+
module Parser
|
5
|
+
module Element
|
6
|
+
class PostcodeQuestion < Question
|
7
|
+
|
8
|
+
rule(:question_type) {
|
9
|
+
str("postcode")
|
10
|
+
}
|
11
|
+
|
12
|
+
rule(:postcode_question) {
|
13
|
+
question_tag.as(:postcode)
|
14
|
+
}
|
15
|
+
|
16
|
+
root(:postcode_question)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -7,20 +7,21 @@ require 'smartdown/parser/node_transform'
|
|
7
7
|
module Smartdown
|
8
8
|
module Parser
|
9
9
|
class NodeInterpreter
|
10
|
-
attr_reader :name, :source, :reporter
|
10
|
+
attr_reader :name, :source, :reporter, :data_module
|
11
11
|
|
12
12
|
def initialize(name, source, options = {})
|
13
13
|
@name = name
|
14
14
|
@source = source
|
15
|
-
data_module = options.fetch(:data_module, {})
|
15
|
+
@data_module = options.fetch(:data_module, {})
|
16
16
|
@parser = options.fetch(:parser, Smartdown::Parser::NodeParser.new)
|
17
|
-
@transform = options.fetch(:transform, Smartdown::Parser::NodeTransform.new
|
17
|
+
@transform = options.fetch(:transform, Smartdown::Parser::NodeTransform.new)
|
18
18
|
@reporter = options.fetch(:reporter, Parslet::ErrorReporter::Deepest.new)
|
19
19
|
end
|
20
20
|
|
21
21
|
def interpret
|
22
22
|
transform.apply(parser.parse(source, reporter: reporter),
|
23
|
-
node_name: name
|
23
|
+
node_name: name,
|
24
|
+
data_module: data_module,
|
24
25
|
)
|
25
26
|
end
|
26
27
|
|
@@ -7,6 +7,7 @@ require 'smartdown/parser/element/date_question'
|
|
7
7
|
require 'smartdown/parser/element/salary_question'
|
8
8
|
require 'smartdown/parser/element/text_question'
|
9
9
|
require 'smartdown/parser/element/country_question'
|
10
|
+
require 'smartdown/parser/element/postcode_question'
|
10
11
|
require 'smartdown/parser/element/markdown_heading'
|
11
12
|
require 'smartdown/parser/element/markdown_paragraph'
|
12
13
|
require 'smartdown/parser/element/conditional'
|
@@ -23,6 +24,7 @@ module Smartdown
|
|
23
24
|
Element::SalaryQuestion.new |
|
24
25
|
Element::TextQuestion.new |
|
25
26
|
Element::CountryQuestion.new |
|
27
|
+
Element::PostcodeQuestion.new |
|
26
28
|
Rules.new |
|
27
29
|
Element::StartButton.new |
|
28
30
|
Element::NextSteps.new |
|
@@ -10,6 +10,7 @@ require 'smartdown/model/element/question/country'
|
|
10
10
|
require 'smartdown/model/element/question/date'
|
11
11
|
require 'smartdown/model/element/question/salary'
|
12
12
|
require 'smartdown/model/element/question/text'
|
13
|
+
require 'smartdown/model/element/question/postcode'
|
13
14
|
require 'smartdown/model/element/start_button'
|
14
15
|
require 'smartdown/model/element/markdown_heading'
|
15
16
|
require 'smartdown/model/element/markdown_paragraph'
|
@@ -31,46 +32,6 @@ module Smartdown
|
|
31
32
|
module Parser
|
32
33
|
class NodeTransform < Parslet::Transform
|
33
34
|
|
34
|
-
attr_reader :data_module
|
35
|
-
|
36
|
-
def initialize data_module=nil, &block
|
37
|
-
super(&block)
|
38
|
-
|
39
|
-
@data_module = data_module || {}
|
40
|
-
end
|
41
|
-
|
42
|
-
# !!ALERT!! MONKEY PATCHING !!ALERT!!
|
43
|
-
#
|
44
|
-
# This call_on_match method is used for executing all the rule blocks you see
|
45
|
-
# below. The only variables that are in scope for these blocks are the contents
|
46
|
-
# of bindings - which consists of information about bits of the AST that the rule
|
47
|
-
# matched.
|
48
|
-
#
|
49
|
-
# In the country rule: there is a need for accessing an external variable/method
|
50
|
-
# as the information required to create a country question object is defined in
|
51
|
-
# the data_module - so cannot be inferred purely from the syntax fed to parselet.
|
52
|
-
#
|
53
|
-
# There are 2 options we could have chosen, the first would be to have another
|
54
|
-
# transformation layer. We would create intermediate elements that were lacking
|
55
|
-
# information and then recreate them outside of parselet.
|
56
|
-
#
|
57
|
-
# A far simpler option is to manually modify the set of bindings available to
|
58
|
-
# rule blocks, so we can inject our information from the data_module. Unfortunately
|
59
|
-
# the only way to do this is to Monkey patch the call_on_match method to do the
|
60
|
-
# to injecting. The drawbacks of this are if the method changes its name or function
|
61
|
-
# in a newer parselet version; or possibly accidentally overriding some default
|
62
|
-
# bindings with methods from a data module.
|
63
|
-
#
|
64
|
-
# Ideally we would like a way of injecting information into bindings without this
|
65
|
-
# patch so we have submitted a PR to parselet describing this problem:
|
66
|
-
#
|
67
|
-
# https://github.com/kschiess/parslet/pull/119
|
68
|
-
#
|
69
|
-
def call_on_match(bindings, block)
|
70
|
-
bindings.merge! data_module
|
71
|
-
super(bindings, block)
|
72
|
-
end
|
73
|
-
|
74
35
|
rule(body: subtree(:body)) {
|
75
36
|
Smartdown::Model::Node.new(
|
76
37
|
node_name, body, Smartdown::Model::FrontMatter.new({})
|
@@ -120,7 +81,7 @@ module Smartdown
|
|
120
81
|
|
121
82
|
rule(:country => {identifier: simple(:identifier), :option_pairs => subtree(:option_pairs)}) {
|
122
83
|
country_data_method = Smartdown::Parser::OptionPairs.transform(option_pairs).fetch('countries', nil)
|
123
|
-
country_hash =
|
84
|
+
country_hash = data_module[country_data_method].call
|
124
85
|
Smartdown::Model::Element::Question::Country.new(
|
125
86
|
identifier.to_s,
|
126
87
|
country_hash,
|
@@ -152,6 +113,13 @@ module Smartdown
|
|
152
113
|
)
|
153
114
|
}
|
154
115
|
|
116
|
+
rule(:postcode => {identifier: simple(:identifier), :option_pairs => subtree(:option_pairs)}) {
|
117
|
+
Smartdown::Model::Element::Question::Postcode.new(
|
118
|
+
identifier.to_s,
|
119
|
+
Smartdown::Parser::OptionPairs.transform(option_pairs).fetch('alias', nil)
|
120
|
+
)
|
121
|
+
}
|
122
|
+
|
155
123
|
rule(:next_steps => { content: simple(:content) }) {
|
156
124
|
Smartdown::Model::Element::NextSteps.new(content.to_s)
|
157
125
|
}
|
data/lib/smartdown/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
nested outcome
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'smartdown/model/answer/postcode'
|
3
|
+
|
4
|
+
describe Smartdown::Model::Answer::Postcode do
|
5
|
+
|
6
|
+
let(:answer_string) { "WC2B 6SE" }
|
7
|
+
subject(:answer) { described_class.new(answer_string) }
|
8
|
+
|
9
|
+
describe "#humanize" do
|
10
|
+
it "declares itself in the initial format provided" do
|
11
|
+
expect(answer.humanize).to eql("WC2B 6SE")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "errors" do
|
16
|
+
context "partial postcodes are not allowed" do
|
17
|
+
let(:answer_string) { "WC2B" }
|
18
|
+
specify { expect(answer.error).to eql "Please enter a full postcode" }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "invalid postcode" do
|
22
|
+
let(:answer_string) { "invalid" }
|
23
|
+
specify { expect(answer.error).to eql "Invalid postcode" }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "question not answered" do
|
27
|
+
let(:answer_string) { nil }
|
28
|
+
specify { expect(answer.error).to eql "Please answer this question" }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -33,10 +33,17 @@ describe Smartdown::Parser::DirectoryInput do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
describe "#outcomes" do
|
36
|
-
it "returns an InputFile for every file in the outcomes folder" do
|
37
|
-
expect(input.outcomes).to match([
|
38
|
-
|
39
|
-
|
36
|
+
it "returns an InputFile for every file in the outcomes folder to arbitrary sub directory depth" do
|
37
|
+
expect(input.outcomes).to match([
|
38
|
+
instance_of(Smartdown::Parser::InputFile),
|
39
|
+
instance_of(Smartdown::Parser::InputFile),
|
40
|
+
])
|
41
|
+
|
42
|
+
expect(input.outcomes.map(&:name)).
|
43
|
+
to match_array(["o1", "nested/o1",])
|
44
|
+
|
45
|
+
expect(input.outcomes.map(&:read)).
|
46
|
+
to match_array(["outcome one\n", "nested outcome\n"])
|
40
47
|
end
|
41
48
|
end
|
42
49
|
|
@@ -49,16 +56,17 @@ describe Smartdown::Parser::DirectoryInput do
|
|
49
56
|
end
|
50
57
|
|
51
58
|
describe "#snippets" do
|
52
|
-
it "returns an InputFile for every file
|
59
|
+
it "returns an InputFile for every file in the snippets folder to arbitrary sub directory depth" do
|
53
60
|
expect(input.snippets).to match([
|
54
61
|
instance_of(Smartdown::Parser::InputFile),
|
55
62
|
instance_of(Smartdown::Parser::InputFile),
|
56
63
|
])
|
57
|
-
expect(input.snippets.map(&:name)).to include("sn1")
|
58
|
-
expect(input.snippets.map(&:read)).to include("snippet one\n")
|
59
64
|
|
60
|
-
expect(input.snippets.map(&:name)).
|
61
|
-
|
65
|
+
expect(input.snippets.map(&:name)).
|
66
|
+
to match_array(["sn1", "nested/nested_again/nsn1"])
|
67
|
+
|
68
|
+
expect(input.snippets.map(&:read)).
|
69
|
+
to match_array(["snippet one\n", "nested snippet\n"])
|
62
70
|
end
|
63
71
|
end
|
64
72
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'smartdown/parser/node_parser'
|
2
|
+
require 'smartdown/parser/node_interpreter'
|
3
|
+
require 'smartdown/parser/element/postcode_question'
|
4
|
+
|
5
|
+
describe Smartdown::Parser::Element::PostcodeQuestion do
|
6
|
+
subject(:parser) { described_class.new }
|
7
|
+
|
8
|
+
context "with question tag" do
|
9
|
+
let(:source) { "[postcode: home]" }
|
10
|
+
|
11
|
+
it "parses" do
|
12
|
+
should parse(source).as(
|
13
|
+
postcode: {
|
14
|
+
identifier: "home",
|
15
|
+
option_pairs: [],
|
16
|
+
},
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "transformed" do
|
21
|
+
let(:node_name) { "my_node" }
|
22
|
+
subject(:transformed) {
|
23
|
+
Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
|
24
|
+
}
|
25
|
+
|
26
|
+
it { should eq(Smartdown::Model::Element::Question::Postcode.new("home")) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "with question tag and alias" do
|
31
|
+
let(:source) { "[postcode: home, alias: sweet_home]" }
|
32
|
+
|
33
|
+
it "parses" do
|
34
|
+
should parse(source).as(
|
35
|
+
postcode: {
|
36
|
+
identifier: "home",
|
37
|
+
option_pairs:[
|
38
|
+
{
|
39
|
+
key: 'alias',
|
40
|
+
value: 'sweet_home',
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "transformed" do
|
48
|
+
let(:node_name) { "my_node" }
|
49
|
+
subject(:transformed) {
|
50
|
+
Smartdown::Parser::NodeInterpreter.new(node_name, source, parser: parser).interpret
|
51
|
+
}
|
52
|
+
|
53
|
+
it { should eq(Smartdown::Model::Element::Question::Postcode.new("home", "sweet_home")) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smartdown
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.11.
|
4
|
+
version: 0.11.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-12-
|
12
|
+
date: 2014-12-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: parslet
|
16
|
-
requirement: &
|
16
|
+
requirement: &22331140 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,21 @@ dependencies:
|
|
21
21
|
version: 1.6.1
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *22331140
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: uk_postcode
|
27
|
+
requirement: &22330540 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *22330540
|
25
36
|
- !ruby/object:Gem::Dependency
|
26
37
|
name: rspec
|
27
|
-
requirement: &
|
38
|
+
requirement: &22330040 !ruby/object:Gem::Requirement
|
28
39
|
none: false
|
29
40
|
requirements:
|
30
41
|
- - ~>
|
@@ -32,10 +43,10 @@ dependencies:
|
|
32
43
|
version: 3.0.0
|
33
44
|
type: :development
|
34
45
|
prerelease: false
|
35
|
-
version_requirements: *
|
46
|
+
version_requirements: *22330040
|
36
47
|
- !ruby/object:Gem::Dependency
|
37
48
|
name: rake
|
38
|
-
requirement: &
|
49
|
+
requirement: &22329660 !ruby/object:Gem::Requirement
|
39
50
|
none: false
|
40
51
|
requirements:
|
41
52
|
- - ! '>='
|
@@ -43,10 +54,10 @@ dependencies:
|
|
43
54
|
version: '0'
|
44
55
|
type: :development
|
45
56
|
prerelease: false
|
46
|
-
version_requirements: *
|
57
|
+
version_requirements: *22329660
|
47
58
|
- !ruby/object:Gem::Dependency
|
48
59
|
name: pry
|
49
|
-
requirement: &
|
60
|
+
requirement: &22329160 !ruby/object:Gem::Requirement
|
50
61
|
none: false
|
51
62
|
requirements:
|
52
63
|
- - ! '>='
|
@@ -54,10 +65,10 @@ dependencies:
|
|
54
65
|
version: '0'
|
55
66
|
type: :development
|
56
67
|
prerelease: false
|
57
|
-
version_requirements: *
|
68
|
+
version_requirements: *22329160
|
58
69
|
- !ruby/object:Gem::Dependency
|
59
70
|
name: gem_publisher
|
60
|
-
requirement: &
|
71
|
+
requirement: &22328660 !ruby/object:Gem::Requirement
|
61
72
|
none: false
|
62
73
|
requirements:
|
63
74
|
- - ! '>='
|
@@ -65,10 +76,10 @@ dependencies:
|
|
65
76
|
version: '0'
|
66
77
|
type: :development
|
67
78
|
prerelease: false
|
68
|
-
version_requirements: *
|
79
|
+
version_requirements: *22328660
|
69
80
|
- !ruby/object:Gem::Dependency
|
70
81
|
name: timecop
|
71
|
-
requirement: &
|
82
|
+
requirement: &22328220 !ruby/object:Gem::Requirement
|
72
83
|
none: false
|
73
84
|
requirements:
|
74
85
|
- - ! '>='
|
@@ -76,7 +87,7 @@ dependencies:
|
|
76
87
|
version: '0'
|
77
88
|
type: :development
|
78
89
|
prerelease: false
|
79
|
-
version_requirements: *
|
90
|
+
version_requirements: *22328220
|
80
91
|
description:
|
81
92
|
email: david.heath@digital.cabinet-office.gov.uk
|
82
93
|
executables:
|
@@ -99,6 +110,7 @@ files:
|
|
99
110
|
- lib/smartdown/parser/scenario_sets_interpreter.rb
|
100
111
|
- lib/smartdown/parser/element/date_question.rb
|
101
112
|
- lib/smartdown/parser/element/salary_question.rb
|
113
|
+
- lib/smartdown/parser/element/postcode_question.rb
|
102
114
|
- lib/smartdown/parser/element/conditional.rb
|
103
115
|
- lib/smartdown/parser/element/markdown_paragraph.rb
|
104
116
|
- lib/smartdown/parser/element/start_button.rb
|
@@ -114,6 +126,7 @@ files:
|
|
114
126
|
- lib/smartdown/api/coversheet.rb
|
115
127
|
- lib/smartdown/api/question.rb
|
116
128
|
- lib/smartdown/api/salary_question.rb
|
129
|
+
- lib/smartdown/api/postcode_question.rb
|
117
130
|
- lib/smartdown/api/flow.rb
|
118
131
|
- lib/smartdown/api/question_page.rb
|
119
132
|
- lib/smartdown/api/state.rb
|
@@ -154,6 +167,7 @@ files:
|
|
154
167
|
- lib/smartdown/model/answer/date.rb
|
155
168
|
- lib/smartdown/model/answer/base.rb
|
156
169
|
- lib/smartdown/model/answer/salary.rb
|
170
|
+
- lib/smartdown/model/answer/postcode.rb
|
157
171
|
- lib/smartdown/model/answer/country.rb
|
158
172
|
- lib/smartdown/model/answer/money.rb
|
159
173
|
- lib/smartdown/model/answer/multiple_choice.rb
|
@@ -165,6 +179,7 @@ files:
|
|
165
179
|
- lib/smartdown/model/element/markdown_heading.rb
|
166
180
|
- lib/smartdown/model/element/question/date.rb
|
167
181
|
- lib/smartdown/model/element/question/salary.rb
|
182
|
+
- lib/smartdown/model/element/question/postcode.rb
|
168
183
|
- lib/smartdown/model/element/question/country.rb
|
169
184
|
- lib/smartdown/model/element/question/multiple_choice.rb
|
170
185
|
- lib/smartdown/model/element/question/text.rb
|
@@ -183,6 +198,7 @@ files:
|
|
183
198
|
- spec/parser/input_set_spec.rb
|
184
199
|
- spec/parser/rules_spec.rb
|
185
200
|
- spec/parser/predicates_spec.rb
|
201
|
+
- spec/parser/element/postcode_question_spec.rb
|
186
202
|
- spec/parser/element/next_steps_spec.rb
|
187
203
|
- spec/parser/element/multiple_choice_question_spec.rb
|
188
204
|
- spec/parser/element/country_question_spec.rb
|
@@ -211,6 +227,7 @@ files:
|
|
211
227
|
- spec/engine/transition_spec.rb
|
212
228
|
- spec/engine/conditional_resolver_spec.rb
|
213
229
|
- spec/fixtures/directory_input/scenarios/s1.txt
|
230
|
+
- spec/fixtures/directory_input/outcomes/nested/o1.txt
|
214
231
|
- spec/fixtures/directory_input/outcomes/o1.txt
|
215
232
|
- spec/fixtures/directory_input/questions/q1.txt
|
216
233
|
- spec/fixtures/directory_input/cover-sheet.txt
|
@@ -259,6 +276,7 @@ files:
|
|
259
276
|
- spec/model/answer/base_spec.rb
|
260
277
|
- spec/model/answer/salary_spec.rb
|
261
278
|
- spec/model/answer/date_spec.rb
|
279
|
+
- spec/model/answer/postcode_spec.rb
|
262
280
|
- spec/model/answer/country_spec.rb
|
263
281
|
- spec/model/predicates/not_operation_spec.rb
|
264
282
|
- spec/model/predicates/set_membership_spec.rb
|
@@ -283,7 +301,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
283
301
|
version: '0'
|
284
302
|
segments:
|
285
303
|
- 0
|
286
|
-
hash:
|
304
|
+
hash: -2697176231460023822
|
287
305
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
288
306
|
none: false
|
289
307
|
requirements:
|
@@ -292,7 +310,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
292
310
|
version: '0'
|
293
311
|
segments:
|
294
312
|
- 0
|
295
|
-
hash:
|
313
|
+
hash: -2697176231460023822
|
296
314
|
requirements: []
|
297
315
|
rubyforge_project:
|
298
316
|
rubygems_version: 1.8.11
|
@@ -309,6 +327,7 @@ test_files:
|
|
309
327
|
- spec/parser/input_set_spec.rb
|
310
328
|
- spec/parser/rules_spec.rb
|
311
329
|
- spec/parser/predicates_spec.rb
|
330
|
+
- spec/parser/element/postcode_question_spec.rb
|
312
331
|
- spec/parser/element/next_steps_spec.rb
|
313
332
|
- spec/parser/element/multiple_choice_question_spec.rb
|
314
333
|
- spec/parser/element/country_question_spec.rb
|
@@ -337,6 +356,7 @@ test_files:
|
|
337
356
|
- spec/engine/transition_spec.rb
|
338
357
|
- spec/engine/conditional_resolver_spec.rb
|
339
358
|
- spec/fixtures/directory_input/scenarios/s1.txt
|
359
|
+
- spec/fixtures/directory_input/outcomes/nested/o1.txt
|
340
360
|
- spec/fixtures/directory_input/outcomes/o1.txt
|
341
361
|
- spec/fixtures/directory_input/questions/q1.txt
|
342
362
|
- spec/fixtures/directory_input/cover-sheet.txt
|
@@ -385,6 +405,7 @@ test_files:
|
|
385
405
|
- spec/model/answer/base_spec.rb
|
386
406
|
- spec/model/answer/salary_spec.rb
|
387
407
|
- spec/model/answer/date_spec.rb
|
408
|
+
- spec/model/answer/postcode_spec.rb
|
388
409
|
- spec/model/answer/country_spec.rb
|
389
410
|
- spec/model/predicates/not_operation_spec.rb
|
390
411
|
- spec/model/predicates/set_membership_spec.rb
|