implemented first version for unique check on controller level.

This commit is contained in:
mio 2011-11-07 20:41:51 +01:00
parent 24d0c46080
commit 09bfa2f27e
9 changed files with 327 additions and 135 deletions

View File

@ -3,10 +3,10 @@ require 'digest/sha2'
module ImpressionistController module ImpressionistController
module ClassMethods module ClassMethods
def impressionist(opts={}) def impressionist(opts={})
before_filter { |c| c.impressionist_subapp_filter opts[:actions] } before_filter { |c| c.impressionist_subapp_filter(opts[:actions], opts[:unique])}
end end
end end
module InstanceMethods module InstanceMethods
def self.included(base) def self.included(base)
base.before_filter :impressionist_app_filter base.before_filter :impressionist_app_filter
@ -15,14 +15,7 @@ module ImpressionistController
def impressionist(obj,message=nil) def impressionist(obj,message=nil)
unless bypass unless bypass
if obj.respond_to?("impressionable?") if obj.respond_to?("impressionable?")
obj.impressions.create(:message=> message, obj.impressions.create(create_statement({:message => message}))
:request_hash=> @impressionist_hash,
:session_hash=> request.session_options[:id],
:ip_address=> request.remote_ip,
:user_id=> user_id,
:controller_name=>controller_name,
:action_name=> action_name,
:referrer=>request.referer)
else else
raise "#{obj.class.to_s} is not impressionable!" raise "#{obj.class.to_s} is not impressionable!"
end end
@ -33,31 +26,76 @@ module ImpressionistController
@impressionist_hash = Digest::SHA2.hexdigest(Time.now.to_f.to_s+rand(10000).to_s) @impressionist_hash = Digest::SHA2.hexdigest(Time.now.to_f.to_s+rand(10000).to_s)
end end
def impressionist_subapp_filter(actions=nil) def impressionist_subapp_filter(actions=nil, unique_opts=nil)
unless bypass unless bypass
actions.collect!{|a|a.to_s} unless actions.blank? actions.collect!{|a|a.to_s} unless actions.blank?
if actions.blank? or actions.include?(action_name) if (actions.blank? || actions.include?(action_name)) && (unique_opts.blank? || is_unique(unique_opts))
Impression.create(:controller_name=> controller_name, if (!actions.blank? && !unique_opts.blank?)
:action_name=> action_name, logger.info "Restricted to actions #{actions.inspect} and uniqueness for #{unique_opts.inspect}"
:user_id=> user_id, end
:request_hash=> @impressionist_hash, Impression.create(create_statement(
:session_hash=> request.session_options[:id], :impressionable_type => controller_name.singularize.camelize,
:ip_address=> request.remote_ip, :impressionable_id=> params[:id]
:impressionable_type=> controller_name.singularize.camelize, ))
:impressionable_id=> params[:id],
:referrer=>request.referer)
end end
end end
end end
private private
def bypass def bypass
Impressionist::Bots::WILD_CARDS.each do |wild_card| Impressionist::Bots::WILD_CARDS.each do |wild_card|
return true if request.user_agent and request.user_agent.downcase.include? wild_card return true if request.user_agent and request.user_agent.downcase.include? wild_card
end end
Impressionist::Bots::LIST.include? request.user_agent Impressionist::Bots::LIST.include? request.user_agent
end end
def is_unique(unique_opts)
# FIXME think about uniqueness in relation to impressionable_id, impressionable_type and controller_name
# is controller name redundant? does the controller name always have to match?
default_statement = create_statement(
:impressionable_type => controller_name.singularize.camelize,
:impressionable_id=> params[:id]
)
statement = unique_opts.reduce({}) do |query, param|
query[param] = default_statement[param]
query
end
#logger.debug "Statement params: #{statement.inspect}."
# always use impressionable type?
statement[:impressionable_type] = controller_name.singularize.camelize
#statement[:impressionable_id] = params[:id]
return Impression.where(statement).size == 0
end
# creates a statment hash that contains default values for creating an impression (without
# :impressionable_type and impressionable_id as they are not needed for creating via association).
def create_statement(query_params={})
query_params.reverse_merge!(
:controller_name => controller_name,
:action_name => action_name,
:user_id => user_id,
:request_hash => @impressionist_hash,
:session_hash => session_hash,
:ip_address => remote_ip,
:referrer => request.referer
)
end
def session_hash
# # careful: request.session_options[:id] encoding in rspec test was ASCII-8BIT
# # that broke the database query for uniqueness. not sure how to solve this issue
# # seems to depend on app setup/config
# str = request.session_options[:id]
# # probably this isn't a fix: request.session_options[:id].encode("ISO-8859-1")
# logger.debug "Encoding: #{str.encoding.inspect}"
request.session_options[:id]
end
def remote_ip
request.remote_ip
end
#use both @current_user and current_user helper #use both @current_user and current_user helper
def user_id def user_id
user_id = @current_user ? @current_user.id : nil rescue nil user_id = @current_user ? @current_user.id : nil rescue nil

View File

@ -1,9 +1,9 @@
source 'http://rubygems.org' source 'http://rubygems.org'
gem 'rails', '3.1.0.rc1' gem 'rails', '3.1'
gem 'sqlite3-ruby', :require => 'sqlite3' gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'impressionist', :path=>"#{File.dirname(__FILE__)}/../" gem 'impressionist', :path=>"#{File.dirname(__FILE__)}/../"
gem "pg" #gem "pg"
group :development do group :development do
gem 'ZenTest' gem 'ZenTest'

View File

@ -1,155 +1,156 @@
PATH PATH
remote: /rails_plugins/mine/impressionist remote: /home/mio/prog/projects/impressionist
specs: specs:
impressionist (0.3.2) impressionist (0.4.0)
GEM GEM
remote: http://rubygems.org/ remote: http://rubygems.org/
specs: specs:
ZenTest (4.5.0) ZenTest (4.6.2)
actionmailer (3.1.0.rc1) actionmailer (3.1.0)
actionpack (= 3.1.0.rc1) actionpack (= 3.1.0)
mail (~> 2.3.0) mail (~> 2.3.0)
actionpack (3.1.0.rc1) actionpack (3.1.0)
activemodel (= 3.1.0.rc1) activemodel (= 3.1.0)
activesupport (= 3.1.0.rc1) activesupport (= 3.1.0)
builder (~> 3.0.0) builder (~> 3.0.0)
erubis (~> 2.7.0) erubis (~> 2.7.0)
i18n (~> 0.6.0beta1) i18n (~> 0.6)
rack (~> 1.3.0.beta2) rack (~> 1.3.2)
rack-cache (~> 1.0.1) rack-cache (~> 1.0.3)
rack-mount (~> 0.8.1) rack-mount (~> 0.8.2)
rack-test (~> 0.6.0) rack-test (~> 0.6.1)
sprockets (~> 2.0.0.beta.5) sprockets (~> 2.0.0)
tzinfo (~> 0.3.27) activemodel (3.1.0)
activemodel (3.1.0.rc1) activesupport (= 3.1.0)
activesupport (= 3.1.0.rc1) bcrypt-ruby (~> 3.0.0)
bcrypt-ruby (~> 2.1.4)
builder (~> 3.0.0) builder (~> 3.0.0)
i18n (~> 0.6.0beta1) i18n (~> 0.6)
activerecord (3.1.0.rc1) activerecord (3.1.0)
activemodel (= 3.1.0.rc1) activemodel (= 3.1.0)
activesupport (= 3.1.0.rc1) activesupport (= 3.1.0)
arel (~> 2.1.1) arel (~> 2.2.1)
tzinfo (~> 0.3.27) tzinfo (~> 0.3.29)
activeresource (3.1.0.rc1) activeresource (3.1.0)
activemodel (= 3.1.0.rc1) activemodel (= 3.1.0)
activesupport (= 3.1.0.rc1) activesupport (= 3.1.0)
activesupport (3.1.0.rc1) activesupport (3.1.0)
multi_json (~> 1.0) multi_json (~> 1.0)
arel (2.1.1) addressable (2.2.6)
arel (2.2.1)
autotest (4.4.6) autotest (4.4.6)
ZenTest (>= 4.4.1) ZenTest (>= 4.4.1)
autotest-notification (2.3.1) autotest-notification (2.3.3)
autotest (~> 4.3) autotest-standalone (~> 4.5)
bcrypt-ruby (2.1.4) autotest-standalone (4.5.8)
bcrypt-ruby (3.0.1)
builder (3.0.0) builder (3.0.0)
capybara (1.0.0.beta1) capybara (1.1.1)
mime-types (>= 1.16) mime-types (>= 1.16)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
selenium-webdriver (>= 0.0.27) selenium-webdriver (~> 2.0)
xpath (~> 0.1.4) xpath (~> 0.1.4)
childprocess (0.1.9) childprocess (0.2.2)
ffi (~> 1.0.6) ffi (~> 1.0.6)
configuration (1.2.0) cucumber (1.1.1)
cucumber (0.10.3)
builder (>= 2.1.2) builder (>= 2.1.2)
diff-lcs (>= 1.1.2) diff-lcs (>= 1.1.2)
gherkin (>= 2.3.8) gherkin (~> 2.6.0)
json (>= 1.4.6) json (>= 1.4.6)
term-ansicolor (>= 1.0.5) term-ansicolor (>= 1.0.6)
cucumber-rails (0.5.1) cucumber-rails (1.2.0)
capybara (>= 1.0.0.beta1) capybara (>= 1.1.1)
cucumber (>= 0.10.3) cucumber (>= 1.1.1)
nokogiri (>= 1.4.4) nokogiri (>= 1.5.0)
rack-test (>= 0.5.7)
daemons (1.0.10) daemons (1.0.10)
database_cleaner (0.6.7) database_cleaner (0.6.7)
diff-lcs (1.1.2) diff-lcs (1.1.3)
erubis (2.7.0) erubis (2.7.0)
ffi (1.0.9) ffi (1.0.9)
gem_plugin (0.2.3) gem_plugin (0.2.3)
gherkin (2.3.10) gherkin (2.6.2)
json (>= 1.4.6) json (>= 1.4.6)
hike (1.0.0) hike (1.2.1)
i18n (0.6.0) i18n (0.6.0)
json (1.5.1) json (1.6.1)
json_pure (1.5.1) json_pure (1.6.1)
launchy (0.4.0) launchy (2.0.5)
configuration (>= 0.0.5) addressable (~> 2.2.6)
rake (>= 0.8.1)
mail (2.3.0) mail (2.3.0)
i18n (>= 0.4.0) i18n (>= 0.4.0)
mime-types (~> 1.16) mime-types (~> 1.16)
treetop (~> 1.4.8) treetop (~> 1.4.8)
mime-types (1.16) mime-types (1.17.2)
mongrel (1.2.0.pre2) mongrel (1.2.0.pre2)
daemons (~> 1.0.10) daemons (~> 1.0.10)
gem_plugin (~> 0.2.3) gem_plugin (~> 0.2.3)
multi_json (1.0.3) multi_json (1.0.3)
nokogiri (1.4.4) nokogiri (1.5.0)
pg (0.11.0) polyglot (0.3.3)
polyglot (0.3.1) rack (1.3.5)
rack (1.3.0) rack-cache (1.0.3)
rack-cache (1.0.2)
rack (>= 0.4) rack (>= 0.4)
rack-mount (0.8.1) rack-mount (0.8.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-ssl (1.3.2) rack-ssl (1.3.2)
rack rack
rack-test (0.6.0) rack-test (0.6.1)
rack (>= 1.0) rack (>= 1.0)
rails (3.1.0.rc1) rails (3.1.0)
actionmailer (= 3.1.0.rc1) actionmailer (= 3.1.0)
actionpack (= 3.1.0.rc1) actionpack (= 3.1.0)
activerecord (= 3.1.0.rc1) activerecord (= 3.1.0)
activeresource (= 3.1.0.rc1) activeresource (= 3.1.0)
activesupport (= 3.1.0.rc1) activesupport (= 3.1.0)
bundler (~> 1.0) bundler (~> 1.0)
railties (= 3.1.0.rc1) railties (= 3.1.0)
railties (3.1.0.rc1) railties (3.1.0)
actionpack (= 3.1.0.rc1) actionpack (= 3.1.0)
activesupport (= 3.1.0.rc1) activesupport (= 3.1.0)
rack-ssl (~> 1.3.2) rack-ssl (~> 1.3.2)
rake (>= 0.8.7) rake (>= 0.8.7)
rdoc (~> 3.4)
thor (~> 0.14.6) thor (~> 0.14.6)
rake (0.9.1) rake (0.9.2.2)
rspec (2.6.0) rdoc (3.11)
rspec-core (~> 2.6.0) json (~> 1.4)
rspec-expectations (~> 2.6.0) rspec (2.7.0)
rspec-mocks (~> 2.6.0) rspec-core (~> 2.7.0)
rspec-core (2.6.3) rspec-expectations (~> 2.7.0)
rspec-expectations (2.6.0) rspec-mocks (~> 2.7.0)
rspec-core (2.7.1)
rspec-expectations (2.7.0)
diff-lcs (~> 1.1.2) diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0) rspec-mocks (2.7.0)
rspec-rails (2.6.1) rspec-rails (2.7.0)
actionpack (~> 3.0) actionpack (~> 3.0)
activesupport (~> 3.0) activesupport (~> 3.0)
railties (~> 3.0) railties (~> 3.0)
rspec (~> 2.6.0) rspec (~> 2.7.0)
rubyzip (0.9.4) rubyzip (0.9.4)
selenium-webdriver (0.2.1) selenium-webdriver (2.10.0)
childprocess (>= 0.1.7) childprocess (>= 0.2.1)
ffi (>= 1.0.7) ffi (= 1.0.9)
json_pure json_pure
rubyzip rubyzip
spork (0.8.5) spork (0.8.5)
sprockets (2.0.0.beta.9) sprockets (2.0.3)
hike (~> 1.0) hike (~> 1.2)
rack (~> 1.0) rack (~> 1.0)
tilt (~> 1.1, != 1.3.0) tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.3) sqlite3 (1.3.4)
sqlite3-ruby (1.3.3) sqlite3-ruby (1.3.3)
sqlite3 (>= 1.3.3) sqlite3 (>= 1.3.3)
systemu (2.2.0) systemu (2.4.1)
term-ansicolor (1.0.5) term-ansicolor (1.0.7)
thor (0.14.6) thor (0.14.6)
tilt (1.3.2) tilt (1.3.3)
treetop (1.4.9) treetop (1.4.10)
polyglot
polyglot (>= 0.3.1) polyglot (>= 0.3.1)
tzinfo (0.3.27) tzinfo (0.3.31)
xpath (0.1.4) xpath (0.1.4)
nokogiri (~> 1.3) nokogiri (~> 1.3)
@ -167,8 +168,7 @@ DEPENDENCIES
impressionist! impressionist!
launchy launchy
mongrel (= 1.2.0.pre2) mongrel (= 1.2.0.pre2)
pg rails (= 3.1)
rails (= 3.1.0.rc1)
rspec rspec
rspec-rails rspec-rails
spork spork

View File

@ -1,5 +1,5 @@
class WidgetsController < ApplicationController class WidgetsController < ApplicationController
impressionist :actions=>[:show,:index] impressionist :actions=>[:show,:index], :unique => [:action_name, :impressionable_id]
def show def show
end end
@ -8,4 +8,5 @@ class WidgetsController < ApplicationController
def new def new
end end
end end

View File

@ -6,28 +6,19 @@ development:
pool: 5 pool: 5
timeout: 5000 timeout: 5000
# Warning: The database defined as "test" will be erased and test: &test
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
#test: &test
# adapter: sqlite3
# database: db/test.sqlite3
# pool: 5
# timeout: 5000
test:
adapter: sqlite3 adapter: sqlite3
database: db/test.sqlite3 database: db/test.sqlite3
pool: 5 pool: 5
timeout: 5000 timeout: 5000
pg_test: #pg_test:
adapter: postgresql # adapter: postgresql
database: impressionist_test # database: impressionist_test
username: johnmcaliley # username: johnmcaliley
password: # password:
host: localhost # host: localhost
encoding: UTF8 # encoding: UTF8
production: production:
adapter: sqlite3 adapter: sqlite3

View File

@ -78,6 +78,28 @@ describe WidgetsController do
Impression.all.size.should eq 13 Impression.all.size.should eq 13
end end
it "should log unique impressions at the per action" do
get "show", :id=> 1
Impression.all.size.should eq 12
get "show", :id=> 2
Impression.all.size.should eq 13
get "show", :id => 2
Impression.all.size.should eq 13
get "index"
Impression.all.size.should eq 14
end
it "should log unique impressions only once per id" do
get "show", :id=> 1
Impression.all.size.should eq 12
get "show", :id=> 2
Impression.all.size.should eq 13
get "show", :id => 2
Impression.all.size.should eq 13
get "index"
Impression.all.size.should eq 14
end
it "should not log impression when user-agent is in wildcard list" do it "should not log impression when user-agent is in wildcard list" do
request.stub!(:user_agent).and_return('somebot') request.stub!(:user_agent).and_return('somebot')
get "show", :id=> 1 get "show", :id=> 1

View File

@ -0,0 +1,132 @@
require 'spec_helper.rb'
# we use the posts controller as it uses the impressionsist module. any such controller would do.
describe PostsController do
before do
@impression_count = Impression.all.size
end
it "should ignore uniqueness if not requested" do
controller.impressionist_subapp_filter(nil, nil)
controller.impressionist_subapp_filter(nil, nil)
Impression.should have(@impression_count + 2).records
end
it "should recognize session uniqueness" do
# the following line was necessary as session hash returned a binary string (ASCII-8BIT encoded)
# not sure how to 'fix' this. setup/config issue?
controller.stub!(:session_hash).and_return(request.session_options[:id].encode("ISO-8859-1"))
controller.impressionist_subapp_filter(nil, [:session_hash])
controller.impressionist_subapp_filter(nil, [:session_hash])
Impression.should have(@impression_count + 1).records
end
it "should recognize ip uniqueness" do
controller.stub!(:action_name).and_return("test_action")
controller.impressionist_subapp_filter(nil, [:ip_address])
controller.impressionist_subapp_filter(nil, [:ip_address])
Impression.should have(@impression_count + 1).records
end
it "should recognize request uniqueness" do
controller.impressionist_subapp_filter(nil, [:request_hash])
controller.impressionist_subapp_filter(nil, [:request_hash])
Impression.should have(@impression_count + 1).records
end
it "should recognize action uniqueness" do
controller.stub!(:action_name).and_return("test_action")
controller.impressionist_subapp_filter(nil, [:action_name])
controller.impressionist_subapp_filter(nil, [:action_name])
Impression.should have(@impression_count + 1).records
end
it "should recognize controller uniqueness" do
controller.stub!(:controller_name).and_return("test_controller")
controller.impressionist_subapp_filter(nil, [:controller_name])
controller.impressionist_subapp_filter(nil, [:controller_name])
Impression.should have(@impression_count + 1).records
end
it "should recognize user uniqueness" do
controller.stub!(:user_id).and_return(1)
controller.impressionist_subapp_filter(nil, [:user_id])
controller.impressionist_subapp_filter(nil, [:user_id])
Impression.should have(@impression_count + 1).records
end
it "should recognize referrer uniqueness" do
controller.stub!(:referrer).and_return("http://somehost.someurl.somdomain/some/path")
controller.impressionist_subapp_filter(nil, [:referrer])
controller.impressionist_subapp_filter(nil, [:referrer])
Impression.should have(@impression_count + 1).records
end
# extra redundant test for important controller and action combination.
it "should recognize difference in controller and action" do
controller.stub!(:controller_name).and_return("test_controller")
controller.stub!(:action_name).and_return("test_action")
controller.impressionist_subapp_filter(nil, [:controller_name, :action_name])
Impression.should have(@impression_count + 1).records
controller.stub!(:action_name).and_return("another_action")
controller.impressionist_subapp_filter(nil, [:controller_name, :action_name])
Impression.should have(@impression_count + 2).records
controller.stub!(:controller_name).and_return("another_controller")
controller.impressionist_subapp_filter(nil, [:controller_name, :action_name])
Impression.should have(@impression_count + 3).records
end
it "should recognize difference in action" do
controller.stub!(:action_name).and_return("test_action")
controller.impressionist_subapp_filter(nil, [:action_name])
Impression.should have(@impression_count + 1).records
controller.stub!(:action_name).and_return("another_action")
controller.impressionist_subapp_filter(nil, [:action_name])
Impression.should have(@impression_count + 2).records
end
it "should recognize difference in controller" do
controller.stub!(:controller_name).and_return("test_controller")
controller.impressionist_subapp_filter(nil, [:controller_name])
Impression.should have(@impression_count + 1).records
controller.stub!(:controller_name).and_return("another_controller")
controller.impressionist_subapp_filter(nil, [:controller_name])
Impression.should have(@impression_count + 2).records
end
it "should recognize difference in session" do
controller.stub!(:session_hash).and_return(request.session_options[:id].encode("ISO-8859-1"))
controller.impressionist_subapp_filter(nil, [:session_hash])
Impression.should have(@impression_count + 1).records
controller.stub!(:session_hash).and_return("anothersessionhash")
controller.impressionist_subapp_filter(nil, [:session_hash])
Impression.should have(@impression_count + 2).records
end
it "should recognize combined uniqueness" do
controller.stub!(:action_name).and_return("test_action")
controller.impressionist_subapp_filter(nil, [:ip_address, :request_hash, :action_name])
controller.impressionist_subapp_filter(nil, [:request_hash, :ip_address, :action_name])
controller.impressionist_subapp_filter(nil, [:request_hash, :action_name])
controller.impressionist_subapp_filter(nil, [:ip_address, :action_name])
controller.impressionist_subapp_filter(nil, [:ip_address, :request_hash])
controller.impressionist_subapp_filter(nil, [:action_name])
controller.impressionist_subapp_filter(nil, [:ip_address])
controller.impressionist_subapp_filter(nil, [:request_hash])
Impression.should have(@impression_count + 1).records
end
it "should recognize combined non-uniqueness" do
controller.stub!(:action_name).and_return(nil)
controller.impressionist_subapp_filter(nil, [:ip_address, :action_name])
controller.stub!(:action_name).and_return("test_action")
controller.impressionist_subapp_filter(nil, [:ip_address, :action_name])
controller.stub!(:action_name).and_return("another_action")
controller.impressionist_subapp_filter(nil, [:ip_address, :action_name])
Impression.should have(@impression_count + 3).records
end
end

View File

@ -1,6 +1,8 @@
require 'spec_helper' require 'spec_helper'
require 'systemu' require 'systemu'
# FIXME this test might break the others if run before them
#
describe Impressionist do describe Impressionist do
fixtures :articles,:impressions,:posts fixtures :articles,:impressions,:posts
it "should delete existing migration and generate the migration file" do it "should delete existing migration and generate the migration file" do

View File

@ -24,4 +24,10 @@ RSpec.configure do |config|
# examples within a transaction, remove the following line or assign false # examples within a transaction, remove the following line or assign false
# instead of true. # instead of true.
config.use_transactional_fixtures = true config.use_transactional_fixtures = true
# make the rails logger usable in the tests as logger.xxx "..."
def logger
Rails.logger
end
end end