surveyor 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/app/controllers/surveyor_controller.rb +3 -4
- data/app/models/answer.rb +1 -2
- data/app/models/dependency.rb +7 -6
- data/app/models/dependency_condition.rb +11 -48
- data/app/models/response.rb +2 -14
- data/app/models/response_set.rb +1 -1
- data/app/models/validation.rb +13 -0
- data/app/models/validation_condition.rb +39 -0
- data/config/routes.rb +3 -3
- data/generators/surveyor/surveyor_generator.rb +1 -1
- data/generators/surveyor/templates/migrate/create_validation_conditions.rb +32 -0
- data/generators/surveyor/templates/migrate/create_validations.rb +20 -0
- data/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +19 -3
- data/lib/surveyor/acts_as_response.rb +33 -0
- data/lib/surveyor.rb +1 -0
- data/script/surveyor/question_group.rb +1 -0
- data/script/surveyor/survey.rb +3 -0
- data/spec/controllers/surveyor_controller_spec.rb +68 -203
- data/spec/factories.rb +58 -25
- data/spec/models/answer_spec.rb +1 -1
- data/spec/models/dependency_condition_spec.rb +47 -64
- data/spec/models/dependency_spec.rb +11 -5
- data/spec/models/question_group_spec.rb +25 -1
- data/spec/models/question_spec.rb +2 -2
- data/spec/models/response_set_spec.rb +10 -25
- data/spec/models/response_spec.rb +11 -46
- data/spec/models/survey_spec.rb +9 -10
- data/spec/models/validation_condition_spec.rb +53 -0
- data/spec/models/validation_spec.rb +32 -0
- data/surveyor.gemspec +11 -2
- metadata +11 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.0
|
@@ -42,19 +42,18 @@ class SurveyorController < ApplicationController
|
|
42
42
|
@sections = @survey.sections
|
43
43
|
@section = params[:section] ? @sections.with_includes.find(section_id_from(params[:section])) || @sections.with_includes.first : @sections.with_includes.first
|
44
44
|
@questions = @section.questions
|
45
|
+
@dependents = (@response_set.unanswered_dependencies - @section.questions) || []
|
45
46
|
else
|
46
47
|
flash[:notice] = "Unable to find your responses to the survey"
|
47
48
|
redirect_to(available_surveys_path)
|
48
49
|
end
|
49
|
-
|
50
|
-
@dependents = (@response_set.unanswered_dependencies - @section.questions) || []
|
51
50
|
end
|
52
51
|
def update
|
53
52
|
if @response_set = ResponseSet.find_by_access_code(params[:response_set_code], :include => {:responses => :answer})
|
54
53
|
@response_set.current_section_id = params[:current_section_id]
|
55
54
|
else
|
56
55
|
flash[:notice] = "Unable to find your responses to the survey"
|
57
|
-
redirect_to(available_surveys_path)
|
56
|
+
redirect_to(available_surveys_path) and return
|
58
57
|
end
|
59
58
|
|
60
59
|
if params[:responses] or params[:response_groups]
|
@@ -71,7 +70,7 @@ class SurveyorController < ApplicationController
|
|
71
70
|
flash[:notice] = "Completed survey"
|
72
71
|
redirect_to surveyor_default(:finish)
|
73
72
|
else
|
74
|
-
flash[:notice] = "Unable to update survey" if !saved and !saved.nil? # saved.nil? is true if there are no questions on the page (i.e. if it only contains a label)
|
73
|
+
flash[:notice] = "Unable to update survey" if !saved #and !saved.nil? # saved.nil? is true if there are no questions on the page (i.e. if it only contains a label)
|
75
74
|
redirect_to :action => "edit", :anchor => anchor_from(params[:section]), :params => {:section => section_id_from(params[:section])}
|
76
75
|
end
|
77
76
|
end
|
data/app/models/answer.rb
CHANGED
@@ -10,11 +10,10 @@ class Answer < ActiveRecord::Base
|
|
10
10
|
# Validations
|
11
11
|
validates_presence_of :text
|
12
12
|
validates_numericality_of :question_id, :allow_nil => false, :only_integer => true
|
13
|
-
#validates_uniqueness_of :reference_identifier
|
14
13
|
|
15
14
|
# Methods
|
16
15
|
def renderer(q = question)
|
17
|
-
r = [q.pick.to_s, self.response_class].compact.join("_")
|
16
|
+
r = [q.pick.to_s, self.response_class].compact.map(&:downcase).join("_")
|
18
17
|
r.blank? ? :default : r.to_sym
|
19
18
|
end
|
20
19
|
|
data/app/models/dependency.rb
CHANGED
@@ -10,19 +10,20 @@ class Dependency < ActiveRecord::Base
|
|
10
10
|
# Validations
|
11
11
|
validates_presence_of :rule
|
12
12
|
validates_format_of :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/ #TODO properly formed parenthesis etc.
|
13
|
-
|
14
|
-
|
13
|
+
validates_numericality_of :question_id, :if => Proc.new { |d| d.question_group_id.nil? }
|
14
|
+
validates_numericality_of :question_group_id, :if => Proc.new { |d| d.question_id.nil? }
|
15
|
+
|
15
16
|
# Attribute aliases
|
16
17
|
alias_attribute :dependent_question_id, :question_id
|
17
18
|
|
18
19
|
def question_group_id=(i)
|
19
|
-
write_attribute(:question_id, nil)
|
20
|
+
write_attribute(:question_id, nil) unless i.nil?
|
20
21
|
write_attribute(:question_group_id, i)
|
21
22
|
end
|
22
23
|
|
23
24
|
def question_id=(i)
|
24
|
-
write_attribute(:question_group_id, nil)
|
25
|
-
write_attribute(:question_id, i)
|
25
|
+
write_attribute(:question_group_id, nil) unless i.nil?
|
26
|
+
write_attribute(:question_id, i)
|
26
27
|
end
|
27
28
|
|
28
29
|
# Is the method that determines if this dependency has been met within
|
@@ -42,7 +43,7 @@ class Dependency < ActiveRecord::Base
|
|
42
43
|
keyed_pairs = {}
|
43
44
|
# logger.debug dependency_conditions.inspect
|
44
45
|
self.dependency_conditions.each do |dc|
|
45
|
-
keyed_pairs.merge!(dc.
|
46
|
+
keyed_pairs.merge!(dc.to_hash(response_set))
|
46
47
|
end
|
47
48
|
return(keyed_pairs)
|
48
49
|
end
|
@@ -13,67 +13,30 @@ class DependencyCondition < ActiveRecord::Base
|
|
13
13
|
validates_inclusion_of :operator, :in => OPERATORS
|
14
14
|
validates_uniqueness_of :rule_key, :scope => :dependency_id
|
15
15
|
|
16
|
+
acts_as_response # includes "as" instance method
|
17
|
+
|
16
18
|
# Class methods
|
17
19
|
def self.operators
|
18
20
|
OPERATORS
|
19
21
|
end
|
20
22
|
|
21
23
|
# Instance methods
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
x = {rule_key.to_sym => self.evaluation_of(response_set)}
|
26
|
-
# logger.warn "to_evaluation_hash #{x.inspect}"
|
27
|
-
return x
|
28
|
-
end
|
29
|
-
|
30
|
-
# # Evaluates the condition on the response_set
|
31
|
-
# def evaluation_of(response_set)
|
32
|
-
# response = response_set.responses.detect{|r| r.answer_id == answer_id} || false # turns out eval("nil and false") => nil so we need to return false if no response is found
|
33
|
-
# return(response and self.is_satisfied_by?(response))
|
34
|
-
# end
|
35
|
-
# Evaluates the condition on the response_set
|
36
|
-
def evaluation_of(response_set)
|
37
|
-
# response = response_set.find_response(self.answer_id) || false # turns out eval("nil and false") => nil so we need to return false if no response is found
|
38
|
-
response = response_set.responses.detect{|r| r.answer_id.to_i == self.answer_id.to_i} || false # turns out eval("nil and false") => nil so we need to return false if no response is found
|
39
|
-
# logger.warn "evaluation_of_response #{response.inspect}"
|
40
|
-
return(response and self.is_satisfied_by?(response))
|
24
|
+
def to_hash(response_set)
|
25
|
+
response = response_set.responses.detect{|r| r.answer_id.to_i == self.answer_id.to_i} || false # eval("nil and false") => nil so return false if no response is found
|
26
|
+
{rule_key.to_sym => (response and self.is_met?(response))}
|
41
27
|
end
|
42
28
|
|
43
|
-
# Checks to see if the response passed in
|
44
|
-
def
|
45
|
-
|
46
|
-
# response.as(response_class).send(operator.to_sym, self.as(response_class))
|
29
|
+
# Checks to see if the response passed in meets the dependency condition
|
30
|
+
def is_met?(response)
|
31
|
+
klass = response.answer.response_class
|
47
32
|
return case self.operator
|
48
|
-
when "=="
|
49
|
-
response.as(
|
33
|
+
when "==", "<", ">", "<=", ">="
|
34
|
+
response.as(klass).send(self.operator, self.as(klass))
|
50
35
|
when "!="
|
51
|
-
response.as(
|
52
|
-
when "<"
|
53
|
-
response.as(response_class) < self.as(response_class)
|
54
|
-
when ">"
|
55
|
-
response.as(response_class) > self.as(response_class)
|
56
|
-
when "<="
|
57
|
-
response.as(response_class) <= self.as(response_class)
|
58
|
-
when ">="
|
59
|
-
response.as(response_class) >= self.as(response_class)
|
36
|
+
!(response.as(klass) == self.as(klass))
|
60
37
|
else
|
61
38
|
false
|
62
39
|
end
|
63
40
|
end
|
64
41
|
|
65
|
-
# Method that returns the dependency as a particular response_class type
|
66
|
-
def as(type_symbol)
|
67
|
-
return case type_symbol.to_sym
|
68
|
-
when :string, :text, :integer, :float, :datetime
|
69
|
-
self.send("#{type_symbol}_value".to_sym)
|
70
|
-
when :date
|
71
|
-
self.datetime_value.nil? ? nil : self.datetime_value.to_date
|
72
|
-
when :time
|
73
|
-
self.datetime_value.nil? ? nil : self.datetime_value.to_time
|
74
|
-
else # :answer_id
|
75
|
-
self.answer_id
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
42
|
end
|
data/app/models/response.rb
CHANGED
@@ -12,6 +12,8 @@ class Response < ActiveRecord::Base
|
|
12
12
|
# Named scopes
|
13
13
|
named_scope :in_section, lambda {|section_id| {:include => :question, :conditions => ['questions.survey_section_id =?', section_id.to_i ]}}
|
14
14
|
|
15
|
+
acts_as_response # includes "as" instance method
|
16
|
+
|
15
17
|
def selected
|
16
18
|
!self.new_record?
|
17
19
|
end
|
@@ -21,20 +23,6 @@ class Response < ActiveRecord::Base
|
|
21
23
|
def selected=(value)
|
22
24
|
true
|
23
25
|
end
|
24
|
-
|
25
|
-
#Method that returns the response as a particular response_class type
|
26
|
-
def as(type_symbol)
|
27
|
-
return case type_symbol.to_sym
|
28
|
-
when :string, :text, :integer, :float, :datetime
|
29
|
-
self.send("#{type_symbol}_value".to_sym)
|
30
|
-
when :date
|
31
|
-
self.datetime_value.nil? ? nil : self.datetime_value.to_date
|
32
|
-
when :time
|
33
|
-
self.datetime_value.nil? ? nil : self.datetime_value.to_time
|
34
|
-
else # :answer_id
|
35
|
-
self.answer_id
|
36
|
-
end
|
37
|
-
end
|
38
26
|
|
39
27
|
def to_s
|
40
28
|
if self.answer.response_class == "answer" and self.answer_id
|
data/app/models/response_set.rb
CHANGED
@@ -105,7 +105,7 @@ class ResponseSet < ActiveRecord::Base
|
|
105
105
|
|
106
106
|
def all_dependencies
|
107
107
|
arr = dependencies.partition{|d| d.met?(self) }
|
108
|
-
{:show => arr[0].map{|d| d.question_group_id.nil? ? "question_#{d.question_id}" : "
|
108
|
+
{:show => arr[0].map{|d| d.question_group_id.nil? ? "question_#{d.question_id}" : "question_group_#{d.question_group_id}"}, :hide => arr[1].map{|d| d.question_group_id.nil? ? "question_#{d.question_id}" : "question_group_#{d.question_group_id}"}}
|
109
109
|
end
|
110
110
|
|
111
111
|
protected
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Validation < ActiveRecord::Base
|
2
|
+
# Associations
|
3
|
+
belongs_to :answer
|
4
|
+
|
5
|
+
# Scopes
|
6
|
+
|
7
|
+
# Validations
|
8
|
+
validates_presence_of :rule
|
9
|
+
validates_format_of :rule, :with => /^(?:and|or|\)|\(|[A-Z]|\s)+$/
|
10
|
+
validates_numericality_of :answer_id
|
11
|
+
|
12
|
+
# Instance Methods
|
13
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class ValidationCondition < ActiveRecord::Base
|
2
|
+
# Constants
|
3
|
+
OPERATORS = %w(== != < > <= >= =~)
|
4
|
+
|
5
|
+
# Associations
|
6
|
+
belongs_to :validation
|
7
|
+
|
8
|
+
# Scopes
|
9
|
+
|
10
|
+
# Validations
|
11
|
+
validates_numericality_of :validation_id #, :question_id, :answer_id
|
12
|
+
validates_presence_of :operator, :rule_key
|
13
|
+
validates_inclusion_of :operator, :in => OPERATORS
|
14
|
+
validates_uniqueness_of :rule_key, :scope => :validation_id
|
15
|
+
|
16
|
+
# Class methods
|
17
|
+
def self.operators
|
18
|
+
OPERATORS
|
19
|
+
end
|
20
|
+
|
21
|
+
# Instance Methods
|
22
|
+
# def to_hash(answer, response)
|
23
|
+
# {rule_key.to_sym => self.is_valid?(response)}
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def is_valid?(response)
|
27
|
+
# klass = response.answer.response_class
|
28
|
+
# case self.operator
|
29
|
+
# when "==", "<", ">", "<=", ">="
|
30
|
+
# response.as(klass).send(self.operator, self.as(klass))
|
31
|
+
# when "!="
|
32
|
+
# !(response.as(klass) == self.as(klass))
|
33
|
+
# when "=~"
|
34
|
+
# response.as(klass).to_s =~ self.regexp
|
35
|
+
# else
|
36
|
+
# false
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
end
|
data/config/routes.rb
CHANGED
@@ -2,9 +2,9 @@ ActionController::Routing::Routes.draw do |map|
|
|
2
2
|
root = Surveyor::Config['default.relative_url_root'] || "surveys/"
|
3
3
|
map.with_options :controller => 'surveyor' do |s|
|
4
4
|
s.available_surveys "#{root}", :conditions => {:method => :get}, :action => "new" # GET survey list
|
5
|
-
s.take_survey "#{root}:survey_code", :conditions => {:method => :post}, :action => "create" # Only POST of survey to
|
6
|
-
s.view_my_survey "#{root}:survey_code/:response_set_code", :conditions => {:method => :get}, :action => "show" # GET viewable/printable?
|
5
|
+
s.take_survey "#{root}:survey_code", :conditions => {:method => :post}, :action => "create" # Only POST of survey to create
|
6
|
+
s.view_my_survey "#{root}:survey_code/:response_set_code", :conditions => {:method => :get}, :action => "show" # GET viewable/printable? survey
|
7
7
|
s.edit_my_survey "#{root}:survey_code/:response_set_code/take", :conditions => {:method => :get}, :action => "edit" # GET editable survey
|
8
8
|
s.update_my_survey "#{root}:survey_code/:response_set_code", :conditions => {:method => :put}, :action => "update" # PUT edited survey
|
9
|
-
end
|
9
|
+
end
|
10
10
|
end
|
@@ -25,7 +25,7 @@ class SurveyorGenerator < Rails::Generator::Base
|
|
25
25
|
# not using m.migration_template because all migration timestamps end up the same, causing a collision when running rake db:migrate
|
26
26
|
# coped functionality from RAILS_GEM_PATH/lib/rails_generator/commands.rb
|
27
27
|
m.directory "db/migrate"
|
28
|
-
["surveys", "survey_sections", "questions", "answers", "response_sets", "responses", "dependencies", "
|
28
|
+
["surveys", "survey_sections", "questions", "question_groups", "answers", "response_sets", "responses", "dependencies", "dependency_conditions", "validations", "validation_conditions"].each_with_index do |model, i|
|
29
29
|
unless (prev_migrations = Dir.glob("db/migrate/[0-9]*_*.rb").grep(/[0-9]+_create_#{model}.rb$/)).empty?
|
30
30
|
prev_migration_timestamp = prev_migrations[0].match(/([0-9]+)_create_#{model}.rb$/)[1]
|
31
31
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class CreateValidationConditions < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :validation_conditions do |t|
|
4
|
+
# Context
|
5
|
+
t.integer :validation_id
|
6
|
+
t.string :rule_key
|
7
|
+
|
8
|
+
# Conditional
|
9
|
+
t.string :operator
|
10
|
+
|
11
|
+
# Optional external reference
|
12
|
+
t.integer :question_id
|
13
|
+
t.integer :answer_id
|
14
|
+
|
15
|
+
# Value
|
16
|
+
t.datetime :datetime_value
|
17
|
+
t.integer :integer_value
|
18
|
+
t.float :float_value
|
19
|
+
t.string :unit
|
20
|
+
t.text :text_value
|
21
|
+
t.string :string_value
|
22
|
+
t.string :response_other
|
23
|
+
t.string :regexp
|
24
|
+
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.down
|
30
|
+
drop_table :validation_conditions
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateValidations < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :validations do |t|
|
4
|
+
# Context
|
5
|
+
t.integer :answer_id # the answer to validate
|
6
|
+
|
7
|
+
# Conditional
|
8
|
+
t.string :rule
|
9
|
+
|
10
|
+
# Message
|
11
|
+
t.string :message
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.down
|
18
|
+
drop_table :validations
|
19
|
+
end
|
20
|
+
end
|
@@ -33,9 +33,25 @@ survey "“Kitchen Sink” survey" do
|
|
33
33
|
condition_D :q_2, "==", :a_4
|
34
34
|
|
35
35
|
# When :pick isn't specified, the default is :none (no checkbox or radio button)
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
q_montypython3 "What... is your name? (e.g. It is 'Arthur', King of the Britons)"
|
37
|
+
a_1 :string
|
38
|
+
|
39
|
+
# Dependency conditions can refer to any value, not just answer_id. An answer_reference still needs to be specified, to know which answer you would like to check
|
40
|
+
q_montypython4 "What... is your quest? (e.g. To seek the Holy Grail)"
|
41
|
+
a_1 :string
|
42
|
+
dependency :rule => "A"
|
43
|
+
condition_A :q_montypython3, "==", {:string_value => "It is 'Arthur', King of the Britons", :answer_reference => "1"}
|
44
|
+
|
45
|
+
# http://www.imdb.com/title/tt0071853/quotes
|
46
|
+
q_montypython5 "What... is the air-speed velocity of an unladen swallow? (e.g. What do you mean? An African or European swallow?)"
|
47
|
+
a_1 :string
|
48
|
+
dependency :rule => "A"
|
49
|
+
condition_A :q_montypython4, "==", {:string_value => "To seek the Holy Grail", :answer_reference => "1"}
|
50
|
+
|
51
|
+
label "Huh? I-- I don't know that! Auuuuuuuugh!"
|
52
|
+
dependency :rule => "A"
|
53
|
+
condition_A :q_montypython5, "==", {:string_value => "What do you mean? An African or European swallow?", :answer_reference => "1"}
|
54
|
+
|
39
55
|
# Surveys, sections, questions, groups, and answers all take the following reference arguments
|
40
56
|
# :reference_identifier # usually from paper
|
41
57
|
# :data_export_identifier # data export
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Surveyor
|
4
|
+
module Response
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def acts_as_response
|
11
|
+
include Surveyor::Response::InstanceMethods
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
# Returns the response as a particular response_class type
|
17
|
+
def as(type_symbol)
|
18
|
+
return case type_symbol.to_sym
|
19
|
+
when :string, :text, :integer, :float, :datetime
|
20
|
+
self.send("#{type_symbol}_value".to_sym)
|
21
|
+
when :date
|
22
|
+
self.datetime_value.nil? ? nil : self.datetime_value.to_date
|
23
|
+
when :time
|
24
|
+
self.datetime_value.nil? ? nil : self.datetime_value.to_time
|
25
|
+
else # :answer_id
|
26
|
+
self.answer_id
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
ActiveRecord::Base.send(:include, Surveyor::Response)
|
data/lib/surveyor.rb
CHANGED
data/script/surveyor/survey.rb
CHANGED
@@ -83,6 +83,9 @@ class Survey
|
|
83
83
|
section.questions.each do |question|
|
84
84
|
question.dependency.dependency_conditions.each { |con| con.reconcile_dependencies} unless question.dependency.nil?
|
85
85
|
end
|
86
|
+
section.question_groups.each do |group|
|
87
|
+
group.dependency.dependency_conditions.each { |con| con.reconcile_dependencies} unless group.dependency.nil?
|
88
|
+
end
|
86
89
|
end
|
87
90
|
end
|
88
91
|
|