surveyor 1.1.0 → 1.2.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.
@@ -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