surveyor 0.22.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. data/.gitignore +1 -0
  2. data/.rspec +1 -0
  3. data/CHANGELOG.md +83 -0
  4. data/Gemfile.rails_version +7 -1
  5. data/README.md +114 -16
  6. data/Rakefile +15 -9
  7. data/app/inputs/quiet_input.rb +5 -0
  8. data/app/inputs/surveyor_check_boxes_input.rb +35 -0
  9. data/app/inputs/surveyor_radio_input.rb +18 -0
  10. data/app/views/partials/_answer.html.haml +4 -4
  11. data/app/views/partials/_question.html.haml +7 -7
  12. data/app/views/partials/_question_group.html.haml +9 -8
  13. data/app/views/surveyor/export.json.rabl +28 -25
  14. data/app/views/surveyor/new.html.haml +8 -5
  15. data/app/views/surveyor/show.json.rabl +3 -2
  16. data/ci-exec.sh +13 -7
  17. data/cucumber.yml +3 -3
  18. data/doc/REPRESENTATIONS.md +34 -0
  19. data/doc/api_id_schema.json +7 -0
  20. data/doc/response_set_schema.json +54 -0
  21. data/doc/surveyor question combinations.png +0 -0
  22. data/doc/surveyor_timestamp_schema.json +9 -0
  23. data/features/ajax_submissions.feature +140 -0
  24. data/features/export_to_json.feature +182 -34
  25. data/features/no_duplicates.feature +110 -0
  26. data/features/show_survey.feature +1 -1
  27. data/features/step_definitions/parser_steps.rb +25 -2
  28. data/features/step_definitions/surveyor_steps.rb +145 -20
  29. data/features/step_definitions/ui_steps.rb +3 -0
  30. data/features/support/database_cleaner.rb +16 -0
  31. data/features/support/env.rb +21 -17
  32. data/features/support/simultaneous_ajax.rb +101 -0
  33. data/features/support/single_quit_selenium_driver.rb +23 -0
  34. data/features/support/slow_updates.rb +18 -0
  35. data/features/surveyor.feature +174 -44
  36. data/features/surveyor_dependencies.feature +80 -39
  37. data/features/surveyor_parser.feature +114 -20
  38. data/features/z_redcap_parser.feature +0 -1
  39. data/lib/{generators/surveyor/templates/public → assets}/images/surveyor/next.gif +0 -0
  40. data/lib/{generators/surveyor/templates/public → assets}/images/surveyor/prev.gif +0 -0
  41. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  42. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  43. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_glass_65_ffffff_1x400.png +0 -0
  44. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  45. data/lib/assets/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  46. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  47. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-icons_ef8c08_256x240.png +0 -0
  48. data/lib/{generators/surveyor/templates/public/stylesheets/surveyor → assets/images}/ui-icons_ffffff_256x240.png +0 -0
  49. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery-ui-timepicker-addon.js +23 -23
  50. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery-ui.js +125 -125
  51. data/lib/assets/javascripts/surveyor/jquery.selectToUISlider.js +240 -0
  52. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery.surveyor.js +52 -57
  53. data/lib/{generators/surveyor/templates/public → assets}/javascripts/surveyor/jquery.tools.min.js +7 -7
  54. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/dateinput.css +13 -13
  55. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/jquery-ui-timepicker-addon.css +0 -0
  56. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/jquery-ui.custom.css +17 -17
  57. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/reset.css +1 -1
  58. data/lib/{generators/surveyor/templates/public → assets}/stylesheets/surveyor/results.css +0 -0
  59. data/lib/assets/stylesheets/surveyor/ui.slider.extras.css +110 -0
  60. data/lib/{generators/surveyor/templates/public/stylesheets/sass → assets/stylesheets}/surveyor.sass +15 -7
  61. data/lib/generators/surveyor/custom_generator.rb +3 -2
  62. data/lib/generators/surveyor/install_generator.rb +59 -17
  63. data/lib/generators/surveyor/templates/app/assets/javascripts/surveyor_all.js +5 -0
  64. data/lib/generators/surveyor/templates/app/assets/stylesheets/surveyor_all.css +9 -0
  65. data/lib/generators/surveyor/templates/app/controllers/surveyor_controller.rb +2 -1
  66. data/lib/generators/surveyor/templates/app/views/layouts/surveyor_custom.html.erb +1 -0
  67. data/lib/generators/surveyor/templates/config/locales/surveyor_es.yml +1 -0
  68. data/lib/generators/surveyor/templates/config/locales/surveyor_he.yml +1 -0
  69. data/lib/generators/surveyor/templates/db/migrate/add_api_id_to_question_groups.rb +1 -0
  70. data/lib/generators/surveyor/templates/db/migrate/add_api_ids.rb +1 -0
  71. data/lib/generators/surveyor/templates/db/migrate/add_api_ids_to_response_sets_and_responses.rb +1 -0
  72. data/lib/generators/surveyor/templates/db/migrate/add_correct_answer_id_to_questions.rb +1 -0
  73. data/lib/generators/surveyor/templates/db/migrate/add_default_value_to_answers.rb +1 -0
  74. data/lib/generators/surveyor/templates/db/migrate/add_display_order_to_surveys.rb +1 -0
  75. data/lib/generators/surveyor/templates/db/migrate/add_display_type_to_answers.rb +1 -0
  76. data/lib/generators/surveyor/templates/db/migrate/add_index_to_response_sets.rb +1 -0
  77. data/lib/generators/surveyor/templates/db/migrate/add_index_to_surveys.rb +1 -0
  78. data/lib/generators/surveyor/templates/db/migrate/add_section_id_to_responses.rb +1 -0
  79. data/lib/generators/surveyor/templates/db/migrate/add_unique_index_on_access_code_and_version_in_surveys.rb +10 -0
  80. data/lib/generators/surveyor/templates/db/migrate/add_unique_indicies.rb +3 -2
  81. data/lib/generators/surveyor/templates/db/migrate/add_version_to_surveys.rb +10 -0
  82. data/lib/generators/surveyor/templates/db/migrate/api_ids_must_be_unique.rb +23 -0
  83. data/lib/generators/surveyor/templates/db/migrate/create_answers.rb +6 -5
  84. data/lib/generators/surveyor/templates/db/migrate/create_dependencies.rb +2 -1
  85. data/lib/generators/surveyor/templates/db/migrate/create_dependency_conditions.rb +2 -1
  86. data/lib/generators/surveyor/templates/db/migrate/create_question_groups.rb +5 -4
  87. data/lib/generators/surveyor/templates/db/migrate/create_questions.rb +4 -3
  88. data/lib/generators/surveyor/templates/db/migrate/create_response_sets.rb +1 -0
  89. data/lib/generators/surveyor/templates/db/migrate/create_responses.rb +10 -9
  90. data/lib/generators/surveyor/templates/db/migrate/create_survey_sections.rb +5 -4
  91. data/lib/generators/surveyor/templates/db/migrate/create_surveys.rb +4 -3
  92. data/lib/generators/surveyor/templates/db/migrate/create_validation_conditions.rb +5 -4
  93. data/lib/generators/surveyor/templates/db/migrate/create_validations.rb +3 -2
  94. data/lib/generators/surveyor/templates/db/migrate/drop_unique_index_on_access_code_in_surveys.rb +10 -0
  95. data/lib/generators/surveyor/templates/db/migrate/update_blank_api_ids_on_question_group.rb +22 -0
  96. data/lib/generators/surveyor/templates/db/migrate/update_blank_versions_on_surveys.rb +13 -0
  97. data/lib/generators/surveyor/templates/surveys/date_survey.rb +1 -0
  98. data/lib/generators/surveyor/templates/surveys/kitchen_sink_survey.rb +54 -24
  99. data/lib/generators/surveyor/templates/surveys/quiz.rb +1 -0
  100. data/lib/generators/surveyor/templates/{public/stylesheets/sass → vendor/assets/stylesheets}/custom.sass +1 -1
  101. data/lib/surveyor/common.rb +16 -31
  102. data/lib/surveyor/engine.rb +2 -4
  103. data/lib/surveyor/helpers/asset_pipeline.rb +13 -0
  104. data/lib/surveyor/helpers/formtastic_custom_input.rb +17 -0
  105. data/lib/surveyor/helpers/surveyor_helper_methods.rb +10 -8
  106. data/lib/surveyor/models/answer_methods.rb +3 -0
  107. data/lib/surveyor/models/dependency_condition_methods.rb +27 -28
  108. data/lib/surveyor/models/dependency_methods.rb +3 -0
  109. data/lib/surveyor/models/question_group_methods.rb +3 -0
  110. data/lib/surveyor/models/question_methods.rb +10 -7
  111. data/lib/surveyor/models/response_methods.rb +16 -0
  112. data/lib/surveyor/models/response_set_methods.rb +71 -64
  113. data/lib/surveyor/models/survey_methods.rb +19 -28
  114. data/lib/surveyor/models/survey_section_methods.rb +3 -0
  115. data/lib/surveyor/models/validation_condition_methods.rb +4 -2
  116. data/lib/surveyor/models/validation_methods.rb +3 -0
  117. data/lib/surveyor/parser.rb +198 -148
  118. data/lib/surveyor/redcap_parser.rb +120 -80
  119. data/lib/surveyor/surveyor_controller_methods.rb +86 -37
  120. data/lib/surveyor/version.rb +2 -2
  121. data/lib/surveyor.rb +5 -6
  122. data/lib/tasks/surveyor_tasks.rake +19 -7
  123. data/spec/controllers/surveyor_controller_spec.rb +166 -92
  124. data/spec/factories.rb +33 -32
  125. data/spec/helpers/formtastic_custom_input_spec.rb +16 -0
  126. data/spec/lib/common_spec.rb +0 -39
  127. data/spec/lib/redcap_parser_spec.rb +24 -24
  128. data/spec/models/answer_spec.rb +12 -0
  129. data/spec/models/dependency_condition_spec.rb +279 -323
  130. data/spec/models/dependency_spec.rb +10 -0
  131. data/spec/models/question_group_spec.rb +12 -0
  132. data/spec/models/question_spec.rb +12 -0
  133. data/spec/models/response_set_spec.rb +189 -139
  134. data/spec/models/response_spec.rb +60 -0
  135. data/spec/models/survey_section_spec.rb +9 -0
  136. data/spec/models/survey_spec.rb +72 -9
  137. data/spec/models/validation_condition_spec.rb +9 -1
  138. data/spec/models/validation_spec.rb +10 -0
  139. data/spec/spec_helper.rb +25 -6
  140. data/surveyor.gemspec +5 -4
  141. metadata +332 -291
  142. data/features/step_definitions/common_steps.rb +0 -3
  143. data/lib/formtastic/surveyor_builder.rb +0 -82
  144. data/lib/generators/surveyor/templates/public/javascripts/surveyor/jquery.blockUI.js +0 -499
@@ -43,6 +43,16 @@ describe Dependency do
43
43
  @dependency.rule = "a and b"
44
44
  @dependency.should have(1).error_on(:rule)
45
45
  end
46
+ it "should protect timestamps" do
47
+ saved_attrs = @dependency.attributes
48
+ if defined? ActiveModel::MassAssignmentSecurity::Error
49
+ lambda {@dependency.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
50
+ else
51
+ @dependency.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
52
+ end
53
+ @dependency.attributes.should == saved_attrs
54
+ end
55
+
46
56
 
47
57
  end
48
58
 
@@ -32,4 +32,16 @@ describe QuestionGroup do
32
32
  @dependency.should_receive(:is_met?).and_return(false)
33
33
  @question_group.css_class(Factory(:response_set)).should == "g_dependent g_hidden foo bar"
34
34
  end
35
+ it "should protect api_id, timestamps" do
36
+ saved_attrs = @question_group.attributes
37
+ if defined? ActiveModel::MassAssignmentSecurity::Error
38
+ lambda {@question_group.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
39
+ lambda {@question_group.update_attributes(:api_id => "NEW")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
40
+ else
41
+ @question_group.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
42
+ @question_group.attributes = {:api_id => "NEW"} # Rails doesn't return false, but this will be checked in the comparison to saved_attrs
43
+ end
44
+ @question_group.attributes.should == saved_attrs
45
+ end
46
+
35
47
  end
@@ -42,6 +42,18 @@ describe Question, "when creating a new question" do
42
42
  it "should have an api_id" do
43
43
  @question.api_id.length.should == 36
44
44
  end
45
+
46
+ it "should protect api_id, timestamps" do
47
+ saved_attrs = @question.attributes
48
+ if defined? ActiveModel::MassAssignmentSecurity::Error
49
+ lambda {@question.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
50
+ lambda {@question.update_attributes(:api_id => "NEW")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
51
+ else
52
+ @question.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
53
+ @question.attributes = {:api_id => "NEW"} # Rails doesn't return false, but this will be checked in the comparison to saved_attrs
54
+ end
55
+ @question.attributes.should == saved_attrs
56
+ end
45
57
  end
46
58
 
47
59
  describe Question, "that has answers" do
@@ -1,6 +1,8 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  describe ResponseSet do
4
+ let(:response_set) { Factory(:response_set) }
5
+
4
6
  before(:each) do
5
7
  @response_set = Factory(:response_set)
6
8
  @radio_response_attributes = HashWithIndifferentAccess.new({"1"=>{"question_id"=>"1", "answer_id"=>"1", "string_value"=>"XXL"}, "2"=>{"question_id"=>"2", "answer_id"=>"6"}, "3"=>{"question_id"=>"3"}})
@@ -13,13 +15,33 @@ describe ResponseSet do
13
15
  @response_set.access_code.length.should == 10
14
16
  end
15
17
 
18
+ it "should protect api_id, timestamps, access_code, started_at, completed_at" do
19
+ saved_attrs = @response_set.attributes
20
+ if defined? ActiveModel::MassAssignmentSecurity::Error
21
+ lambda {@response_set.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
22
+ lambda {@response_set.update_attributes(:api_id => "NEW")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
23
+ lambda {@response_set.update_attributes(:access_code => "AND")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
24
+ lambda {@response_set.update_attributes(:started_at => 10.days.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
25
+ lambda {@response_set.update_attributes(:completed_at => 2.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
26
+ else
27
+ @response_set.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
28
+ @response_set.attributes = {:api_id => "NEW"} # Rails doesn't return false, but this will be checked in the comparison to saved_attrs
29
+ @response_set.attributes = {:access_code => "AND"}
30
+ @response_set.attributes = {:started_at => 10.days.ago}
31
+ @response_set.attributes = {:completed_at => 2.hours.ago}
32
+ end
33
+ @response_set.attributes.should == saved_attrs
34
+ end
35
+
16
36
  describe '#access_code' do
17
37
  let!(:rs1) { Factory(:response_set).tap { |rs| rs.update_attribute(:access_code, 'one') } }
18
38
  let!(:rs2) { Factory(:response_set).tap { |rs| rs.update_attribute(:access_code, 'two') } }
19
39
 
20
40
  # Regression test for #263
21
41
  it 'accepts an access code in the constructor' do
22
- ResponseSet.new(:access_code => 'eleven').access_code.should == 'eleven'
42
+ rs = ResponseSet.new
43
+ rs.access_code = 'eleven'
44
+ rs.access_code.should == 'eleven'
23
45
  end
24
46
 
25
47
  # Regression test for #263
@@ -33,14 +55,6 @@ describe ResponseSet do
33
55
  rs2.should_not be_valid
34
56
  rs2.should have(1).errors_on(:access_code)
35
57
  end
36
-
37
- it 'defaults to a random, non-conflicting value on init' do
38
- Surveyor::Common.should_receive(:make_tiny_code).and_return('one')
39
- Surveyor::Common.should_receive(:make_tiny_code).and_return('two')
40
- Surveyor::Common.should_receive(:make_tiny_code).and_return('three')
41
-
42
- ResponseSet.new.access_code.should == 'three'
43
- end
44
58
  end
45
59
 
46
60
  it "is completable" do
@@ -63,18 +77,6 @@ describe ResponseSet do
63
77
  @response_set.completed_at.should be_nil
64
78
  end
65
79
 
66
- it "should save new responses from radio buttons, ignoring blanks" do
67
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable(@radio_response_attributes))
68
- @response_set.responses.should have(2).items
69
- @response_set.responses.detect{|r| r.question_id == 2}.answer_id.should == 6
70
- end
71
-
72
- it "should save new responses from other types, ignoring blanks" do
73
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable(@other_response_attributes))
74
- @response_set.responses.should have(1).items
75
- @response_set.responses.detect{|r| r.question_id == 7}.text_value.should == "Brian is tired"
76
- end
77
-
78
80
  it 'saves its responses' do
79
81
  new_set = ResponseSet.new(:survey => Factory(:survey))
80
82
  new_set.responses.build(:question_id => 1, :answer_id => 1, :string_value => 'XXL')
@@ -83,124 +85,151 @@ describe ResponseSet do
83
85
  ResponseSet.find(new_set.id).responses.should have(1).items
84
86
  end
85
87
 
86
- it "should ignore data if corresponding radio button is not selected" do
87
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable(@radio_response_attributes))
88
- @response_set.responses.select{|r| r.question_id == 2}.should have(1).item
89
- @response_set.responses.detect{|r| r.question_id == 2}.string_value.should == nil
90
- end
91
-
92
- it "should preserve response ids in checkboxes when adding another checkbox" do
93
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable(@checkbox_response_attributes))
94
- @response_set.responses.should have(2).items
95
- initial_response_ids = @response_set.responses.map(&:id)
96
- # adding a checkbox
97
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable({"1"=>{"question_id"=>"9", "answer_id"=>"13"}}))
98
- @response_set.responses.should have(3).items
99
- (@response_set.responses.map(&:id) - initial_response_ids).size.should == 1
100
- end
101
-
102
- it "should preserve response ids in checkboxes when removing another checkbox" do
103
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable(@checkbox_response_attributes))
104
- @response_set.responses.should have(2).items
105
- initial_response_ids = @response_set.responses.map(&:id)
106
- # removing a checkbox, reload the response set
107
- @response_set.update_attributes(:responses_attributes => ResponseSet.to_savable({"1"=>{"question_id"=>"9", "answer_id"=>"", "id" => initial_response_ids.first}}))
108
- @response_set.reload.responses.should have(1).items
109
- (initial_response_ids - @response_set.responses.map(&:id)).size.should == 1
110
- end
111
- it "should clean up a blank or empty hash" do
112
- ResponseSet.to_savable(nil).should == []
113
- ResponseSet.to_savable({}).should == []
114
- end
115
-
116
- it "should clean up responses_attributes before passing to nested_attributes" do
117
- hash_of_hashes = {
118
- "11" => {"question_id" => "1", "answer_id" => [""]}, # new checkbox, blank
119
- "12" => {"question_id" => "2", "answer_id" => ["", "124"]}, # new checkbox, checked
120
- "13" => {"id" => "101", "question_id" => "3", "answer_id" => [""]}, # existing checkbox, unchecked
121
- "14" => {"id" => "102", "question_id" => "4", "answer_id" => ["", "147"]}, # existing checkbox, left alone
122
- "15" => {"question_id" => "5", "answer_id" => ""}, # new radio, blank
123
- "16" => {"question_id" => "6", "answer_id" => "161"}, # new radio, selected
124
- "17" => {"id" => "103", "question_id" => "7", "answer_id" => "171"}, # existing radio, changed
125
- "18" => {"id" => "104", "question_id" => "8", "answer_id" => "181"}, # existing radio, unchanged
126
- "19" => {"question_id" => "9", "answer_id" => "191", "string_value" => ""}, # new string, blank
127
- "20" => {"question_id" => "10", "answer_id" => "201", "string_value" => "hi"}, # new string, filled
128
- "21" => {"id" => "105", "question_id" => "11", "answer_id" => "211", "string_value" => ""}, # existing string, cleared
129
- "22" => {"id" => "106", "question_id" => "12", "answer_id" => "221", "string_value" => "ho"}, # existing string, changed
130
- "23" => {"id" => "107", "question_id" => "13", "answer_id" => "231", "string_value" => "hi"}, # existing string, unchanged
131
- "24" => {"question_id" => "14", "answer_id" => [""], "string_value" => "foo"}, # new checkbox with string value, blank
132
- "25" => {"question_id" => "15", "answer_id" => ["", "241"], "string_value" => "bar"}, # new checkbox with string value, checked
133
- "26" => {"id" => "108", "question_id" => "14", "answer_id" => [""], "string_value" => "moo"}, # existing checkbox with string value, unchecked
134
- "27" => {"id" => "109", "question_id" => "15", "answer_id" => ["", "251"], "string_value" => "mar"}, # existing checkbox with string value, left alone
135
- "28" => {"question_id" => "16", "answer_id" => "", "string_value" => "foo"}, # new radio with string value, blank
136
- "29" => {"question_id" => "17", "answer_id" => "261", "string_value" => "bar"}, # new radio with string value, selected
137
- "30" => {"id" => "110", "question_id" => "18", "answer_id" => "271", "string_value" => "moo"}, # existing radio with string value, changed
138
- "31" => {"id" => "111", "question_id" => "19", "answer_id" => "281", "string_value" => "mar"} # existing radio with string value, unchanged
139
- }
140
-
141
- Set.new(ResponseSet.to_savable(hash_of_hashes)).should == Set.new([
142
- # "11" => {"question_id" => "1", "answer_id" => [""]}, # new checkbox, blank
143
- {"question_id"=>"2", "answer_id"=>["", "124"]}, # new checkbox, checked
144
- {"question_id"=>"3", "id"=>"101", "_destroy"=>"1"}, # existing checkbox, unchecked
145
- {"question_id"=>"4", "id"=>"102", "answer_id"=>["", "147"]}, # existing checkbox, left alone
146
- # "15" => {"question_id" => "5", "answer_id" => ""}, # new radio, blank
147
- {"question_id"=>"6", "answer_id"=>"161"}, # new radio, selected
148
- {"question_id"=>"7", "id"=>"103", "answer_id"=>"171"}, # existing radio, changed
149
- {"question_id"=>"8", "id"=>"104", "answer_id"=>"181"}, # existing radio, unchanged
150
- # "19" => {"question_id" => "9", "answer_id" => "191", "string_value" => ""}, # new string, blank
151
- {"question_id"=>"10", "string_value"=>"hi", "answer_id"=>"201"}, # new string, filled
152
- {"question_id"=>"11", "string_value"=>"", "id"=>"105", "_destroy"=>"1"}, # existing string, cleared
153
- {"question_id"=>"12", "id"=>"106", "string_value"=>"ho", "answer_id"=>"221"}, # existing string, changed
154
- {"question_id"=>"13", "id"=>"107", "string_value"=>"hi", "answer_id"=>"231"}, # existing string, unchanged
155
- # "24" => {"question_id" => "14", "answer_id" => [""], "string_value" => "foo"}, # new checkbox with string value, blank
156
- {"question_id"=>"15", "string_value"=>"bar", "answer_id"=>["", "241"]}, # new checkbox with string value, checked
157
- {"question_id"=>"14", "string_value"=>"moo", "id"=>"108", "_destroy"=>"1"}, # existing checkbox with string value, unchecked
158
- {"question_id"=>"15", "id"=>"109", "string_value"=>"mar", "answer_id"=>["", "251"]},# existing checkbox with string value, left alone
159
- # "28" => {"question_id" => "16", "answer_id" => "", "string_value" => "foo"}, # new radio with string value, blank
160
- {"question_id"=>"17", "string_value"=>"bar", "answer_id"=>"261"}, # new radio with string value, selected
161
- {"question_id"=>"18", "id"=>"110", "string_value"=>"moo", "answer_id"=>"271"}, # existing radio with string value, changed
162
- {"question_id"=>"19", "id"=>"111", "string_value"=>"mar", "answer_id"=>"281"} # existing radio with string value, unchanged
163
- ])
164
- end
165
-
166
- it "should clean up radio and string responses_attributes before passing to nested_attributes" do
167
- @qone = Factory(:question, :pick => "one")
168
- hash_of_hashes = {
169
- "32" => {"question_id" => @qone.id, "answer_id" => "291", "string_value" => ""} # new radio with blank string value, selected
170
- }
171
- ResponseSet.to_savable(hash_of_hashes).should == [
172
- {"question_id" => @qone.id, "answer_id" => "291", "string_value" => ""} # new radio with blank string value, selected
173
- ]
174
- end
175
-
176
- it "should clean up responses for lookups to get ids after saving via ajax" do
177
- hash_of_hashes = {"1"=>{"question_id"=>"2", "answer_id"=>"1"},
178
- "2"=>{"question_id"=>"3", "answer_id"=>["", "6"]},
179
- "9"=>{"question_id"=>"6", "string_value"=>"jack", "answer_id"=>"13"},
180
- "17"=>{"question_id"=>"13", "datetime_value(1i)"=>"2006", "datetime_value(2i)"=>"2", "datetime_value(3i)"=>"4", "datetime_value(4i)"=>"02", "datetime_value(5i)"=>"05", "answer_id"=>"21"},
181
- "18"=>{"question_id"=>"14", "datetime_value(1i)"=>"1", "datetime_value(2i)"=>"1", "datetime_value(3i)"=>"1", "datetime_value(4i)"=>"01", "datetime_value(5i)"=>"02", "answer_id"=>"22"},
182
- "19"=>{"question_id"=>"15", "datetime_value"=>"", "answer_id"=>"23", "id" => "1"},
183
- "47"=>{"question_id"=>"38", "answer_id"=>"220", "integer_value"=>"2", "id" => "2"},
184
- "61"=>{"question_id"=>"44", "response_group"=>"0", "answer_id"=>"241", "integer_value"=>"12"}}
185
- ResponseSet.trim_for_lookups(hash_of_hashes).should ==
186
- { "1"=>{"question_id"=>"2", "answer_id"=>"1"},
187
- "2"=>{"question_id"=>"3", "answer_id"=>["", "6"]},
188
- "9"=>{"question_id"=>"6", "answer_id"=>"13"},
189
- "17"=>{"question_id"=>"13", "answer_id"=>"21"},
190
- "18"=>{"question_id"=>"14", "answer_id"=>"22"},
191
- "19"=>{"question_id"=>"15", "answer_id"=>"23", "id" => "1", "_destroy" => "true"},
192
- "47"=>{"question_id"=>"38", "answer_id"=>"220", "id" => "2"},
193
- "61"=>{"question_id"=>"44", "response_group"=>"0", "answer_id"=>"241"}
194
- }
195
- end
196
-
197
- it "should remove responses" do
198
- r = @response_set.responses.create(:question_id => 1, :answer_id => 2)
199
- r.id.should_not be nil
200
- @response_set.should have(1).responses
201
- ResponseSet.to_savable({"2"=>{"question_id"=>"1", "id"=> r.id, "answer_id"=>[""]}}).should == [{"question_id"=>"1", "id"=> r.id, "_destroy"=> "1" }]
202
- @response_set.update_attributes(:responses_attributes => [{"question_id"=>"1", "id"=> r.id, "_destroy"=> "1"}]).should be_true
203
- @response_set.reload.should have(0).responses
88
+ describe '#update_from_ui_hash' do
89
+ let(:ui_hash) { {} }
90
+ let(:api_id) { 'ABCDEF-1234-567890' }
91
+
92
+ let(:question_id) { 42 }
93
+ let(:answer_id) { 137 }
94
+
95
+ def ui_response(attrs={})
96
+ { 'question_id' => question_id.to_s, 'api_id' => api_id }.merge(attrs)
97
+ end
98
+
99
+ def do_ui_update
100
+ response_set.update_from_ui_hash(ui_hash)
101
+ end
102
+
103
+ def resulting_response
104
+ # response_set_id criterion is to make sure a created response is
105
+ # appropriately associated.
106
+ Response.where(:api_id => api_id, :response_set_id => response_set).first
107
+ end
108
+
109
+ shared_examples 'pick one or any' do
110
+ it 'saves an answer alone' do
111
+ ui_hash['3'] = ui_response('answer_id' => set_answer_id)
112
+ do_ui_update
113
+ resulting_response.answer_id.should == answer_id
114
+ end
115
+
116
+ it 'preserves the question' do
117
+ ui_hash['4'] = ui_response('answer_id' => set_answer_id)
118
+ do_ui_update
119
+ resulting_response.question_id.should == question_id
120
+ end
121
+
122
+ it 'interprets a blank answer as no response' do
123
+ ui_hash['7'] = ui_response('answer_id' => blank_answer_id)
124
+ do_ui_update
125
+ resulting_response.should be_nil
126
+ end
127
+
128
+ it 'interprets no answer_id as no response' do
129
+ ui_hash['8'] = ui_response
130
+ do_ui_update
131
+ resulting_response.should be_nil
132
+ end
133
+
134
+ [
135
+ ['string_value', 'foo', '', 'foo'],
136
+ ['datetime_value', '2010-10-01', '', Date.new(2010, 10, 1)],
137
+ ['integer_value', '9', '', 9],
138
+ ['float_value', '4.0', '', 4.0],
139
+ ['text_value', 'more than foo', '', 'more than foo']
140
+ ].each do |value_type, set_value, blank_value, expected_value|
141
+ describe "plus #{value_type}" do
142
+ it 'saves the value' do
143
+ ui_hash['11'] = ui_response('answer_id' => set_answer_id, value_type => set_value)
144
+ do_ui_update
145
+ resulting_response.send(value_type).should == expected_value
146
+ end
147
+
148
+ it 'interprets a blank answer as no response' do
149
+ ui_hash['18'] = ui_response('answer_id' => blank_answer_id, value_type => set_value)
150
+ do_ui_update
151
+ resulting_response.should be_nil
152
+ end
153
+
154
+ it 'interprets a blank value as no response' do
155
+ ui_hash['29'] = ui_response('answer_id' => set_answer_id, value_type => blank_value)
156
+ do_ui_update
157
+ resulting_response.should be_nil
158
+ end
159
+
160
+ it 'interprets no answer_id as no response' do
161
+ ui_hash['8'] = ui_response(value_type => set_value)
162
+ do_ui_update
163
+ resulting_response.should be_nil
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ shared_examples 'response interpretation' do
170
+ it 'fails when api_id is not provided' do
171
+ ui_hash['0'] = { 'question_id' => question_id }
172
+ lambda { do_ui_update }.should raise_error(/api_id missing from response 0/)
173
+ end
174
+
175
+ describe 'for a radio button' do
176
+ let(:set_answer_id) { answer_id.to_s }
177
+ let(:blank_answer_id) { '' }
178
+
179
+ include_examples 'pick one or any'
180
+ end
181
+
182
+ describe 'for a checkbox' do
183
+ let(:set_answer_id) { ['', answer_id.to_s] }
184
+ let(:blank_answer_id) { [''] }
185
+
186
+ include_examples 'pick one or any'
187
+ end
188
+ end
189
+
190
+ describe 'with a new response' do
191
+ include_examples 'response interpretation'
192
+
193
+ # After much effort I cannot produce this situation in a test, either with
194
+ # with threads or separate processes. While SQLite 3 will nominally allow
195
+ # for some coarse-grained concurrency, it does not appear to work with
196
+ # simultaneous write transactions the way AR uses SQLite. Instead,
197
+ # simultaneous write transactions always result in a
198
+ # SQLite3::BusyException, regardless of the connection's timeout setting.
199
+ it 'fails predicably when another response with the same api_id is created in a simultaneous open transaction'
200
+ end
201
+
202
+ describe 'with an existing response' do
203
+ let!(:original_response) {
204
+ response_set.responses.build(:question_id => question_id, :answer_id => answer_id).tap do |r|
205
+ r.api_id = api_id # not mass assignable
206
+ r.save!
207
+ end
208
+ }
209
+
210
+ include_examples 'response interpretation'
211
+
212
+ it 'fails when the existing response is for a different question' do
213
+ ui_hash['76'] = ui_response('question_id' => '43', 'answer_id' => answer_id.to_s)
214
+
215
+ lambda { do_ui_update }.should raise_error(/Illegal attempt to change question for response #{api_id}./)
216
+ end
217
+ end
218
+
219
+ # clean_with_truncation is necessary because AR 3.0 can't roll back a nested
220
+ # transaction with SQLite.
221
+ it 'rolls back all changes on failure', :clean_with_truncation do
222
+ ui_hash['0'] = ui_response('question_id' => '42', 'answer_id' => answer_id.to_s)
223
+ ui_hash['1'] = { 'answer_id' => '7' } # no api_id
224
+
225
+ begin
226
+ do_ui_update
227
+ fail "Expected error did not occur"
228
+ rescue
229
+ end
230
+
231
+ response_set.reload.responses.should be_empty
232
+ end
204
233
  end
205
234
  end
206
235
 
@@ -423,3 +452,24 @@ describe ResponseSet, "exporting csv" do
423
452
  csv.should match /pecan pie/
424
453
  end
425
454
  end
455
+
456
+ describe ResponseSet, "#as_json" do
457
+ let(:rs) {
458
+ Factory(:response_set, :responses => [
459
+ Factory(:response, :question => Factory(:question), :answer => Factory(:answer), :string_value => '2')])
460
+ }
461
+
462
+ let(:js) {rs.as_json}
463
+
464
+ it "should include uuid, survey_id" do
465
+ js[:uuid].should == rs.api_id
466
+ end
467
+
468
+ it "should include responses with uuid, question_id, answer_id, value" do
469
+ r0 = rs.responses[0]
470
+ js[:responses][0][:uuid].should == r0.api_id
471
+ js[:responses][0][:answer_id].should == r0.answer.api_id
472
+ js[:responses][0][:question_id].should == r0.question.api_id
473
+ js[:responses][0][:value].should == r0.string_value
474
+ end
475
+ end
@@ -39,6 +39,19 @@ describe Response, "when saving a response" do
39
39
  response2 = Factory(:response, :question => Factory(:question), :answer => Factory(:answer), :response_set => @response.response_set, :created_at => (@response.created_at + 1))
40
40
  Response.all.should == [@response, response2]
41
41
  end
42
+
43
+ it "should protect api_id, timestamps" do
44
+ saved_attrs = @response.attributes
45
+ if defined? ActiveModel::MassAssignmentSecurity::Error
46
+ lambda {@response.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
47
+ lambda {@response.update_attributes(:api_id => "NEW")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
48
+ else
49
+ @response.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
50
+ @response.attributes = {:api_id => "NEW"} # Rails doesn't return false, but this will be checked in the comparison to saved_attrs
51
+ end
52
+ @response.attributes.should == saved_attrs
53
+ end
54
+
42
55
 
43
56
  describe "returns the response as the type requested" do
44
57
  it "returns 'string'" do
@@ -114,3 +127,50 @@ describe Response, "applicable_attributes" do
114
127
  should == {"question_id"=>@who.id, "answer_id"=>[""], "string_value"=>"Frank"}
115
128
  end
116
129
  end
130
+
131
+ describe Response, '#json_value' do
132
+ context "when integer" do
133
+ let(:r) {Response.new(:integer_value => 2, :answer => Answer.new(:response_class => 'integer'))}
134
+ it "should be 2" do
135
+ r.json_value.should == 2
136
+ end
137
+ end
138
+
139
+ context "when float" do
140
+ let(:r) {Response.new(:float_value => 3.14, :answer => Answer.new(:response_class => 'float'))}
141
+ it "should be 3.14" do
142
+ r.json_value.should == 3.14
143
+ end
144
+ end
145
+
146
+ context "when string" do
147
+ let(:r) {Response.new(:string_value => 'bar', :answer => Answer.new(:response_class => 'string'))}
148
+ it "should be 'bar'" do
149
+ r.json_value.should == 'bar'
150
+ end
151
+ end
152
+
153
+ context "when datetime" do
154
+ let(:r) {Response.new(:datetime_value => DateTime.strptime('2010-04-08T10:30+00:00', '%Y-%m-%dT%H:%M%z'), :answer => Answer.new(:response_class => 'datetime'))}
155
+ it "should be '2010-04-08T10:30+00:00'" do
156
+ r.json_value.should == '2010-04-08T10:30+00:00'
157
+ r.json_value.to_json.should == '"2010-04-08T10:30+00:00"'
158
+ end
159
+ end
160
+
161
+ context "when date" do
162
+ let(:r) {Response.new(:datetime_value => DateTime.strptime('2010-04-08', '%Y-%m-%d'), :answer => Answer.new(:response_class => 'date'))}
163
+ it "should be '2010-04-08'" do
164
+ r.json_value.should == '2010-04-08'
165
+ r.json_value.to_json.should == '"2010-04-08"'
166
+ end
167
+ end
168
+
169
+ context "when time" do
170
+ let(:r) {Response.new(:datetime_value => DateTime.strptime('10:30', '%H:%M'), :answer => Answer.new(:response_class => 'time'))}
171
+ it "should be '10:30'" do
172
+ r.json_value.should == '10:30'
173
+ r.json_value.to_json.should == '"10:30"'
174
+ end
175
+ end
176
+ end
@@ -16,6 +16,15 @@ describe SurveySection, "when saving a survey_section" do
16
16
  # @survey_section.survey_id = nil
17
17
  # @survey_section.should have(1).error_on(:survey)
18
18
  end
19
+ it "should protect timestamps" do
20
+ saved_attrs = @survey_section.attributes
21
+ if defined? ActiveModel::MassAssignmentSecurity::Error
22
+ lambda {@survey_section.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
23
+ else
24
+ @survey_section.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
25
+ end
26
+ @survey_section.attributes.should == saved_attrs
27
+ end
19
28
  end
20
29
 
21
30
  describe SurveySection, "with questions" do
@@ -11,15 +11,28 @@ describe Survey, "when saving a new one" do
11
11
  @survey.should have(1).error_on(:title)
12
12
  end
13
13
 
14
- it "should adjust the title to save unique titles" do
14
+ it "should adjust the survey_version to save unique survey_version for each title" do
15
15
  original = Survey.new(:title => "Foo")
16
16
  original.save.should be_true
17
+ original.survey_version.should == 0
17
18
  imposter = Survey.new(:title => "Foo")
18
19
  imposter.save.should be_true
19
- imposter.title.should == "Foo 1"
20
+ imposter.title.should == "Foo"
21
+ imposter.survey_version.should == 1
20
22
  bandwagoneer = Survey.new(:title => "Foo")
21
23
  bandwagoneer.save.should be_true
22
- bandwagoneer.title.should == "Foo 2"
24
+ bandwagoneer.title.should == "Foo"
25
+ bandwagoneer.survey_version.should == 2
26
+ end
27
+
28
+ it "should not allow to have duplicate survey_versions of the survey" do
29
+ survey = Survey.new(:title => "Foo")
30
+ survey.save.should be_true
31
+ imposter = Survey.new(:title => "Foo")
32
+ imposter.save.should be_true
33
+ imposter.survey_version = 0
34
+ imposter.save.should be_false
35
+ imposter.should have(1).error_on(:survey_version)
23
36
  end
24
37
 
25
38
  it "should not adjust the title when updating itself" do
@@ -72,19 +85,21 @@ describe Survey do
72
85
  it "should be inactive by default" do
73
86
  @survey.active?.should == false
74
87
  end
75
-
88
+ it "should have both inactive_at and active_at be null by default" do
89
+ @survey.active_at.should be_nil
90
+ @survey.inactive_at.should be_nil
91
+ end
92
+
76
93
  it "should be active or active as of a certain date/time" do
77
- @survey.inactive_at = 3.days.ago
94
+ @survey.inactive_at = 2.days.from_now
78
95
  @survey.active_at = 2.days.ago
79
96
  @survey.active?.should be_true
80
- @survey.inactive_at.should be_nil
81
97
  end
82
98
 
83
99
  it "should be able to deactivate as of a certain date/time" do
84
- @survey.active_at = 2.days.ago
85
- @survey.inactive_at = 3.days.ago
100
+ @survey.active_at = 3.days.ago
101
+ @survey.inactive_at = 1.days.ago
86
102
  @survey.active?.should be_false
87
- @survey.active_at.should be_nil
88
103
  end
89
104
 
90
105
  it "should activate and deactivate" do
@@ -94,4 +109,52 @@ describe Survey do
94
109
  @survey.active?.should be_false
95
110
  end
96
111
 
112
+ it "should should nil out values of inactive_at that are in the past on activate" do
113
+ @survey.inactive_at = 5.days.ago
114
+ @survey.active?.should be_false
115
+ @survey.activate!
116
+ @survey.active?.should be_true
117
+ @survey.inactive_at.should be_nil
118
+ end
119
+
120
+ it "should should nil out values of active_at that are in the past on deactivate" do
121
+ @survey.active_at = 5.days.ago
122
+ @survey.active?.should be_true
123
+ @survey.deactivate!
124
+ @survey.active?.should be_false
125
+ @survey.active_at.should be_nil
126
+ end
127
+
128
+ it "should protect access_code, api_id, active_at, inactive_at, timestamps" do
129
+ saved_attrs = @survey.attributes
130
+ if defined? ActiveModel::MassAssignmentSecurity::Error
131
+ lambda {@survey.update_attributes(:access_code => "NEW")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
132
+ lambda {@survey.update_attributes(:api_id => "AND")}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
133
+ lambda {@survey.update_attributes(:active_at => 2.days.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
134
+ lambda {@survey.update_attributes(:inactive_at => 3.days.from_now)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
135
+ lambda {@survey.update_attributes(:created_at => 3.days.ago, :updated_at => 3.hours.ago)}.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
136
+ else
137
+ @survey.update_attributes(:access_code => "NEW").should be_false
138
+ @survey.update_attributes(:api_id => "AND").should be_false
139
+ @survey.update_attributes(:active_at => 2.days.ago).should be_false
140
+ @survey.update_attributes(:inactive_at => 3.days.from_now).should be_false
141
+ @survey.attributes = {:created_at => 3.days.ago, :updated_at => 3.hours.ago} # automatically protected by Rails
142
+ end
143
+ @survey.attributes.should == saved_attrs
144
+ end
145
+
146
+ it "should include title, sections, and questions when serialized" do
147
+ survey = Factory(:survey, :title => "Foo")
148
+ s1 = Factory(:survey_section, :survey => survey, :title => "wise")
149
+ s2 = Factory(:survey_section, :survey => survey, :title => "er")
150
+ q1 = Factory(:question, :survey_section => s1, :text => "what is wise?")
151
+ q2 = Factory(:question, :survey_section => s2, :text => "what is er?")
152
+ q3 = Factory(:question, :survey_section => s2, :text => "what is mill?")
153
+
154
+ actual = survey.as_json
155
+ actual[:title].should == 'Foo'
156
+ actual[:sections].size.should == 2
157
+ actual[:sections][0][:questions_and_groups].size.should == 1
158
+ actual[:sections][1][:questions_and_groups].size.should == 2
159
+ end
97
160
  end