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 CHANGED
@@ -1 +1 @@
1
- 0.7.1
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
 
@@ -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
- # validates_numericality_of :question_id
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.to_evaluation_hash(response_set))
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
- # The hash used in the dependency parent object to evaluate its rule string
24
- def to_evaluation_hash(response_set)
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 satisfies the dependency condition
44
- def is_satisfied_by?(response)
45
- response_class = response.answer.response_class
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(response_class) == self.as(response_class)
33
+ when "==", "<", ">", "<=", ">="
34
+ response.as(klass).send(self.operator, self.as(klass))
50
35
  when "!="
51
- response.as(response_class) != self.as(response_class)
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
@@ -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
@@ -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}" : "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}"}}
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 cre
6
- s.view_my_survey "#{root}:survey_code/:response_set_code", :conditions => {:method => :get}, :action => "show" # GET viewable/printable? su
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", "question_groups", "dependency_conditions"].each_with_index do |model, i|
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 "&#8220;Kitchen Sink&#8221; 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
- q "What is your name?"
37
- a :string
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
@@ -2,6 +2,7 @@ dir = File.dirname(__FILE__)
2
2
  $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
3
 
4
4
  require 'surveyor/config'
5
+ require 'surveyor/acts_as_response'
5
6
 
6
7
  module Surveyor
7
8
  end
@@ -30,6 +30,7 @@ class QuestionGroup
30
30
 
31
31
  def to_file
32
32
  File.open(self.parser.question_groups_yml, File::CREAT|File::APPEND|File::WRONLY){ |f| f << to_yml }
33
+ if self.dependency then self.dependency.to_file end
33
34
  end
34
35
 
35
36
  end
@@ -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