surveyor 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,30 @@
1
1
  History for Surveyor
2
2
  ====================
3
3
 
4
+ 1.2.0
5
+ -----
6
+
7
+ ### Features
8
+
9
+ - Allow rendering of simple hash contexts with Mustache (#296)
10
+ - Allow configuration of question numbering (#136)
11
+ - Allow references to question_ and answer_ in dependency conditions (#345)
12
+
13
+ ### Fixes
14
+
15
+ - Surveyor will never require 'fastercsv' on Ruby 1.9. (#381)
16
+ - Add question_groups/question/answer/reference_identifier to JSON
17
+ serialization for Survey. (#390)
18
+ - Evaluate dependencies even when the last response is removed (#362, #215)
19
+ - Add answer help text (#373)
20
+ - SurveyorController#export now renders 404 when surveys are not found (#391)
21
+
4
22
  1.1.0
5
23
  -----
6
24
 
7
25
  ### Features
8
26
 
9
- - Breaking change: Question#is_mandatory => false by default. For those who found it useful to have
27
+ - Breaking change: Question#is_mandatory => false by default. For those who found it useful to have
10
28
  all questions mandatory, the parser accepts `:default_mandatory => true` as an argument to the survey.
11
29
 
12
30
  ### Fixes
@@ -8,11 +8,11 @@ if ENV['RAILS_VERSION']
8
8
  when /3.1$/
9
9
  gem 'rails', '~> 3.1.0'
10
10
  # A JS runtime is required for Rails 3.1+
11
- gem 'therubyracer'
11
+ gem 'therubyracer', '~> 0.10.2'
12
12
  when /3.2$/
13
13
  gem 'rails', '~> 3.2.0'
14
14
  # A JS runtime is required for Rails 3.1+
15
- gem 'therubyracer'
15
+ gem 'therubyracer', '~> 0.10.2'
16
16
  else
17
17
  fail "Unknown Rails version #{ENV['RAILS_VERSION']}"
18
18
  end
@@ -4,6 +4,7 @@
4
4
  - i = response_idx(q.pick != "one") # argument will be false (don't increment i) if we're on radio buttons
5
5
  - disabled = defined?(disableFlag) ? disableFlag : false
6
6
  = f.semantic_fields_for i, r do |ff|
7
+ %span.help= render_help_text(a, @render_context) unless g && g.display_type == "grid"
7
8
  = ff.input :question_id, :as => :quiet unless q.pick == "one" # don't repeat question_id if we're on radio buttons
8
9
  = ff.input :api_id, :as => :quiet unless q.pick == "one"
9
10
  = ff.input :response_group, :value => rg, :as => :quiet if q.pick != "one" && g && g.display_type == "repeater"
@@ -15,7 +15,9 @@
15
15
  %tr
16
16
  %th  
17
17
  - ten_questions.first.answers.each do |a|
18
- %th= a_text(a)
18
+ %th
19
+ =a_text(a)
20
+ %span.help= render_help_text(a, @render_context)
19
21
  %th  
20
22
  - ten_questions.each_with_index do |q, i|
21
23
  %tr{:id => "q_#{q.id}", :class => "q_#{renderer} #{q.css_class(@response_set)}"}
@@ -51,6 +51,7 @@ child :sections => :sections do
51
51
  node(:post_text, :if => lambda { |q| !q.split_text(:post).blank? }){ |q| q.split_text(:post) }
52
52
  node(:help_text, :if => lambda { |q| !q.help_text.blank? }){ |q| q.help_text }
53
53
  node(:reference_identifier, :if => lambda { |q| !q.reference_identifier.blank? }){ |q| q.reference_identifier }
54
+ node(:data_export_identifier, :if => lambda { |q| !q.data_export_identifier.blank? }){ |q| q.data_export_identifier }
54
55
  node(:type, :if => lambda { |q| q.display_type != "default" }){ |q| q.display_type }
55
56
  node(:pick, :if => lambda { |q| q.pick != "none" }){ |q| q.pick }
56
57
 
@@ -61,6 +62,8 @@ child :sections => :sections do
61
62
  node(:text){ |a| a.split_or_hidden_text(:pre) }
62
63
  node(:post_text, :if => lambda { |a| !a.split_or_hidden_text(:post).blank? }){ |a| a.split_or_hidden_text(:post) }
63
64
  node(:type, :if => lambda { |a| a.response_class != "answer" }){ |a| a.response_class }
65
+ node(:reference_identifier, :if => lambda { |a| !a.reference_identifier.blank? }){ |a| a.reference_identifier }
66
+ node(:data_export_identifier, :if => lambda { |a| !a.data_export_identifier.blank? }){ |a| a.data_export_identifier }
64
67
  end
65
68
 
66
69
  child :dependency, :if => lambda { |q| q.dependency } do
@@ -75,4 +78,4 @@ child :sections => :sections do
75
78
  end
76
79
  end
77
80
 
78
- end
81
+ end
@@ -241,4 +241,8 @@ Feature: Survey export
241
241
  And I export the response set
242
242
  Then the JSON at "responses" should have 1 entry
243
243
  And the JSON response at "responses/0/value" should be "blueish"
244
- And the JSON response at "responses/0/answer_id" should correspond to an answer with text "most favorite color"
244
+ And the JSON response at "responses/0/answer_id" should correspond to an answer with text "most favorite color"
245
+
246
+ Scenario: Exporting non-existent surveys
247
+ When I visit "/surveys/simple-json.json"
248
+ Then I should get a "404" response
@@ -50,8 +50,11 @@ Then /^there should be (\d+) question(?:s?) with:$/ do |x, table|
50
50
  table.hashes.each do |hash|
51
51
  hash["reference_identifier"] = nil if hash["reference_identifier"] == "nil"
52
52
  hash["custom_class"] = nil if hash["custom_class"] == "nil"
53
- hash["is_mandatory"] = (hash["is_mandatory"] == "true" ? true : (hash["is_mandatory"] == "false" ? false : hash["is_mandatory"]))
53
+ if hash.has_key?("is_mandatory")
54
+ hash["is_mandatory"] = (hash["is_mandatory"] == "true" ? true : (hash["is_mandatory"] == "false" ? false : hash["is_mandatory"]))
55
+ end
54
56
  result = Question.find(:first, :conditions => hash)
57
+ puts hash if result.nil?
55
58
  result.should_not be_nil
56
59
  end
57
60
  end
@@ -225,6 +225,26 @@ Given /^I have survey context of "(.*)"$/ do |context|
225
225
  end
226
226
  end
227
227
 
228
+ Given /^I have a simple hash context$/ do
229
+ class SurveyorController < ApplicationController
230
+ def render_context
231
+ {:name => "Moses", :site => "Northwestern"}
232
+ end
233
+ end
234
+ end
235
+
236
+ Given /^I replace question numbers with letters$/ do
237
+ module SurveyorHelper
238
+ include Surveyor::Helpers::SurveyorHelperMethods
239
+ def next_question_number(question)
240
+ @letters ||= ("A".."Z").to_a
241
+ @n ||= 25
242
+ "<span class='qnum'>#{@letters[(@n += 1)%26]}. </span>"
243
+ end
244
+ end
245
+ end
246
+
247
+
228
248
  ## Various input elements
229
249
 
230
250
  Then /^I should see (\d+) textareas on the page$/ do |i|
@@ -219,3 +219,7 @@ end
219
219
  Then /^show me the page$/ do
220
220
  save_and_open_page
221
221
  end
222
+
223
+ Then /^I should get a "(\d+)" response$/ do |http_status|
224
+ page.status_code.should == http_status.to_i
225
+ end
@@ -334,7 +334,7 @@ Feature: Survey creation
334
334
 
335
335
 
336
336
  # Issue 259 - substitution of the text with Mustache
337
- @javascript
337
+ @javascript @mustache
338
338
  Scenario: Creating a question with an mustache syntax
339
339
  Given I have survey context of "FakeMustacheContext"
340
340
  Given I parse
@@ -364,6 +364,36 @@ Feature: Survey creation
364
364
  And I should see "Santa Claus lives on South Pole"
365
365
  And I should see "Santa Claus doesn't exist"
366
366
 
367
+ # Issue 296 - Mustache rendering doesn't work with simple hash contexts
368
+ @javascript @mustache
369
+ Scenario: Creating a question with an mustache syntax
370
+ Given I have a simple hash context
371
+ Given I parse
372
+ """
373
+ survey "Overall info" do
374
+ section "Group of questions" do
375
+ group "Information on {{name}}?", :help_text => "Answer all you know on {{name}}" do
376
+ label "{{name}} does not work for {{site}}!", :help_text => "Make sure you sure {{name}} doesn't work for {{site}}"
377
+
378
+ q "Where does {{name}} live?", :pick => :one,
379
+ :help_text => "If you don't know where {{name}} lives, skip the question"
380
+ a "{{name}} lives on North Pole"
381
+ a "{{name}} lives on South Pole"
382
+ a "{{name}} doesn't exist"
383
+ end
384
+ end
385
+ end
386
+ """
387
+ When I start the "Overall info" survey
388
+ Then I should see "Information on Moses"
389
+ And I should see "Answer all you know on Moses"
390
+ And I should see "Moses does not work for Northwestern!"
391
+ And I should see "Make sure you sure Moses doesn't work for Northwestern"
392
+ And I should see "Where does Moses live?"
393
+ And I should see "If you don't know where Moses lives, skip the question"
394
+ And I should see "Moses lives on North Pole"
395
+ And I should see "Moses lives on South Pole"
396
+ And I should see "Moses doesn't exist"
367
397
 
368
398
  Scenario: Creating and saving grids
369
399
  Given I parse
@@ -691,3 +721,59 @@ Feature: Survey creation
691
721
  And I should see "1) What is your favorite number?"
692
722
  And I should not see "What is your name?"
693
723
 
724
+ @numbers
725
+ Scenario: hidden numbers
726
+ Given I parse
727
+ """
728
+ survey "Alpha" do
729
+ section "A-C" do
730
+ q "Aligator"
731
+ q "Barber"
732
+ q "Camel"
733
+ end
734
+ end
735
+ """
736
+ And I replace question numbers with letters
737
+ When I start the "Alpha" survey
738
+ Then I should see "A. Aligator"
739
+ And I should see "B. Barber"
740
+ And I should see "C. Camel"
741
+
742
+ Scenario: help text
743
+ Given I parse
744
+ """
745
+ survey "Help!" do
746
+ section "Songs" do
747
+ q "Do you need anybody?", :pick => :one, :help_text => "select one of the following"
748
+ a "I need somebody to love", :help_text => "like The Beatles"
749
+ a "I am a rock, I am an island", :help_text => "like Simon and Garfunkel"
750
+
751
+ grid "How would these artists respond to 'Do you need anybody?'", :help_text => "in your opinion" do
752
+ a "Yes", :help_text => "would say yes"
753
+ a "No", :help_text => "would say no"
754
+ q "Bobby Darrin", :pick => :one
755
+ q "Kurt Cobain", :pick => :one
756
+ q "Ella Fitzgerald", :pick => :one
757
+ q "Kanye West", :pick => :one
758
+ end
759
+
760
+ repeater "Over and over" do
761
+ q "Row row row your boat", :pick => :any, :help_text => "the 1st part of a round"
762
+ a "gently down the stream", :help_text => "the 2nd part of a round"
763
+ a "merrily merrily merrily merrily", :help_text => "the 3rd part of a round"
764
+ a "life is but a dream", :help_text => "the 4th part of a round"
765
+ end
766
+ end
767
+ end
768
+ """
769
+ When I start the "Help!" survey
770
+ Then I should see "select one of the following"
771
+ And I should see "like The Beatles"
772
+ And I should see "like Simon and Garfunkel"
773
+ And I should see "in your opinion"
774
+ And I should see "would say yes"
775
+ And I should see "would say no"
776
+ And I should see "the 1st part of a round"
777
+ And I should see "the 2nd part of a round"
778
+ And I should see "the 3rd part of a round"
779
+ And I should see "the 4th part of a round"
@@ -361,3 +361,62 @@ Feature: Survey dependencies
361
361
  When I check "Once for you"
362
362
  Then the element "#q_3" should not be hidden
363
363
  And the element "#q_2" should be hidden
364
+
365
+ @javascript
366
+ Scenario: Dependency evaluation when the last response is removed
367
+ Given I parse
368
+ """
369
+ survey "Heating" do
370
+ section "Basics" do
371
+ q_heating_1 "How do you heat your home?", :pick => :any
372
+ a_1 "Forced air"
373
+ a_2 "Radiators"
374
+ a_3 "Oven"
375
+ a_4 "Passive"
376
+
377
+ q_heating_2 "How much does it cost to run your non-passive heating solutions?"
378
+ dependency :rule => "A"
379
+ condition_A :q_heating_1, "==", :a_1
380
+ a_1 "$", :float
381
+ end
382
+ end
383
+ """
384
+ When I go to the surveys page
385
+ And I start the "Heating" survey
386
+ Then the question "How much does it cost to run your non-passive heating solutions?" should be hidden
387
+ And I check "Forced air"
388
+ Then the question "How much does it cost to run your non-passive heating solutions?" should be triggered
389
+ And I uncheck "Forced air"
390
+ Then the question "How much does it cost to run your non-passive heating solutions?" should be hidden
391
+
392
+ @javascript @focus
393
+ Scenario: Dependency evaluation within groups
394
+ Given I parse
395
+ """
396
+ survey "Body" do
397
+ section "Joints" do
398
+ group "Muscle" do
399
+ q_muscles_joints_bones "Muscles, Joints, Bones", :pick => :any, :data_export_identifier => "muscles_joints_bones"
400
+ a_1 "Weakness"
401
+ a_2 "Arthritis"
402
+ a_3 "Cane/Walker"
403
+ a_4 "Morning stiffness"
404
+ a_5 "Joint pain"
405
+ a_6 "Muscle tenderness"
406
+ a_7 :other
407
+
408
+ q_muscles_joints_bones_other "Explain", :data_export_identifier => "muscles_joints_bones_other"
409
+ dependency :rule => "A"
410
+ condition_A :q_muscles_joints_bones, "==", :a_7
411
+ a "Explain", :string
412
+ end
413
+ end
414
+ end
415
+ """
416
+ When I go to the surveys page
417
+ And I start the "Body" survey
418
+ Then the question "Explain" should be hidden
419
+ When I check "Other"
420
+ Then the question "Explain" should be triggered
421
+ When I uncheck "Other"
422
+ Then the question "Explain" should be hidden
@@ -473,3 +473,32 @@ Feature: Survey parser
473
473
  | Did you take out the trash | true |
474
474
  | Did you do the laundry | true |
475
475
  | Optional comments | false |
476
+
477
+ @javascript
478
+ Scenario: Parsing dependencies with "question_" and "answer_" syntax
479
+ Given I parse
480
+ """
481
+ survey "Days" do
482
+ section "Fridays" do
483
+ q_is_it_friday "Is it Friday?", :pick => :one
484
+ a_yes "Yes"
485
+ a_no "No"
486
+
487
+ label "woot!"
488
+ dependency :rule => "A"
489
+ condition_A :question_is_it_friday, "==", :answer_yes
490
+ end
491
+ end
492
+ """
493
+ Then there should be 1 dependency with:
494
+ | rule |
495
+ | A |
496
+ And there should be 1 resolved dependency_condition with:
497
+ | rule_key |
498
+ | A |
499
+ When I go to the surveys page
500
+ And I start the "Days" survey
501
+ Then the question "woot!" should be hidden
502
+ And I choose "Yes"
503
+ Then the question "woot!" should be triggered
504
+
@@ -37,6 +37,23 @@ module Surveyor
37
37
  def generate_api_id
38
38
  UUIDTools::UUID.random_create.to_s
39
39
  end
40
+
41
+ ##
42
+ # @private Intended for internal use only.
43
+ #
44
+ # Loads and uses either `FasterCSV` (for Ruby 1.8) or the stdlib `CSV`
45
+ # (for Ruby 1.9+).
46
+ #
47
+ # @return [Class] either `CSV` for `FasterCSV`.
48
+ def csv_impl
49
+ @csv_impl ||= if RUBY_VERSION < '1.9'
50
+ require 'fastercsv'
51
+ FasterCSV
52
+ else
53
+ require 'csv'
54
+ CSV
55
+ end
56
+ end
40
57
  end
41
58
  end
42
59
  end
@@ -41,11 +41,17 @@ module Surveyor
41
41
 
42
42
  # Questions
43
43
  def q_text(obj, context=nil)
44
- @n ||= 0
44
+
45
45
  return image_tag(obj.text) if obj.is_a?(Question) and obj.display_type == "image"
46
46
  return obj.render_question_text(context) if obj.is_a?(Question) and (obj.dependent? or obj.display_type == "label" or obj.part_of_group?)
47
- "#{@n += 1}) #{obj.render_question_text(context)}"
47
+ "#{next_question_number(obj)}#{obj.render_question_text(context)}"
48
+ end
49
+
50
+ def next_question_number(question)
51
+ @n ||= 0
52
+ "<span class='qnum'>#{@n += 1}) </span>"
48
53
  end
54
+
49
55
  # def split_text(text = "") # Split text into with "|" delimiter - parts to go before/after input element
50
56
  # {:prefix => text.split("|")[0].blank? ? "&nbsp;" : text.split("|")[0], :postfix => text.split("|")[1] || "&nbsp;"}
51
57
  # end
@@ -1,5 +1,3 @@
1
- require 'fastercsv'
2
- require 'csv'
3
1
  require 'rabl'
4
2
 
5
3
  module Surveyor
@@ -54,8 +52,7 @@ module Surveyor
54
52
  qcols = Question.content_columns.map(&:name) - %w(created_at updated_at)
55
53
  acols = Answer.content_columns.map(&:name) - %w(created_at updated_at)
56
54
  rcols = Response.content_columns.map(&:name)
57
- csvlib = CSV.const_defined?(:Reader) ? FasterCSV : CSV
58
- result = csvlib.generate do |csv|
55
+ result = Surveyor::Common.csv_impl.generate do |csv|
59
56
  if print_header
60
57
  csv << (access_code ? ["response set access code"] : []) +
61
58
  qcols.map{|qcol| "question.#{qcol}"} +
@@ -184,6 +181,7 @@ module Surveyor
184
181
  protected
185
182
 
186
183
  def dependencies(question_ids = nil)
184
+ question_ids = survey.sections.map(&:questions).flatten.map(&:id) if responses.blank? and question_ids.blank?
187
185
  deps = Dependency.all(:include => :dependency_conditions,
188
186
  :conditions => {:dependency_conditions => {:question_id => question_ids || responses.map(&:question_id)}})
189
187
  # this is a work around for a bug in active_record in rails 2.3 which incorrectly eager-loads associatins when a
@@ -25,6 +25,10 @@ module Surveyor
25
25
  # Whitelisting attributes
26
26
  base.send :attr_accessible, :title, :description, :reference_identifier, :data_export_identifier, :common_namespace, :common_identifier, :css_url, :custom_class, :display_order
27
27
 
28
+ # Derived attributes
29
+ base.send :before_save, :generate_access_code
30
+ base.send :before_save, :increment_version
31
+
28
32
  # Class methods
29
33
  base.instance_eval do
30
34
  def to_normalized_string(value)
@@ -45,16 +49,6 @@ module Surveyor
45
49
  self.display_order ||= Survey.count
46
50
  end
47
51
 
48
- def title=(value)
49
- return if value == self.title
50
- surveys = Survey.where(:access_code => Survey.to_normalized_string(value)).order("survey_version DESC")
51
- self.survey_version = surveys.first.survey_version.to_i + 1 if surveys.any?
52
- self.access_code = Survey.to_normalized_string(value)
53
- super(value)
54
- # self.access_code = Survey.to_normalized_string(value)
55
- # super
56
- end
57
-
58
52
  def active?
59
53
  self.active_as_of?(DateTime.now)
60
54
  end
@@ -72,7 +66,22 @@ module Surveyor
72
66
  def as_json(options = nil)
73
67
  template_paths = ActionController::Base.view_paths.collect(&:to_path)
74
68
  Rabl.render(self, 'surveyor/export.json', :view_path => template_paths, :format => "hash")
75
- end
69
+ end
70
+
71
+ def default_access_code
72
+ self.class.to_normalized_string(title)
73
+ end
74
+
75
+ def generate_access_code
76
+ self.access_code ||= default_access_code
77
+ end
78
+
79
+ def increment_version
80
+ surveys = self.class.select(:survey_version).where(:access_code => access_code).order("survey_version DESC")
81
+ next_version = surveys.any? ? surveys.first.survey_version.to_i + 1 : 0
82
+
83
+ self.survey_version = next_version
84
+ end
76
85
  end
77
86
  end
78
87
  end