doop 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +203 -0
- data/Rakefile +5 -0
- data/app/assets/images/dash.gif +0 -0
- data/app/assets/images/info-icon.png +0 -0
- data/app/assets/images/progress-tick-large.png +0 -0
- data/app/helpers/doop_helper.rb +51 -0
- data/bin/.gitignore +0 -0
- data/doop.gemspec +26 -0
- data/lib/doop.rb +313 -0
- data/lib/doop/version.rb +3 -0
- data/lib/doop_controller.rb +164 -0
- data/lib/generators/doopgovuk/USAGE +10 -0
- data/lib/generators/doopgovuk/doopgovuk_generator.rb +34 -0
- data/lib/generators/doopgovuk/templates/app/assets/stylesheets/demo.css.scss +188 -0
- data/lib/generators/doopgovuk/templates/app/controllers/demo_controller.rb +155 -0
- data/lib/generators/doopgovuk/templates/app/views/demo/_preamble.html.erb +33 -0
- data/lib/generators/doopgovuk/templates/app/views/demo/_summary.html.erb +10 -0
- data/lib/generators/doopgovuk/templates/app/views/demo/_your_details.html.erb +55 -0
- data/lib/generators/doopgovuk/templates/app/views/demo/index.html.erb +3 -0
- data/lib/generators/doopgovuk/templates/app/views/demo/index.js.erb +4 -0
- data/lib/generators/doopgovuk/templates/app/views/doop/_error.html.erb +3 -0
- data/lib/generators/doopgovuk/templates/app/views/doop/_info_box.html.erb +4 -0
- data/lib/generators/doopgovuk/templates/app/views/doop/_navbar.html.erb +23 -0
- data/lib/generators/doopgovuk/templates/app/views/doop/_question.html.erb +19 -0
- data/lib/generators/doopgovuk/templates/app/views/doop/_question_form.html.erb +15 -0
- data/lib/generators/doopgovuk/templates/app/views/layouts/application.html.erb +22 -0
- data/spec/doop_spec.rb +448 -0
- data/spec/spec_helper.rb +2 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDZjODg0NTdlY2FkODllYmE3YzhhMmJkYzhjNGFhOTUyZDAxYWJhMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MmI1MGNlMWVmNWQ0NTBiMDYzMjVlMzJhYzRhMmNhNWE5NDQ5MWQxNQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZWI3Y2UwYmUzNjhhNGI0YjU5ZTk2YmZiMTBhMDhlY2U2Nzk1Y2I2ZWNiNzY5
|
10
|
+
ZjA4OTNkNmI0YWNjMmIzMzIyZWNiMGFjZjE5OTlmMjJmZTZmMzYxMmZlODhl
|
11
|
+
ZTljMGRiODdkMDM2YWM5YjY1NmJiNjY1NzJhM2ZmYTQwNTI0N2I=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YmI5M2ViZDkzYjYyNzAwYmFlNjQzMmFjZGJkMTdjZmI5YzBiNmUwZGQzZDBi
|
14
|
+
YmE4ZGQyNjQ2M2RkMjg1ZjdkZWYyZmEyMTQzMmIxYTFmMTZhN2M0YmE2MTIz
|
15
|
+
NjQyYjkwNTI0MTllOWYxY2U1NmNmZmY2NzU3MGVhY2JhYmU1ZmE=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 coder36
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# Doop
|
2
|
+
|
3
|
+
A question framework for govuk sites, inspired by the great work GDS have done to standardize the cross government internet presence.
|
4
|
+
|
5
|
+
# Quick start
|
6
|
+
|
7
|
+
Assuming ruby, rails and nodejs is installed:
|
8
|
+
|
9
|
+
$ rails new govsite
|
10
|
+
$ cd govsite
|
11
|
+
$ echo "gem 'doop'" >> Gemfile
|
12
|
+
$ bundle
|
13
|
+
$ rails generate doopgovuk demo
|
14
|
+
$ rails s
|
15
|
+
|
16
|
+
Navigate to http://localhost:3000
|
17
|
+
|
18
|
+
This is still in development, so if you want the latest, then add `gem 'doop', git: 'git://github.com/coder36/doop.git'` to your Gemfile.
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
## Background
|
24
|
+
Whilst working with the Student Loans company and GDS we discovered that the best way to get a student to fill a form in was to progressively ask questions, one after the other rather than as one big form. User experience testing showed this was a far less intimidating experience, and they would more likely stick at it. Furthermore, based on pevious answers, we can choose which questions to ask. This makes each questionaire effectively tailored to the individual student.
|
25
|
+
|
26
|
+
Other things we learned, was that students hate answering lots of questions, only to find at the end they are not eligible. So the order of questions is very important, and thats why some of the early questions when applying for a student loan may seem a bit obscure, but they are designed to weed out those who are inelligble as soon as possible.
|
27
|
+
|
28
|
+
If you compare the Student loans site and the DVLA site, which are both govuk questionaires you will see that they have been implemented in very different ways, and as a result look fairly different, but essentially do the same thing. In fact I found that within the same company, you had multiple approaches, and completely different technology stacks to achieve the same thing. I spent months writing Daone of these sites,
|
29
|
+
|
30
|
+
Designing something from scratch is expensive and time consuming. This is where GDS have added real value. They have provided a set of ruby gems to trivialise the production of
|
31
|
+
govuk sites. One thing they are lacking however is a gem to trivialize the creation of complex questionaires. And this is why government organisations are seemingly reinventing
|
32
|
+
the wheel multiple times over. This is the niche which Doop fills.
|
33
|
+
|
34
|
+
If your govuk site requires any kind of questionaire, doop is the answer.
|
35
|
+
|
36
|
+
|
37
|
+
## Features
|
38
|
+
|
39
|
+
* DSL
|
40
|
+
* Rails generator to quickly get you started
|
41
|
+
* Ability to Change answer
|
42
|
+
* Summarize answers
|
43
|
+
* Broswer backbutton integration
|
44
|
+
* Stateless
|
45
|
+
* Fast
|
46
|
+
* Ajax call backs
|
47
|
+
* Built on rails 4
|
48
|
+
|
49
|
+
|
50
|
+
# Usage
|
51
|
+
|
52
|
+
## Generator
|
53
|
+
|
54
|
+
Make sure that the gemfile contains gem 'doop'. Then run
|
55
|
+
|
56
|
+
$ rails generate doopgovuk demo
|
57
|
+
|
58
|
+
Navigate to http://localhost:3000 and you will see the demo questionaire.
|
59
|
+
|
60
|
+
See the [demo rails controller](https://github.com/coder36/doop/blob/master/lib/generators/doopgovuk/templates/app/controllers/demo_controller.rb) to get a feel for the DSL.
|
61
|
+
|
62
|
+
|
63
|
+
## Yaml
|
64
|
+
|
65
|
+
Doop is initiated with a Yaml data structure:
|
66
|
+
|
67
|
+
page: {
|
68
|
+
preamble: {
|
69
|
+
_page: "preamble",
|
70
|
+
_nav_name: "Apply Online",
|
71
|
+
enrolled_before: {
|
72
|
+
_question: "Have you enrolled for this service before ?"
|
73
|
+
},
|
74
|
+
year_last_applied: {
|
75
|
+
_question: "What year did you last apply?"
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
Once initialized doop will add meta data to the structure. Each question will get meta data. So the above yaml will end up looking like:
|
81
|
+
|
82
|
+
page: {
|
83
|
+
_answered: false,
|
84
|
+
_answer: "",
|
85
|
+
_summary: "",
|
86
|
+
_enabled: true,
|
87
|
+
_open: false,
|
88
|
+
preamble: {
|
89
|
+
_answered: false,
|
90
|
+
_answer: "",
|
91
|
+
_summary: "",
|
92
|
+
_enabled: true,
|
93
|
+
_page: "preamble",
|
94
|
+
_nav_name: "Apply Online",
|
95
|
+
enrolled_before: {
|
96
|
+
_answered: false,
|
97
|
+
_answer: "",
|
98
|
+
_summary: "",
|
99
|
+
_enabled: true,
|
100
|
+
_question: "Have you enrolled for this service before ?"
|
101
|
+
},
|
102
|
+
year_last_applied: {
|
103
|
+
_answered: false,
|
104
|
+
_answer: "",
|
105
|
+
_summary: "",
|
106
|
+
_enabled: true,
|
107
|
+
_question: "What year did you last apply?"
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
Meta data always starts with an _. Doop uses the meta data to keep track of whats been answerd, what's currently being asked, and what questions are enabled.
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
## Question order
|
117
|
+
|
118
|
+
The questions will be asked in the order in which they appear in yaml. So for above, the order of questions will be:
|
119
|
+
|
120
|
+
1. page/preamble/enrolled_before
|
121
|
+
2. page/preamble/year_last_applied
|
122
|
+
3. page/preamble
|
123
|
+
4. page
|
124
|
+
|
125
|
+
The most nested question is asked first, then the next and so ending on the least nested question.
|
126
|
+
|
127
|
+
|
128
|
+
As the questions are answered, callbacks will invoked.
|
129
|
+
|
130
|
+
|
131
|
+
## Callbacks
|
132
|
+
|
133
|
+
Call backs are used to manipulate the yaml structure, to set _metadata etc. In the example below, when /page/preamble/enrolled_before is answered, the summary will be set to the answer.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
on_answer "/page/preamble/enrolled_before" do |question,path, params, answer|
|
137
|
+
answer_with( question, { "_summary" => answer } )
|
138
|
+
end
|
139
|
+
|
140
|
+
```
|
141
|
+
|
142
|
+
On_answer call backs can be used to change the question flow. The code below will enable or disable the year_last_applied question depending on whether the answer was Yes or No:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
on_answer "/page/preamble/enrolled_before" do |question,path, params, answer|
|
146
|
+
answer_with( question, { "_summary" => answer } )
|
147
|
+
enable( "/page/preamble/year_last_applied", answer == "Yes" )
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
|
152
|
+
# Notes
|
153
|
+
|
154
|
+
## Performance
|
155
|
+
|
156
|
+
For the demo, the serialized questionaire is stored as a form parameter. This is a nice approach as a general strategy since its completely stateless and as a result scalable.
|
157
|
+
Its also very fast.
|
158
|
+
|
159
|
+
I've worked with a lot of java farmeworks (I'm thinking JSF!) and in comparison I can assure you that the demo application is fast. Doop fully supports jruby, so you may get some
|
160
|
+
optimisation going down that route. As mentioned earlier its completely stateless, so you can scale by simply firing up more servers.
|
161
|
+
|
162
|
+
I tested it with passenger and nginx and it was lightening fast. Without any optimisation each request was taking about 20ms.
|
163
|
+
|
164
|
+
## Jruby
|
165
|
+
|
166
|
+
The yaml serialization uses encryption, so you will need to tell java to allow unlimted strength cryptography. Create a file config/initializers/unlimited_strength_cryptography
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
platform :jruby do
|
170
|
+
require 'java'
|
171
|
+
security_class = java.lang.Class.for_name('javax.crypto.JceSecurity')
|
172
|
+
restricted_field = security_class.get_declared_field('isRestricted')
|
173
|
+
restricted_field.accessible = true
|
174
|
+
restricted_field.set nil, false
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
Don't forget to deal with database drivers. In your Gemfile, you will need to use JDBC drivers:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
platform :jruby do
|
182
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
183
|
+
end
|
184
|
+
|
185
|
+
platform :ruby do
|
186
|
+
gem 'sqlite3'
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
|
191
|
+
## TODO
|
192
|
+
|
193
|
+
* Refactor - make the code, simpler and read better
|
194
|
+
* Doop-Rspec - it would be nice to have a DSL to drive answering questions. This could be extended to capybara.
|
195
|
+
|
196
|
+
|
197
|
+
## Contributing
|
198
|
+
|
199
|
+
1. Fork it ( https://github.com/coder36/doop/fork )
|
200
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
201
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
202
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
203
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DoopHelper
|
2
|
+
|
3
|
+
def info_box &block
|
4
|
+
render "doop/info_box", :content => block
|
5
|
+
end
|
6
|
+
|
7
|
+
def question_visible? path
|
8
|
+
doop = request[:doop]
|
9
|
+
doop.each_visible_question do |root,p|
|
10
|
+
return true if p == path
|
11
|
+
end
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def question_form doop, res, &block
|
16
|
+
request[:doop] = doop
|
17
|
+
s = render( "doop/question_form", :content => block, :res => res, :doop => doop )
|
18
|
+
s
|
19
|
+
end
|
20
|
+
|
21
|
+
def question path, opts = {:title => nil, :indent => false}, &block
|
22
|
+
doop = request[:doop]
|
23
|
+
root = doop[path]
|
24
|
+
s = ""
|
25
|
+
if question_visible? path
|
26
|
+
s = render( "doop/question", :content => block, :root => root, :path => path, :answer => root["_answer"], :title => opts[:title], :indent => opts[:indent] )
|
27
|
+
end
|
28
|
+
s
|
29
|
+
end
|
30
|
+
|
31
|
+
def when_answered path, &block
|
32
|
+
doop = request[:doop]
|
33
|
+
block.call if doop.all_nested_answered(path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def error res, id
|
37
|
+
render( "doop/error", :msg => res[id] ) if res[id] != nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def list path_regex, &block
|
41
|
+
doop = request[:doop]
|
42
|
+
l = {}
|
43
|
+
doop.each_question do |root,path|
|
44
|
+
m = path.match( "^#{path_regex}$")
|
45
|
+
l[m[1]] = path if m != nil
|
46
|
+
end
|
47
|
+
|
48
|
+
l.keys.sort.each { |k| block.call( l[k], k.to_i ) }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/bin/.gitignore
ADDED
File without changes
|
data/doop.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'doop/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "doop"
|
8
|
+
spec.version = Doop::VERSION
|
9
|
+
spec.authors = ["Mark Middleton"]
|
10
|
+
spec.email = ["markymiddleton@gmail.com"]
|
11
|
+
spec.summary = %q{Question framework for govuk websites.}
|
12
|
+
spec.description = %q{A question framework for govuk sites, inspired by the great work GDS have done to standardize the cross government internet presence.}
|
13
|
+
spec.homepage = "https://github.com/coder36/doop"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib", "app"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
|
25
|
+
spec.add_runtime_dependency "rails"
|
26
|
+
end
|
data/lib/doop.rb
ADDED
@@ -0,0 +1,313 @@
|
|
1
|
+
require "doop/version"
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require 'doop_controller'
|
5
|
+
|
6
|
+
module Doop
|
7
|
+
|
8
|
+
class Engine < ::Rails::Engine
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
class Doop
|
13
|
+
|
14
|
+
attr_accessor :yaml
|
15
|
+
|
16
|
+
def initialize yaml=nil
|
17
|
+
if yaml != nil
|
18
|
+
@yaml = yaml
|
19
|
+
init
|
20
|
+
end
|
21
|
+
setup_handlers
|
22
|
+
end
|
23
|
+
|
24
|
+
def init
|
25
|
+
@hash = YAML.load( yaml )
|
26
|
+
add_meta_data
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_yaml yaml
|
30
|
+
@yaml = yaml
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup_handlers
|
34
|
+
@on_answer_handlers = {}
|
35
|
+
@on_answer_handlers["default"] = method(:default_on_answer)
|
36
|
+
|
37
|
+
@on_all_nested_answer_handlers = {}
|
38
|
+
@on_all_nested_answer_handlers["default"] = method(:default_on_all_nested_answer)
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_on_all_nested_answer( root, path, context )
|
42
|
+
# do nothing
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_on_answer( root, path, context, answer )
|
46
|
+
self[path + "/_answer"] = context["answer"] if context["answer"] != nil
|
47
|
+
self[path + "/_summary"] = context["summary"] if context["summary"] != nil
|
48
|
+
self[path + "/_answered"] = true
|
49
|
+
self[path + "/_open"] = false
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
|
53
|
+
def dump
|
54
|
+
@hash.to_yaml
|
55
|
+
end
|
56
|
+
|
57
|
+
def [](path)
|
58
|
+
path_elements(path).inject(@hash) { |a,n| a[n] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def []=(path,val)
|
62
|
+
l = path_elements(path)
|
63
|
+
missing = []
|
64
|
+
|
65
|
+
# create missing nodes
|
66
|
+
while self[construct_path(l)] == nil
|
67
|
+
missing << l.pop
|
68
|
+
end
|
69
|
+
|
70
|
+
missing.reverse.each do |elem|
|
71
|
+
self[construct_path(l)][elem] = ""
|
72
|
+
l << elem
|
73
|
+
end
|
74
|
+
|
75
|
+
# set node to value
|
76
|
+
elem = l.pop
|
77
|
+
self[construct_path(l)][elem] = val
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def construct_path(elements)
|
82
|
+
elements.join("/")
|
83
|
+
end
|
84
|
+
|
85
|
+
def path_elements(path)
|
86
|
+
path.split("/").select {|n| !n.empty? }
|
87
|
+
end
|
88
|
+
|
89
|
+
def add(path, hash={})
|
90
|
+
self[path] = hash
|
91
|
+
add_meta_data
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove(path)
|
95
|
+
e = path_elements(path)
|
96
|
+
k = e.pop
|
97
|
+
self[construct_path(e)].delete(k)
|
98
|
+
end
|
99
|
+
|
100
|
+
def move( from, to )
|
101
|
+
q = self[from]
|
102
|
+
remove(from)
|
103
|
+
add(to)
|
104
|
+
self[to] = q
|
105
|
+
end
|
106
|
+
|
107
|
+
def renumber( path )
|
108
|
+
q = self[path]
|
109
|
+
i = 1
|
110
|
+
q.keys.select{|n| (n =~ /^(.*)__(\d)+/) != nil }. sort_by{|s| s.scan(/\d+/).map{|s| s.to_i}}.each do |elem|
|
111
|
+
name = elem.match( /(.*)__\d+/)[1]
|
112
|
+
move( "#{path}/#{elem}", "#{path}/#{name}__#{i}" )
|
113
|
+
i+=1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def each_path_elem(path)
|
118
|
+
p = ""
|
119
|
+
path.split("/").select{|r| !r.empty?}.each do |n|
|
120
|
+
p += "/" + n
|
121
|
+
yield( p )
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def each_path_elem_reverse(path)
|
126
|
+
a = []
|
127
|
+
each_path_elem(path) {|n| a << n }
|
128
|
+
a.reverse.each { |n| yield(n) }
|
129
|
+
end
|
130
|
+
|
131
|
+
def each_question(root=@hash, path="", &block)
|
132
|
+
root.keys.each do |key|
|
133
|
+
next if key.start_with?("_")
|
134
|
+
new_path = path + "/" + key
|
135
|
+
new_root = root[key]
|
136
|
+
block.call( new_root, new_path )
|
137
|
+
each_question( new_root, new_path, &block )
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def add_meta_data
|
142
|
+
each_question do |root, path|
|
143
|
+
root["_open"] = false if !root.has_key?("_open")
|
144
|
+
root["_enabled"] = true if !root.has_key?("_enabled")
|
145
|
+
root["_answered"] = false if !root.has_key?("_answered")
|
146
|
+
root["_answer"] = nil if !root.has_key?("_answer")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def ask_next
|
151
|
+
|
152
|
+
q = ""
|
153
|
+
disabled_path = nil
|
154
|
+
|
155
|
+
each_question do |root, path|
|
156
|
+
self[path]["_open"] = false
|
157
|
+
next if disabled_path != nil && path.start_with?(disabled_path)
|
158
|
+
if self[path+"/_enabled"] == false
|
159
|
+
disabled_path = path
|
160
|
+
next
|
161
|
+
end
|
162
|
+
|
163
|
+
q = path if path.start_with?(q) && root["_answered"] == false
|
164
|
+
end
|
165
|
+
|
166
|
+
each_path_elem( q ) { |n| self[n]["_open"] = true }
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
def currently_asked
|
171
|
+
# get the most nested open answer
|
172
|
+
p = ""
|
173
|
+
each_question do |root, path|
|
174
|
+
p = path if path.start_with?(p) && root["_open"] == true
|
175
|
+
end
|
176
|
+
p.empty? ? nil : p
|
177
|
+
end
|
178
|
+
|
179
|
+
def question
|
180
|
+
currently_asked
|
181
|
+
end
|
182
|
+
|
183
|
+
def answer context
|
184
|
+
path = currently_asked
|
185
|
+
root = self[path]
|
186
|
+
root["_answered"] = false
|
187
|
+
bind_params( root, context )
|
188
|
+
res = get_handler(@on_answer_handlers, path, "_on_answer_handler").call( root, path, context, root["_answer"] )
|
189
|
+
res = {} if ! res.is_a? Hash
|
190
|
+
|
191
|
+
ask_next
|
192
|
+
path = currently_asked
|
193
|
+
return if path == nil
|
194
|
+
each_path_elem_reverse(path) do |p|
|
195
|
+
if all_nested_answered( p ) == true
|
196
|
+
get_handler( @on_all_nested_answer_handlers, p, "_on_all_nested_answered" ).call( self[p], p, context )
|
197
|
+
ask_next
|
198
|
+
end
|
199
|
+
end
|
200
|
+
res
|
201
|
+
end
|
202
|
+
|
203
|
+
def bind_params root, context
|
204
|
+
a = {}
|
205
|
+
context.keys.each do |k|
|
206
|
+
if k == "b_answer"
|
207
|
+
root["_answer"] = context[k]
|
208
|
+
else
|
209
|
+
a[k[2..-1]] = context[k] if k.to_s.start_with?("b_")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
root["_answer"] = a if !a.empty?
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
def get_handler handlers, path, handler_elem
|
218
|
+
handler = self[path][handler_elem]
|
219
|
+
if handler != nil
|
220
|
+
block = handlers[handler]
|
221
|
+
else
|
222
|
+
keys = handlers.keys.select { |k| path.match( "^#{k}$" ) != nil }
|
223
|
+
block = keys.empty? ? handlers["default"] : handlers[keys[0]]
|
224
|
+
end
|
225
|
+
block
|
226
|
+
end
|
227
|
+
|
228
|
+
def all_nested_answered path
|
229
|
+
return true if path==nil
|
230
|
+
each_question( self[path], path ) do |root,path|
|
231
|
+
if root["_enabled"]
|
232
|
+
return false if root["_answered"] == false || root["_open"] == true
|
233
|
+
end
|
234
|
+
end
|
235
|
+
true
|
236
|
+
end
|
237
|
+
|
238
|
+
def disable path
|
239
|
+
self[path + "/_enabled"] = false
|
240
|
+
ask_next
|
241
|
+
end
|
242
|
+
|
243
|
+
def enable path, enable = true
|
244
|
+
self[path + "/_enabled"] = enable
|
245
|
+
ask_next
|
246
|
+
end
|
247
|
+
|
248
|
+
def change path
|
249
|
+
each_path_elem_reverse(currently_asked) do |p|
|
250
|
+
self[p + "/_open"] = false
|
251
|
+
end
|
252
|
+
|
253
|
+
# open all parent questions
|
254
|
+
each_path_elem_reverse(path) do |p|
|
255
|
+
self[p + "/_open"] = true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def on_answer(path, &block)
|
260
|
+
@on_answer_handlers[path] = block
|
261
|
+
end
|
262
|
+
|
263
|
+
def on_all_nested_answer(path, &block)
|
264
|
+
@on_all_nested_answer_handlers[path] = block
|
265
|
+
end
|
266
|
+
|
267
|
+
def answer_path path, a, summary = nil
|
268
|
+
self[path]["_answer"] = a
|
269
|
+
self[path]["_answered"] = true
|
270
|
+
self[path]["_summary"] = summary == nil ? a : summary
|
271
|
+
{}
|
272
|
+
end
|
273
|
+
|
274
|
+
def answer_with root, hash
|
275
|
+
root["_answered"] = true
|
276
|
+
hash.keys.each { |k| root[k] = hash[k] }
|
277
|
+
end
|
278
|
+
|
279
|
+
def unanswer_path path
|
280
|
+
self[path]["_answered"] = false
|
281
|
+
{}
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
def each_visible_question
|
286
|
+
open_paths = []
|
287
|
+
each_question do |root,path|
|
288
|
+
if root["_enabled"]
|
289
|
+
open_paths << path if root["_open"]
|
290
|
+
is_child = open_paths.select { |n| path.start_with?(n) && n.count('/')+1 == path.count('/') }.count > 0
|
291
|
+
yield(root, path) if is_child || root["_open"]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def all_answered path
|
297
|
+
each_question(self[path], path) do |root, p|
|
298
|
+
return false if root["_answered"] == false && root["_enabled"]
|
299
|
+
end
|
300
|
+
true
|
301
|
+
end
|
302
|
+
|
303
|
+
|
304
|
+
def each_question_with_regex_filter regex
|
305
|
+
each_question do |question,path|
|
306
|
+
yield(question, path) if path.match( regex ) != nil
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|