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 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