Commit e6b5fe35992613ac39492bd140c027571360682f

Authored by Dmitri Garbuzov
1 parent d5055b63

Added reporting functionality to /index of questions and visitors

app/controllers/questions_controller.rb
@@ -158,7 +158,7 @@ class QuestionsController < InheritedResources::Base @@ -158,7 +158,7 @@ class QuestionsController < InheritedResources::Base
158 elsif object_type == "uploaded_ideas" 158 elsif object_type == "uploaded_ideas"
159 159
160 uploaded_ideas_by_visitor_id = @question.choices.find(:all, :select => 'creator_id, count(*) as ideas_count', 160 uploaded_ideas_by_visitor_id = @question.choices.find(:all, :select => 'creator_id, count(*) as ideas_count',
161 - :conditions => "choices.creator_id != #{@question.creator_id}", 161 + :conditions => "choices.creator_id != #{@question.creator_id}",
162 :group => 'creator_id') 162 :group => 'creator_id')
163 163
164 count = 0 164 count = 0
@@ -307,7 +307,30 @@ class QuestionsController < InheritedResources::Base @@ -307,7 +307,30 @@ class QuestionsController < InheritedResources::Base
307 end 307 end
308 308
309 def index 309 def index
310 - @questions = current_user.questions.find(:all) 310 + @questions = current_user.questions.scoped({})
  311 + @questions = @questions.created_by(params[:creator]) if params[:creator]
  312 +
  313 + counts = {}
  314 + if params[:user_ideas]
  315 + counts[:user_ideas] = Choice.count(:joins => :question,
  316 + :conditions => "choices.creator_id <> questions.creator_id",
  317 + :group => "choices.question_id")
  318 + end
  319 + if params[:active_user_ideas]
  320 + counts[:active_user_ideas] = Choice.count(:joins => :question,
  321 + :conditions => "choices.active = 1 AND choices.creator_id <> questions.creator_id",
  322 + :group => "choices.question_id")
  323 + end
  324 + if params[:votes_since]
  325 + counts[:recent_votes] = Vote.count(:joins => :question,
  326 + :conditions => ["votes.created_at > ?", params[:votes_since]],
  327 + :group => "votes.question_id")
  328 + end
  329 +
  330 + counts.each_pair do |attr,hash|
  331 + @questions.each{ |q| q[attr] = hash[q.id] || 0 }
  332 + end
  333 +
311 index! 334 index!
312 end 335 end
313 336
app/controllers/visitors_controller.rb
1 class VisitorsController < InheritedResources::Base 1 class VisitorsController < InheritedResources::Base
2 respond_to :xml, :json 2 respond_to :xml, :json
3 before_filter :authenticate 3 before_filter :authenticate
4 - actions :none 4 + actions :index
  5 +
  6 + def index
  7 + cond = params[:question_id] ? "question_id = #{params[:question_id]}" : nil
  8 +
  9 + counts = {}
  10 + if params[:votes_count]
  11 + counts[:votes_count] = Vote.count(:conditions => cond, :group => "voter_id")
  12 + end
  13 + if params[:skips_count]
  14 + counts[:skips_count] = Skip.count(:conditions => cond, :group => "skipper_id")
  15 + end
  16 + if params[:ideas_count]
  17 + idea_cond = "choices.creator_id != questions.creator_id" +
  18 + (cond ? " AND #{cond}" : "")
  19 + counts[:ideas_count] = Choice.count(:joins => :question,
  20 + :conditions => idea_cond,
  21 + :group => "choices.creator_id")
  22 + end
  23 + if params[:bounces]
  24 + counts[:bounces] = Appearance.count(:conditions => cond,
  25 + :group => "voter_id",
  26 + :having => "count(answerable_id) = 0")
  27 + end
  28 + if params[:questions_created]
  29 + counts[:questions_created] = Question.count(:group => :creator_id)
  30 + end
  31 +
  32 + # visitors belong to a site, so we can't just scope them to a question.
  33 + # instead, take the union of visitor ids with counted objects
  34 + if counts.empty?
  35 + @visitors = current_user.visitors.scoped({})
  36 + else
  37 + ids = counts.inject([]){ |ids,(k,v)| ids | v.keys }
  38 + @visitors = current_user.visitors.scoped(:conditions => { :id => ids })
  39 + end
  40 +
  41 + counts.each_pair do |attr,values|
  42 + @visitors.each{ |v| v[attr] = values[v.id] || 0 }
  43 + end
  44 +
  45 + index!
  46 + end
5 47
6 def objects_by_session_ids 48 def objects_by_session_ids
7 session_ids = params[:session_ids] 49 session_ids = params[:session_ids]
app/models/question.rb
@@ -29,6 +29,10 @@ class Question &lt; ActiveRecord::Base @@ -29,6 +29,10 @@ class Question &lt; ActiveRecord::Base
29 29
30 attr_readonly :site_id 30 attr_readonly :site_id
31 31
  32 + named_scope :created_by, lambda { |id|
  33 + {:conditions => { :local_identifier => id } }
  34 + }
  35 +
32 def create_choices_from_ideas 36 def create_choices_from_ideas
33 if ideas && ideas.any? 37 if ideas && ideas.any?
34 ideas.each do |idea| 38 ideas.each do |idea|
config/routes.rb
1 ActionController::Routing::Routes.draw do |map| 1 ActionController::Routing::Routes.draw do |map|
2 map.resources :densities, :only => :index 2 map.resources :densities, :only => :index
3 - map.resources :visitors, :only => :none, 3 + map.resources :visitors, :only => :index,
4 :collection => {:objects_by_session_ids => :post}, 4 :collection => {:objects_by_session_ids => :post},
5 :member => {:votes => :get} 5 :member => {:votes => :get}
6 map.resources :questions, :except => [:edit, :destroy], 6 map.resources :questions, :except => [:edit, :destroy],
spec/factories.rb
@@ -58,6 +58,19 @@ Factory.define(:vote) do |f| @@ -58,6 +58,19 @@ Factory.define(:vote) do |f|
58 f.voter {|v| v.question.creator} 58 f.voter {|v| v.question.creator}
59 end 59 end
60 60
  61 +Factory.define(:skip) do |f|
  62 + f.association :question, :factory => :aoi_question
  63 + f.prompt {|s| s.question.prompts.first}
  64 + f.skipper {|s| s.question.creator}
  65 +end
  66 +
  67 +Factory.define(:appearance) do |f|
  68 + f.association :question, :factory => :aoi_question
  69 + f.prompt {|a| a.question.prompts.rand}
  70 + f.voter {|a| a.question.creator}
  71 + f.answerable { nil }
  72 +end
  73 +
61 Factory.sequence :email do |n| 74 Factory.sequence :email do |n|
62 "user#{n}@example.com" 75 "user#{n}@example.com"
63 end 76 end
spec/integration/choices_spec.rb
@@ -22,7 +22,7 @@ describe &quot;Choices&quot; do @@ -22,7 +22,7 @@ describe &quot;Choices&quot; do
22 end 22 end
23 23
24 after do 24 after do
25 - post_auth question_choices_path(@question, :format => 'xml'), @params 25 + post_auth question_choices_path(@question), @params
26 response.should be_success 26 response.should be_success
27 response.should have_tag "choice" 27 response.should have_tag "choice"
28 end 28 end
@@ -35,7 +35,7 @@ describe &quot;Choices&quot; do @@ -35,7 +35,7 @@ describe &quot;Choices&quot; do
35 :data => "foo", 35 :data => "foo",
36 :local_identifier => "bar" } } 36 :local_identifier => "bar" } }
37 37
38 - post_auth question_choices_path(@question, :format => 'xml'), @params 38 + post_auth question_choices_path(@question), @params
39 39
40 response.should be_success 40 response.should be_success
41 response.should have_tag "choice creator-id", @visitor.id.to_s 41 response.should have_tag "choice creator-id", @visitor.id.to_s
@@ -52,32 +52,23 @@ describe &quot;Choices&quot; do @@ -52,32 +52,23 @@ describe &quot;Choices&quot; do
52 end 52 end
53 53
54 it "should return the deactivated choice given no arguments" do 54 it "should return the deactivated choice given no arguments" do
55 - put_auth flag_question_choice_path(@question, @choice, :format => 'xml') 55 + put_auth flag_question_choice_path(@question, @choice)
56 56
57 response.should be_success 57 response.should be_success
58 response.should have_tag "choice active", "false" 58 response.should have_tag "choice active", "false"
59 end 59 end
60 60
61 it "should return the deactivated choice given an explanation" do 61 it "should return the deactivated choice given an explanation" do
62 - put_auth flag_question_choice_path(@question, @choice, :format => 'xml'), :explanation => "foo" 62 + put_auth flag_question_choice_path(@question, @choice), :explanation => "foo"
63 63
64 response.should be_success 64 response.should be_success
65 response.should have_tag "choice active", "false" 65 response.should have_tag "choice active", "false"
66 end 66 end
67 67
68 - context "when trying to flag another site's choices" do  
69 - before do  
70 - # this is ugly  
71 - @orig_user = @api_user  
72 - @api_user = Factory(:email_confirmed_user)  
73 - end  
74 -  
75 - it "should fail" do  
76 - put_auth flag_question_choice_path(@question, @choice, :format => 'xml'), :explanation => "foo"  
77 - response.should_not be_success  
78 - end  
79 -  
80 - after { @api_user = @orig_user } 68 + it "should fail when trying to flag another site's choices" do
  69 + other_user = Factory(:email_confirmed_user)
  70 + put_auth other_user, flag_question_choice_path(@question, @choice), :explanation => "foo"
  71 + response.should_not be_success
81 end 72 end
82 end 73 end
83 74
@@ -89,14 +80,14 @@ describe &quot;Choices&quot; do @@ -89,14 +80,14 @@ describe &quot;Choices&quot; do
89 end 80 end
90 81
91 it "should return all active choices given no optional parameters" do 82 it "should return all active choices given no optional parameters" do
92 - get_auth question_choices_path(@question, :format => 'xml') 83 + get_auth question_choices_path(@question)
93 84
94 response.should be_success 85 response.should be_success
95 response.should have_tag "choices choice", 5 86 response.should have_tag "choices choice", 5
96 end 87 end
97 88
98 it "should return all choices if include_inactive is set" do 89 it "should return all choices if include_inactive is set" do
99 - get_auth question_choices_path(@question, :format => 'xml'), :include_inactive => true 90 + get_auth question_choices_path(@question), :include_inactive => true
100 91
101 response.should be_success 92 response.should be_success
102 response.should have_tag "choices choice", 10 93 response.should have_tag "choices choice", 10
@@ -105,31 +96,23 @@ describe &quot;Choices&quot; do @@ -105,31 +96,23 @@ describe &quot;Choices&quot; do
105 96
106 97
107 it "should return 3 choices when limt is set to 3" do 98 it "should return 3 choices when limt is set to 3" do
108 - get_auth question_choices_path(@question, :format => 'xml'), :limit => 3 99 + get_auth question_choices_path(@question), :limit => 3
109 100
110 response.should be_success 101 response.should be_success
111 response.should have_tag "choices choice", 3 102 response.should have_tag "choices choice", 3
112 end 103 end
113 104
114 it "should return the remaining choices when offset is provided" do 105 it "should return the remaining choices when offset is provided" do
115 - get_auth question_choices_path(@question, :format => 'xml'), :offset => 2, :limit => 4 106 + get_auth question_choices_path(@question), :offset => 2, :limit => 4
116 107
117 response.should be_success 108 response.should be_success
118 response.should have_tag "choices choice", 3 109 response.should have_tag "choices choice", 3
119 end 110 end
120 111
121 - context "when trying to access another site's choices" do  
122 - before do  
123 - @orig_user = @api_user  
124 - @api_user = Factory(:email_confirmed_user)  
125 - end  
126 -  
127 - it "should fail" do  
128 - get_auth question_choices_path(@question, :format => 'xml'), :offset => 2, :limit => 4  
129 - response.should_not be_success  
130 - end  
131 -  
132 - after { @api_user = @orig_user } 112 + it "should fail when trying to access another site's choices" do
  113 + other_user = Factory(:email_confirmed_user)
  114 + get_auth other_user, question_choices_path(@question), :offset => 2, :limit => 4
  115 + response.should_not be_success
133 end 116 end
134 117
135 end 118 end
@@ -141,23 +124,16 @@ describe &quot;Choices&quot; do @@ -141,23 +124,16 @@ describe &quot;Choices&quot; do
141 end 124 end
142 125
143 it "should return a choice" do 126 it "should return a choice" do
144 - get_auth question_choice_path(@question, @choice, :format => 'xml') 127 + get_auth question_choice_path(@question, @choice)
145 128
146 response.should be_success 129 response.should be_success
147 response.should have_tag "choice", 1 130 response.should have_tag "choice", 1
148 end 131 end
149 132
150 - context "when requesting a choice from another site" do  
151 - before do  
152 - @other_user = Factory(:email_confirmed_user)  
153 - @other_question = Factory.create(:aoi_question, :site => @other_user)  
154 - @other_choice = Factory.create(:choice, :question => @other_question)  
155 - end  
156 -  
157 - it "should fail" do  
158 - get_auth question_choice_path(@other_question, @other_choice, :format => 'xml')  
159 - response.should_not be_success  
160 - end 133 + it "should fail when requesting a choice from another site" do
  134 + other_user = Factory(:email_confirmed_user)
  135 + get_auth other_user, question_choice_path(@question, @choice)
  136 + response.should_not be_success
161 end 137 end
162 138
163 end 139 end
@@ -171,23 +147,15 @@ describe &quot;Choices&quot; do @@ -171,23 +147,15 @@ describe &quot;Choices&quot; do
171 147
172 it "should succeed given valid attributes" do 148 it "should succeed given valid attributes" do
173 params = { :choice => { :data => "foo" } } 149 params = { :choice => { :data => "foo" } }
174 - put_auth question_choice_path(@question, @choice, :format => 'xml'), params 150 + put_auth question_choice_path(@question, @choice), params
175 response.should be_success 151 response.should be_success
176 end 152 end
177 153
178 - context "when updatng another site's choice" do  
179 - before do  
180 - @orig_user = @api_user  
181 - @api_user = Factory(:email_confirmed_user)  
182 - end  
183 -  
184 - it "should fail" do  
185 - params = { :choice => { :data => "foo" } }  
186 - put_auth question_choice_path(@question, @choice, :format => 'xml'), params  
187 - response.should_not be_success  
188 - end  
189 -  
190 - after { @api_user = @orig_user } 154 + it "should fail when updating another site's choice" do
  155 + other_user = Factory(:email_confirmed_user)
  156 + params = { :choice => { :data => "foo" } }
  157 + put_auth other_user, question_choice_path(@question, @choice), params
  158 + response.should_not be_success
191 end 159 end
192 end 160 end
193 161
spec/integration/prompts_spec.rb
@@ -13,7 +13,7 @@ describe &quot;Prompts&quot; do @@ -13,7 +13,7 @@ describe &quot;Prompts&quot; do
13 end 13 end
14 14
15 it "returns a prompt object" do 15 it "returns a prompt object" do
16 - get_auth question_prompt_path(@question, @prompt, :format => 'xml') 16 + get_auth question_prompt_path(@question, @prompt)
17 response.should be_success 17 response.should be_success
18 response.should have_tag "prompt", 1 18 response.should have_tag "prompt", 1
19 end 19 end
@@ -31,11 +31,11 @@ describe &quot;Prompts&quot; do @@ -31,11 +31,11 @@ describe &quot;Prompts&quot; do
31 :with_prompt => true, 31 :with_prompt => true,
32 :visitor_identifier => @visitor.identifier ) 32 :visitor_identifier => @visitor.identifier )
33 @appearance_id = info[:appearance_id] 33 @appearance_id = info[:appearance_id]
34 - @prompt_id = info[:picked_prompt_id] 34 + @picked_prompt_id = info[:picked_prompt_id]
35 end 35 end
36 36
37 it "should return a new skip object given no optional parameters" do 37 it "should return a new skip object given no optional parameters" do
38 - post_auth skip_question_prompt_path(@question.id, @prompt_id, :format => 'xml') 38 + post_auth skip_question_prompt_path(@question.id, @picked_prompt_id)
39 response.should be_success 39 response.should be_success
40 response.should have_tag "skip", 1 40 response.should have_tag "skip", 1
41 end 41 end
@@ -48,7 +48,7 @@ describe &quot;Prompts&quot; do @@ -48,7 +48,7 @@ describe &quot;Prompts&quot; do
48 :skip_reason => "bar", 48 :skip_reason => "bar",
49 :appearance_lookup => @appearance_id, 49 :appearance_lookup => @appearance_id,
50 :time_viewed => 47 } } 50 :time_viewed => 47 } }
51 - post_auth skip_question_prompt_path(@question, @prompt_id, :format => 'xml'), params 51 + post_auth skip_question_prompt_path(@question, @picked_prompt_id), params
52 response.should be_success 52 response.should be_success
53 response.should have_tag "skip", 1 53 response.should have_tag "skip", 1
54 response.should have_tag "skip appearance-id", @appearance_id.to_s 54 response.should have_tag "skip appearance-id", @appearance_id.to_s
@@ -65,7 +65,7 @@ describe &quot;Prompts&quot; do @@ -65,7 +65,7 @@ describe &quot;Prompts&quot; do
65 :with_appearance => true, 65 :with_appearance => true,
66 :algorithm => "catchup", 66 :algorithm => "catchup",
67 :with_visitor_stats => true } } 67 :with_visitor_stats => true } }
68 - post_auth skip_question_prompt_path(@question, @prompt_id, :format => 'xml'), params 68 + post_auth skip_question_prompt_path(@question, @picked_prompt_id), params
69 response.should be_success 69 response.should be_success
70 response.should have_tag "prompt", 1 70 response.should have_tag "prompt", 1
71 response.should have_tag "prompt appearance_id", /.+/ 71 response.should have_tag "prompt appearance_id", /.+/
@@ -73,19 +73,12 @@ describe &quot;Prompts&quot; do @@ -73,19 +73,12 @@ describe &quot;Prompts&quot; do
73 response.should have_tag "prompt visitor_ideas", /\d+/ 73 response.should have_tag "prompt visitor_ideas", /\d+/
74 end 74 end
75 75
76 - context "when trying to skip another site's questions" do  
77 - before do  
78 - @orig_user = @api_user  
79 - @api_user = Factory(:email_confirmed_user)  
80 - end  
81 -  
82 - it "should fail" do  
83 - post_auth skip_question_prompt_path(@question.id, @prompt_id, :format => 'xml')  
84 - response.should_not be_success  
85 - end  
86 -  
87 - after { @api_user = @orig_user } 76 + it "should fail when trying to skip another site's questions" do
  77 + other_user = Factory(:email_confirmed_user)
  78 + post_auth other_user, skip_question_prompt_path(@question, @picked_prompt_id)
  79 + response.should_not be_success
88 end 80 end
  81 +
89 end 82 end
90 83
91 describe "POST 'vote'" do 84 describe "POST 'vote'" do
@@ -101,17 +94,17 @@ describe &quot;Prompts&quot; do @@ -101,17 +94,17 @@ describe &quot;Prompts&quot; do
101 :with_prompt => true, 94 :with_prompt => true,
102 :visitor_identifier => @visitor.identifier ) 95 :visitor_identifier => @visitor.identifier )
103 @appearance_id = info[:appearance_id] 96 @appearance_id = info[:appearance_id]
104 - @prompt_id = info[:picked_prompt_id] 97 + @picked_prompt_id = info[:picked_prompt_id]
105 end 98 end
106 99
107 it "should fail without the required 'direction' parameter" do 100 it "should fail without the required 'direction' parameter" do
108 - post_auth vote_question_prompt_path(@question.id, @prompt_id, :format => 'xml') 101 + post_auth vote_question_prompt_path(@question.id, @picked_prompt_id)
109 response.should_not be_success 102 response.should_not be_success
110 end 103 end
111 104
112 it "should return a new vote object given no optional parameters" do 105 it "should return a new vote object given no optional parameters" do
113 params = { :vote => { :direction => "left" } } 106 params = { :vote => { :direction => "left" } }
114 - post_auth vote_question_prompt_path(@question.id, @prompt_id, :format => 'xml'), params 107 + post_auth vote_question_prompt_path(@question.id, @picked_prompt_id), params
115 response.should be_success 108 response.should be_success
116 response.should have_tag "vote", 1 109 response.should have_tag "vote", 1
117 end 110 end
@@ -124,7 +117,7 @@ describe &quot;Prompts&quot; do @@ -124,7 +117,7 @@ describe &quot;Prompts&quot; do
124 :direction => "right", 117 :direction => "right",
125 :appearance_lookup => @appearance_id, 118 :appearance_lookup => @appearance_id,
126 :time_viewed => 47 } } 119 :time_viewed => 47 } }
127 - post_auth vote_question_prompt_path(@question, @prompt_id, :format => 'xml'), params 120 + post_auth vote_question_prompt_path(@question, @picked_prompt_id), params
128 response.should be_success 121 response.should be_success
129 response.should have_tag "vote", 1 122 response.should have_tag "vote", 1
130 response.should have_tag "vote appearance-id", @appearance_id.to_s 123 response.should have_tag "vote appearance-id", @appearance_id.to_s
@@ -143,7 +136,7 @@ describe &quot;Prompts&quot; do @@ -143,7 +136,7 @@ describe &quot;Prompts&quot; do
143 :with_appearance => true, 136 :with_appearance => true,
144 :algorithm => "catchup", 137 :algorithm => "catchup",
145 :with_visitor_stats => true } } 138 :with_visitor_stats => true } }
146 - post_auth vote_question_prompt_path(@question, @prompt_id, :format => 'xml'), params 139 + post_auth vote_question_prompt_path(@question, @picked_prompt_id), params
147 response.should be_success 140 response.should be_success
148 response.should have_tag "prompt", 1 141 response.should have_tag "prompt", 1
149 response.should have_tag "prompt appearance_id", /.+/ 142 response.should have_tag "prompt appearance_id", /.+/
@@ -151,19 +144,11 @@ describe &quot;Prompts&quot; do @@ -151,19 +144,11 @@ describe &quot;Prompts&quot; do
151 response.should have_tag "prompt visitor_ideas", /\d+/ 144 response.should have_tag "prompt visitor_ideas", /\d+/
152 end 145 end
153 146
154 - context "when trying to vote on another site's questions" do  
155 - before do  
156 - @orig_user = @api_user  
157 - @api_user = Factory(:email_confirmed_user)  
158 - end  
159 -  
160 - it "should fail" do  
161 - params = { :vote => { :direction => "left" } }  
162 - post_auth vote_question_prompt_path(@question.id, @prompt_id, :format => 'xml'), params  
163 - response.should_not be_success  
164 - end  
165 -  
166 - after { @api_user = @orig_user } 147 + it "should fail when trying to vote on another site's questions" do
  148 + other_user = Factory(:email_confirmed_user)
  149 + params = { :vote => { :direction => "left" } }
  150 + post_auth other_user, vote_question_prompt_path(@question.id, @picked_prompt_id), params
  151 + response.should_not be_success
167 end 152 end
168 153
169 end 154 end
spec/integration/questions_spec.rb
@@ -3,29 +3,81 @@ require File.expand_path(File.dirname(__FILE__) + &#39;/../spec_helper&#39;) @@ -3,29 +3,81 @@ require File.expand_path(File.dirname(__FILE__) + &#39;/../spec_helper&#39;)
3 describe "Questions" do 3 describe "Questions" do
4 include IntegrationSupport 4 include IntegrationSupport
5 before do 5 before do
6 - 3.times{ Factory.create(:aoi_question, :site => @api_user) } 6 + @user = self.default_user = Factory(:email_confirmed_user)
  7 + @choices = {}
  8 + @questions = Array.new(5){ Factory(:aoi_question, :site => @user) }.each do |q|
  9 + @choices[q.id] = Array.new(rand(10)){ Factory(:choice, :question => q, :active => (rand(2)==1)) }
  10 + end
7 end 11 end
8 12
9 describe "GET 'index'" do 13 describe "GET 'index'" do
10 it "should return an array of questions" do 14 it "should return an array of questions" do
11 get_auth questions_path(:format => 'xml') 15 get_auth questions_path(:format => 'xml')
  16 + response.should be_success
  17 + response.body.should have_tag("questions question", @questions.size)
  18 + end
  19 +
  20 + it "should not return other users' questions" do
  21 + other_user = Factory(:email_confirmed_user)
  22 + other_questions = Array.new(5){ Factory(:aoi_question, :site => other_user) }
  23 +
  24 + get_auth other_user, questions_path(:format => 'xml')
  25 +
  26 + response.should be_success
  27 + response.body.should have_tag "questions question site-id", :count => 5, :text => other_user.id.to_s
  28 + response.body.should_not have_tag "site-id", @user.id.to_s
  29 + end
  30 +
  31 + it "should return a list of questions for a specific creator" do
  32 + 3.times{ Factory(:aoi_question,
  33 + :site => @user,
  34 + :local_identifier => "jim") }
  35 +
  36 + get_auth questions_path(:format => 'xml'), {:creator => "jim"}
  37 + response.should be_success
12 response.body.should have_tag("questions question", 3) 38 response.body.should have_tag("questions question", 3)
  39 + response.body.should have_tag("questions question local-identifier", "jim")
  40 + end
  41 +
  42 + it "should calculate the total number of user-submitted choices" do
  43 + get_auth questions_path(:format => 'xml'), :user_ideas => true
  44 +
13 response.should be_success 45 response.should be_success
  46 + response.body.should have_tag("question", @questions.size)
  47 + @choices.each_value do |cs|
  48 + response.body.should have_tag("user-ideas", :text => cs.size)
  49 + end
14 end 50 end
15 51
16 - context "when calling index as another user" do  
17 - before do  
18 - @orig_user = @api_user  
19 - @api_user = Factory(:email_confirmed_user) 52 + it "should calculate the number of active user-submitted choices" do
  53 + get_auth questions_path(:format => 'xml'), :active_user_ideas => true
  54 +
  55 + response.should be_success
  56 + response.body.should have_tag("question", @questions.size)
  57 + @choices.each_value do |cs|
  58 + count = cs.select{|c| c.active}.size
  59 + response.body.should have_tag "active-user-ideas", :text => count
20 end 60 end
21 -  
22 - it "should not return the questions of the original user" do  
23 - get_auth questions_path(:format => 'xml')  
24 - response.should be_success  
25 - response.body.should_not have_tag("question") 61 + end
  62 +
  63 + it "should calculate the number of votes submitted since some date" do
  64 + votes = {}
  65 + @questions.each do |q|
  66 + votes[q.id] = Array.new(20) do
  67 + Factory(:vote, :question => q, :created_at => rand(365).days.ago)
  68 + end
  69 + end
  70 + date = rand(365).days.ago
  71 + get_auth questions_path(:format => 'xml'), :votes_since => date.strftime("%Y-%m-%d")
  72 +
  73 + response.should be_success
  74 + response.body.should have_tag("question", @questions.size)
  75 + votes.each_value do |vs|
  76 + count = vs.select{|v| v.created_at > date}.size
  77 + response.body.should have_tag"recent-votes", :text => count
26 end 78 end
27 - after { @api_user = @orig_user }  
28 end 79 end
  80 +
29 end 81 end
30 82
31 describe "GET 'new'" do 83 describe "GET 'new'" do
@@ -91,7 +143,7 @@ describe &quot;Questions&quot; do @@ -91,7 +143,7 @@ describe &quot;Questions&quot; do
91 143
92 it "should fail given invalid parameters" do 144 it "should fail given invalid parameters" do
93 params = { :type => "ideas", :response_type => "foo", :redisk_key => "bar" } 145 params = { :type => "ideas", :response_type => "foo", :redisk_key => "bar" }
94 - post_auth export_question_path(@question, :format => 'xml') 146 + post_auth export_question_path(@question)
95 response.should be_success 147 response.should be_success
96 response.body.should =~ /Error/ 148 response.body.should =~ /Error/
97 end 149 end
@@ -108,7 +160,7 @@ describe &quot;Questions&quot; do @@ -108,7 +160,7 @@ describe &quot;Questions&quot; do
108 before { @question = Factory.create(:aoi_question, :site => @api_user) } 160 before { @question = Factory.create(:aoi_question, :site => @api_user) }
109 161
110 it "should succeed given no optional parameters" do 162 it "should succeed given no optional parameters" do
111 - get_auth question_path(@question, :format => 'xml') 163 + get_auth question_path(@question)
112 response.should be_success 164 response.should be_success
113 response.should have_tag "question", 1 165 response.should have_tag "question", 1
114 response.should have_tag "question id", @question.id.to_s 166 response.should have_tag "question id", @question.id.to_s
@@ -121,7 +173,7 @@ describe &quot;Questions&quot; do @@ -121,7 +173,7 @@ describe &quot;Questions&quot; do
121 :with_prompt => true, 173 :with_prompt => true,
122 :with_appearance => true, 174 :with_appearance => true,
123 :with_visitor_stats => true } 175 :with_visitor_stats => true }
124 - get_auth question_path(@question, :format => 'xml'), params 176 + get_auth question_path(@question), params
125 response.should be_success 177 response.should be_success
126 response.should have_tag "question", 1 178 response.should have_tag "question", 1
127 response.should have_tag "question id", @question.id.to_s 179 response.should have_tag "question id", @question.id.to_s
@@ -134,22 +186,15 @@ describe &quot;Questions&quot; do @@ -134,22 +186,15 @@ describe &quot;Questions&quot; do
134 it "should fail if 'with_prompt' is set but 'visitor_identifier' not provided" do 186 it "should fail if 'with_prompt' is set but 'visitor_identifier' not provided" do
135 pending("figure out argument dependencies") do 187 pending("figure out argument dependencies") do
136 params = { :with_prompt => true } 188 params = { :with_prompt => true }
137 - get_auth question_path(@question, :format => 'xml'), params 189 + get_auth question_path(@question), params
138 response.should_not be_success 190 response.should_not be_success
139 end 191 end
140 end 192 end
141 193
142 - context "GET 'show' trying to view others sites' questions" do  
143 - before do  
144 - @orig_user = @api_user  
145 - @api_user = Factory(:email_confirmed_user)  
146 - end  
147 -  
148 - it "should fail" do  
149 - get_auth question_path(@question, :format => 'xml')  
150 - response.should_not be_success  
151 - end  
152 - after { @api_user = @orig_user } 194 + it "should fail when trying to view other sites' questions" do
  195 + other_user = Factory(:email_confirmed_user)
  196 + get_auth other_user, question_path(@question)
  197 + response.should_not be_success
153 end 198 end
154 end 199 end
155 200
@@ -163,38 +208,32 @@ describe &quot;Questions&quot; do @@ -163,38 +208,32 @@ describe &quot;Questions&quot; do
163 :information => "foo", 208 :information => "foo",
164 :name => "bar", 209 :name => "bar",
165 :local_identifier => "baz" } } 210 :local_identifier => "baz" } }
166 - put_auth question_path(@question, :format => 'xml'), params 211 + put_auth question_path(@question), params
167 response.should be_success 212 response.should be_success
168 end 213 end
169 214
170 it "should not be able to change the site id" do 215 it "should not be able to change the site id" do
171 original_site_id = @question.site_id 216 original_site_id = @question.site_id
172 params = { :question => { :site_id => -1 } } 217 params = { :question => { :site_id => -1 } }
173 - put_auth question_path(@question, :format => 'xml'), params 218 + put_auth question_path(@question), params
174 @question.reload.site_id.should == original_site_id 219 @question.reload.site_id.should == original_site_id
175 end 220 end
176 221
177 it "should ignore protected attributes" do 222 it "should ignore protected attributes" do
178 params = { :question => { :votes_count => 999 } } 223 params = { :question => { :votes_count => 999 } }
179 - put_auth question_path(@question, :format => 'xml'), params 224 + put_auth question_path(@question), params
180 response.should be_success 225 response.should be_success
181 - @question.reload.site_id.should_not == 999 226 + @question.reload.votes_count.should_not == 999
182 end 227 end
183 228
184 - context "when updatng another site's question" do  
185 - before do  
186 - @orig_user = @api_user  
187 - @api_user = Factory(:email_confirmed_user)  
188 - end 229 + it "should fail when updating another site's question" do
  230 + other_user = Factory(:email_confirmed_user)
  231 + params = { :question => { :name => "foo" } }
  232 + put_auth other_user, question_path(@question), params
  233 + response.should_not be_success
  234 + end
189 235
190 - it "should fail" do  
191 - params = { :question => { :name => "foo" } }  
192 - put_auth question_path(@question, :format => 'xml'), params  
193 - response.should_not be_success  
194 - end  
195 236
196 - after { @api_user = @orig_user }  
197 - end  
198 end 237 end
199 238
200 describe "GET 'all_object_info_totals_by_date'" do 239 describe "GET 'all_object_info_totals_by_date'" do
spec/integration/visitors_spec.rb
@@ -2,7 +2,200 @@ require File.expand_path(File.dirname(__FILE__) + &#39;/../spec_helper&#39;) @@ -2,7 +2,200 @@ require File.expand_path(File.dirname(__FILE__) + &#39;/../spec_helper&#39;)
2 2
3 describe "Visitors" do 3 describe "Visitors" do
4 include IntegrationSupport 4 include IntegrationSupport
  5 +
  6 + before do
  7 + @user = self.default_user = Factory(:email_confirmed_user)
  8 + @visitors = @user.visitors << Array.new(30){ Factory(:visitor, :site => @user) }
  9 + @questions = Array.new(3){ Factory(:aoi_question, :site => @user, :creator => @visitors.rand) }
  10 + end
  11 +
  12 + describe "GET 'index'" do
  13 + it "should return an array of visitors" do
  14 + get_auth visitors_path
  15 + response.should be_success
  16 + response.body.should have_tag("visitors visitor", @visitors.size)
  17 + end
  18 +
  19 + it "should not return other sites' visitors" do
  20 + other_user = Factory(:email_confirmed_user)
  21 + other_visitors = other_user.visitors << Array.new(10) do
  22 + Factory(:visitor, :site => other_user)
  23 + end
  24 + get_auth other_user, visitors_path
  25 +
  26 + response.should be_success
  27 + response.body.should have_tag("visitors visitor", other_visitors.size)
  28 + end
  29 +
  30 + it "should return the number of votes for each visitor" do
  31 + counts = Hash.new(0)
  32 + 20.times do
  33 + visitor = @visitors.rand
  34 + Factory(:vote, :question => @questions.rand, :voter => visitor)
  35 + counts[visitor.id] += 1
  36 + end
  37 + get_auth visitors_path, :votes_count => true
  38 +
  39 + response.should be_success
  40 + response.should have_tag "visitor", counts.size do |nodes|
  41 + nodes.each do |node|
  42 + id = node.content("id").to_i
  43 + node.should have_tag("id"), :text => id
  44 + node.should have_tag("votes-count"), :text => counts[id]
  45 + end
  46 + end
  47 + end
  48 +
  49 + it "should return the number of skips for each visitor" do
  50 + counts = Hash.new(0)
  51 + 20.times do
  52 + visitor = @visitors.rand
  53 + Factory(:skip, :question => @questions.rand, :skipper => visitor)
  54 + counts[visitor.id] += 1
  55 + end
  56 + get_auth visitors_path, :skips_count => true
  57 +
  58 + response.should be_success
  59 + response.should have_tag "visitor", counts.size do |nodes|
  60 + nodes.each do |node|
  61 + id = node.content("id").to_i
  62 + node.should have_tag("id"), :text => id
  63 + node.should have_tag("skips-count"), :text => counts[id]
  64 + end
  65 + end
  66 + end
  67 +
  68 + it "should return the number of user-submitted choices" do
  69 + 10.times do
  70 + question = @questions.rand
  71 + creator = question.creator
  72 + Factory(:choice, :question => question, :creator => creator)
  73 + end
  74 + counts = Hash.new(0)
  75 + 10.times do
  76 + question = @questions.rand
  77 + creator = (@visitors - [question.creator]).rand
  78 + counts[creator.id] += 1
  79 + Factory(:choice, :question => question, :creator => creator)
  80 + end
  81 + get_auth visitors_path :ideas_count => true
  82 +
  83 + response.should be_success
  84 + response.should have_tag "visitor", counts.size do |nodes|
  85 + nodes.each do |node|
  86 + id = node.content("id").to_i
  87 + node.should have_tag("id"), :text => id
  88 + node.should have_tag("ideas-count"), :text => counts[id]
  89 + end
  90 + end
  91 + end
  92 +
  93 + it "should show which visitors are bounces" do
  94 + bounce = {}
  95 + @visitors.each do |v|
  96 + if [true,false].rand
  97 + Factory(:appearance, :question => @questions.rand, :voter => v)
  98 + bounce[v.id] = 1
  99 + else
  100 + vote = Factory(:vote, :question => @questions.rand, :voter => v)
  101 + Factory(:appearance, :question => @questions.rand,
  102 + :voter => v, :answerable => vote)
  103 + end
  104 + end
  105 + get_auth visitors_path, :bounces => true
  106 +
  107 + response.should be_success
  108 + response.should have_tag "visitor", bounce.size do |nodes|
  109 + nodes.each do |node|
  110 + id = node.content("id").to_i
  111 + node.should have_tag "id", :text => id
  112 + node.should have_tag "bounces", :text => 1
  113 + end
  114 + end
  115 + end
  116 +
  117 + it "should return the number of questions created for each visitor" do
  118 + count = @visitors.inject({}) do |h,v|
  119 + n = @questions.select{ |q| q.creator == v }.size
  120 + h[v.id] = n unless n.zero?
  121 + h
  122 + end
  123 + get_auth visitors_path, :questions_created => true
  124 +
  125 + response.should be_success
  126 + response.should have_tag "visitor", count.size do |nodes|
  127 + nodes.each do |node|
  128 + id = node.content("id").to_i
  129 + node.should have_tag "id", :text => id
  130 + node.should have_tag "questions-created", :text => count[id]
  131 + end
  132 + end
  133 + end
  134 +
  135 + it "should return the visitor counts for a single question" do
  136 + votes, skips, choices = Array.new(3){ Hash.new(0) }
  137 + the_question = @questions.rand
  138 + 20.times do
  139 + question = @questions.rand
  140 + visitor = (@visitors - [question.creator]).rand
  141 + case rand(3)
  142 + when 0 then
  143 + Factory(:vote, :question => question, :voter => visitor)
  144 + votes[visitor.id] += 1 if question == the_question
  145 + when 1 then
  146 + Factory(:skip, :question => question, :skipper => visitor)
  147 + skips[visitor.id] += 1 if question == the_question
  148 + when 2 then
  149 + Factory(:choice, :question => question, :creator => visitor)
  150 + choices[visitor.id] += 1 if question == the_question
  151 + end
  152 + end
  153 + visitors = (votes.keys | skips.keys | choices.keys)
  154 +
  155 + get_auth visitors_path, {
  156 + :votes_count => true,
  157 + :skips_count => true,
  158 + :ideas_count => true,
  159 + :question_id => the_question.id
  160 + }
  161 +
  162 + response.should be_success
  163 + response.should have_tag "visitor", visitors.size do |nodes|
  164 + nodes.each do |node|
  165 + id = node.content("id").to_i
  166 + node.should have_tag "id", :text => id
  167 + node.should have_tag "votes-count", :text => votes[id]
  168 + node.should have_tag "skips-count", :text => skips[id]
  169 + node.should have_tag "ideas-count", :text => choices[id]
  170 + end
  171 + end
  172 + end
  173 +
  174 + it "should return the bounces for a single question" do
  175 + the_question = @questions.rand
  176 + bounces = @visitors.inject({}) do |h,v|
  177 + if v.id.odd? # bounce!
  178 + question = @questions.rand
  179 + Factory(:appearance, :question => question, :voter => v)
  180 + h[v.id] = 1 if question == the_question
  181 + else # appearance w/ answerable
  182 + vote = Factory(:vote, :question => @questions.rand, :voter => v)
  183 + Factory(:appearance, :question => @questions.rand, :voter => v, :answerable => vote)
  184 + end
  185 + h
  186 + end
  187 +
  188 + get_auth visitors_path, :bounces => true, :question_id => the_question.id
  189 + response.should be_success
  190 + response.should have_tag "visitor", bounces.size do |nodes|
  191 + nodes.each do |node|
  192 + id = node.content("id").to_i
  193 + node.should have_tag "id", :text => id
  194 + node.should have_tag "bounces", :text => bounces[id]
  195 + end
  196 + end
  197 + end
  198 + end
5 199
6 - describe "index"  
7 - 200 +
8 end 201 end
spec/support/integration_support.rb
1 module IntegrationSupport 1 module IntegrationSupport
2 2
  3 + @@default_user = nil
  4 +
3 # todo: make automatically included in integration tests 5 # todo: make automatically included in integration tests
4 Spec::Runner.configure do |config| 6 Spec::Runner.configure do |config|
5 config.before(:each, :type => :integration) do 7 config.before(:each, :type => :integration) do
6 - @api_user = Factory(:email_confirmed_user) 8 + # compatibility with old tests using @api_user, remove this
  9 + @api_user = self.default_user = Factory(:email_confirmed_user)
7 end 10 end
8 end 11 end
9 12
10 - def get_auth(path, parameters = {}, headers = {})  
11 - auth_wrap(:get, path, parameters, headers) 13 + def default_user=(user)
  14 + @api_user = @@default_user = user
12 end 15 end
13 16
14 - def put_auth(path, parameters = {}, headers = {})  
15 - auth_wrap(:put, path, parameters, headers) 17 + # generate _auth variation of get/put/post, etc. to automatically
  18 + # send requests with the authentication and accept headers set
  19 + %w(get put post delete head).each do |method|
  20 + define_method(method + "_auth") do |*args|
  21 + if args[0].is_a? User
  22 + user, path, parameters, headers, *ignored = *args
  23 + else
  24 + path, parameters, headers, *ignored = *args
  25 + end
  26 +
  27 + user ||= @@default_user
  28 + raise ArgumentError, "No user provided and default user not set" unless user
  29 +
  30 + auth = ActionController::HttpAuthentication::
  31 + Basic.encode_credentials(user.email, user.password)
  32 + (headers ||= {}).merge!( :authorization => auth,
  33 + :accept => "application/xml" )
  34 +
  35 + send(method, path, parameters, headers)
  36 + end
16 end 37 end
17 38
18 - def post_auth(path, parameters = {}, headers = {} )  
19 - auth_wrap(:post, path, parameters, headers) 39 + # need a way to easily fetch content of a Tag
  40 + class HTML::Tag
  41 + def content(tag)
  42 + n = self.find(:tag => tag) or return nil
  43 + n.children.each{ |c| return c.content if c.is_a? HTML::Text }
  44 + nil
  45 + end
20 end 46 end
21 47
22 - def delete_auth(path, parameters = {}, headers = {})  
23 - auth_wrap(:delete, path, parameters, headers)  
24 - end  
25 48
26 - def head_auth(path, parameters = {}, headers = {})  
27 - auth_wrap(:head, path, parameters, headers)  
28 - end  
29 -  
30 - private  
31 - def auth_wrap(method, path, parameters, headers)  
32 - return nil unless [:get, :put, :post, :delete, :head].include? method  
33 -  
34 - auth = ActionController::HttpAuthentication::Basic.encode_credentials(@api_user.email, @api_user.password)  
35 - headers.merge!(:authorization => auth)  
36 - # headers.merge!(:content_type => "application/xml", :authorization => auth)  
37 - # parameters.merge!(:format => 'xml') 49 + # have_tag doesn't let you iterate over individual nodes like
  50 + # assert_select does for some reason, and using css matchers
  51 + # to do this is ugly. Time for a patch!
  52 + class Spec::Rails::Matchers::AssertSelect
  53 + def doc_from_with_node(node)
  54 + return node if node.is_a? HTML::Node
  55 + doc_from_without_node(node)
  56 + end
38 57
39 - send(method, path, parameters, headers) 58 + alias_method_chain :doc_from, :node
40 end 59 end
  60 +
  61 +
41 end 62 end
42 63