doop 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +15 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +203 -0
  6. data/Rakefile +5 -0
  7. data/app/assets/images/dash.gif +0 -0
  8. data/app/assets/images/info-icon.png +0 -0
  9. data/app/assets/images/progress-tick-large.png +0 -0
  10. data/app/helpers/doop_helper.rb +51 -0
  11. data/bin/.gitignore +0 -0
  12. data/doop.gemspec +26 -0
  13. data/lib/doop.rb +313 -0
  14. data/lib/doop/version.rb +3 -0
  15. data/lib/doop_controller.rb +164 -0
  16. data/lib/generators/doopgovuk/USAGE +10 -0
  17. data/lib/generators/doopgovuk/doopgovuk_generator.rb +34 -0
  18. data/lib/generators/doopgovuk/templates/app/assets/stylesheets/demo.css.scss +188 -0
  19. data/lib/generators/doopgovuk/templates/app/controllers/demo_controller.rb +155 -0
  20. data/lib/generators/doopgovuk/templates/app/views/demo/_preamble.html.erb +33 -0
  21. data/lib/generators/doopgovuk/templates/app/views/demo/_summary.html.erb +10 -0
  22. data/lib/generators/doopgovuk/templates/app/views/demo/_your_details.html.erb +55 -0
  23. data/lib/generators/doopgovuk/templates/app/views/demo/index.html.erb +3 -0
  24. data/lib/generators/doopgovuk/templates/app/views/demo/index.js.erb +4 -0
  25. data/lib/generators/doopgovuk/templates/app/views/doop/_error.html.erb +3 -0
  26. data/lib/generators/doopgovuk/templates/app/views/doop/_info_box.html.erb +4 -0
  27. data/lib/generators/doopgovuk/templates/app/views/doop/_navbar.html.erb +23 -0
  28. data/lib/generators/doopgovuk/templates/app/views/doop/_question.html.erb +19 -0
  29. data/lib/generators/doopgovuk/templates/app/views/doop/_question_form.html.erb +15 -0
  30. data/lib/generators/doopgovuk/templates/app/views/layouts/application.html.erb +22 -0
  31. data/spec/doop_spec.rb +448 -0
  32. data/spec/spec_helper.rb +2 -0
  33. metadata +136 -0
@@ -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=
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in doop.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
@@ -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
File without changes
@@ -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
@@ -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