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 158 elsif object_type == "uploaded_ideas"
159 159  
160 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 162 :group => 'creator_id')
163 163  
164 164 count = 0
... ... @@ -307,7 +307,30 @@ class QuestionsController < InheritedResources::Base
307 307 end
308 308  
309 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 334 index!
312 335 end
313 336  
... ...
app/controllers/visitors_controller.rb
1 1 class VisitorsController < InheritedResources::Base
2 2 respond_to :xml, :json
3 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 48 def objects_by_session_ids
7 49 session_ids = params[:session_ids]
... ...
app/models/question.rb
... ... @@ -29,6 +29,10 @@ class Question &lt; ActiveRecord::Base
29 29  
30 30 attr_readonly :site_id
31 31  
  32 + named_scope :created_by, lambda { |id|
  33 + {:conditions => { :local_identifier => id } }
  34 + }
  35 +
32 36 def create_choices_from_ideas
33 37 if ideas && ideas.any?
34 38 ideas.each do |idea|
... ...
config/routes.rb
1 1 ActionController::Routing::Routes.draw do |map|
2 2 map.resources :densities, :only => :index
3   - map.resources :visitors, :only => :none,
  3 + map.resources :visitors, :only => :index,
4 4 :collection => {:objects_by_session_ids => :post},
5 5 :member => {:votes => :get}
6 6 map.resources :questions, :except => [:edit, :destroy],
... ...
spec/factories.rb
... ... @@ -58,6 +58,19 @@ Factory.define(:vote) do |f|
58 58 f.voter {|v| v.question.creator}
59 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 74 Factory.sequence :email do |n|
62 75 "user#{n}@example.com"
63 76 end
... ...
spec/integration/choices_spec.rb
... ... @@ -22,7 +22,7 @@ describe &quot;Choices&quot; do
22 22 end
23 23  
24 24 after do
25   - post_auth question_choices_path(@question, :format => 'xml'), @params
  25 + post_auth question_choices_path(@question), @params
26 26 response.should be_success
27 27 response.should have_tag "choice"
28 28 end
... ... @@ -35,7 +35,7 @@ describe &quot;Choices&quot; do
35 35 :data => "foo",
36 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 40 response.should be_success
41 41 response.should have_tag "choice creator-id", @visitor.id.to_s
... ... @@ -52,32 +52,23 @@ describe &quot;Choices&quot; do
52 52 end
53 53  
54 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 57 response.should be_success
58 58 response.should have_tag "choice active", "false"
59 59 end
60 60  
61 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 64 response.should be_success
65 65 response.should have_tag "choice active", "false"
66 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 72 end
82 73 end
83 74  
... ... @@ -89,14 +80,14 @@ describe &quot;Choices&quot; do
89 80 end
90 81  
91 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 85 response.should be_success
95 86 response.should have_tag "choices choice", 5
96 87 end
97 88  
98 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 92 response.should be_success
102 93 response.should have_tag "choices choice", 10
... ... @@ -105,31 +96,23 @@ describe &quot;Choices&quot; do
105 96  
106 97  
107 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 101 response.should be_success
111 102 response.should have_tag "choices choice", 3
112 103 end
113 104  
114 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 108 response.should be_success
118 109 response.should have_tag "choices choice", 3
119 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 116 end
134 117  
135 118 end
... ... @@ -141,23 +124,16 @@ describe &quot;Choices&quot; do
141 124 end
142 125  
143 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 129 response.should be_success
147 130 response.should have_tag "choice", 1
148 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 137 end
162 138  
163 139 end
... ... @@ -171,23 +147,15 @@ describe &quot;Choices&quot; do
171 147  
172 148 it "should succeed given valid attributes" do
173 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 151 response.should be_success
176 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 159 end
192 160 end
193 161  
... ...
spec/integration/prompts_spec.rb
... ... @@ -13,7 +13,7 @@ describe &quot;Prompts&quot; do
13 13 end
14 14  
15 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 17 response.should be_success
18 18 response.should have_tag "prompt", 1
19 19 end
... ... @@ -31,11 +31,11 @@ describe &quot;Prompts&quot; do
31 31 :with_prompt => true,
32 32 :visitor_identifier => @visitor.identifier )
33 33 @appearance_id = info[:appearance_id]
34   - @prompt_id = info[:picked_prompt_id]
  34 + @picked_prompt_id = info[:picked_prompt_id]
35 35 end
36 36  
37 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 39 response.should be_success
40 40 response.should have_tag "skip", 1
41 41 end
... ... @@ -48,7 +48,7 @@ describe &quot;Prompts&quot; do
48 48 :skip_reason => "bar",
49 49 :appearance_lookup => @appearance_id,
50 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 52 response.should be_success
53 53 response.should have_tag "skip", 1
54 54 response.should have_tag "skip appearance-id", @appearance_id.to_s
... ... @@ -65,7 +65,7 @@ describe &quot;Prompts&quot; do
65 65 :with_appearance => true,
66 66 :algorithm => "catchup",
67 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 69 response.should be_success
70 70 response.should have_tag "prompt", 1
71 71 response.should have_tag "prompt appearance_id", /.+/
... ... @@ -73,19 +73,12 @@ describe &quot;Prompts&quot; do
73 73 response.should have_tag "prompt visitor_ideas", /\d+/
74 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 80 end
  81 +
89 82 end
90 83  
91 84 describe "POST 'vote'" do
... ... @@ -101,17 +94,17 @@ describe &quot;Prompts&quot; do
101 94 :with_prompt => true,
102 95 :visitor_identifier => @visitor.identifier )
103 96 @appearance_id = info[:appearance_id]
104   - @prompt_id = info[:picked_prompt_id]
  97 + @picked_prompt_id = info[:picked_prompt_id]
105 98 end
106 99  
107 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 102 response.should_not be_success
110 103 end
111 104  
112 105 it "should return a new vote object given no optional parameters" do
113 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 108 response.should be_success
116 109 response.should have_tag "vote", 1
117 110 end
... ... @@ -124,7 +117,7 @@ describe &quot;Prompts&quot; do
124 117 :direction => "right",
125 118 :appearance_lookup => @appearance_id,
126 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 121 response.should be_success
129 122 response.should have_tag "vote", 1
130 123 response.should have_tag "vote appearance-id", @appearance_id.to_s
... ... @@ -143,7 +136,7 @@ describe &quot;Prompts&quot; do
143 136 :with_appearance => true,
144 137 :algorithm => "catchup",
145 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 140 response.should be_success
148 141 response.should have_tag "prompt", 1
149 142 response.should have_tag "prompt appearance_id", /.+/
... ... @@ -151,19 +144,11 @@ describe &quot;Prompts&quot; do
151 144 response.should have_tag "prompt visitor_ideas", /\d+/
152 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 152 end
168 153  
169 154 end
... ...
spec/integration/questions_spec.rb
... ... @@ -3,29 +3,81 @@ require File.expand_path(File.dirname(__FILE__) + &#39;/../spec_helper&#39;)
3 3 describe "Questions" do
4 4 include IntegrationSupport
5 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 11 end
8 12  
9 13 describe "GET 'index'" do
10 14 it "should return an array of questions" do
11 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 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 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 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 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 78 end
27   - after { @api_user = @orig_user }
28 79 end
  80 +
29 81 end
30 82  
31 83 describe "GET 'new'" do
... ... @@ -91,7 +143,7 @@ describe &quot;Questions&quot; do
91 143  
92 144 it "should fail given invalid parameters" do
93 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 147 response.should be_success
96 148 response.body.should =~ /Error/
97 149 end
... ... @@ -108,7 +160,7 @@ describe &quot;Questions&quot; do
108 160 before { @question = Factory.create(:aoi_question, :site => @api_user) }
109 161  
110 162 it "should succeed given no optional parameters" do
111   - get_auth question_path(@question, :format => 'xml')
  163 + get_auth question_path(@question)
112 164 response.should be_success
113 165 response.should have_tag "question", 1
114 166 response.should have_tag "question id", @question.id.to_s
... ... @@ -121,7 +173,7 @@ describe &quot;Questions&quot; do
121 173 :with_prompt => true,
122 174 :with_appearance => true,
123 175 :with_visitor_stats => true }
124   - get_auth question_path(@question, :format => 'xml'), params
  176 + get_auth question_path(@question), params
125 177 response.should be_success
126 178 response.should have_tag "question", 1
127 179 response.should have_tag "question id", @question.id.to_s
... ... @@ -134,22 +186,15 @@ describe &quot;Questions&quot; do
134 186 it "should fail if 'with_prompt' is set but 'visitor_identifier' not provided" do
135 187 pending("figure out argument dependencies") do
136 188 params = { :with_prompt => true }
137   - get_auth question_path(@question, :format => 'xml'), params
  189 + get_auth question_path(@question), params
138 190 response.should_not be_success
139 191 end
140 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 198 end
154 199 end
155 200  
... ... @@ -163,38 +208,32 @@ describe &quot;Questions&quot; do
163 208 :information => "foo",
164 209 :name => "bar",
165 210 :local_identifier => "baz" } }
166   - put_auth question_path(@question, :format => 'xml'), params
  211 + put_auth question_path(@question), params
167 212 response.should be_success
168 213 end
169 214  
170 215 it "should not be able to change the site id" do
171 216 original_site_id = @question.site_id
172 217 params = { :question => { :site_id => -1 } }
173   - put_auth question_path(@question, :format => 'xml'), params
  218 + put_auth question_path(@question), params
174 219 @question.reload.site_id.should == original_site_id
175 220 end
176 221  
177 222 it "should ignore protected attributes" do
178 223 params = { :question => { :votes_count => 999 } }
179   - put_auth question_path(@question, :format => 'xml'), params
  224 + put_auth question_path(@question), params
180 225 response.should be_success
181   - @question.reload.site_id.should_not == 999
  226 + @question.reload.votes_count.should_not == 999
182 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 237 end
199 238  
200 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 2  
3 3 describe "Visitors" do
4 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 201 end
... ...
spec/support/integration_support.rb
1 1 module IntegrationSupport
2 2  
  3 + @@default_user = nil
  4 +
3 5 # todo: make automatically included in integration tests
4 6 Spec::Runner.configure do |config|
5 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 10 end
8 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 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 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 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 59 end
  60 +
  61 +
41 62 end
42 63  
... ...