surveyor 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|