browsing_history 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +89 -0
- data/.rubocop_todo.yml +28 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +180 -0
- data/LICENSE +21 -0
- data/MIT-LICENSE +20 -0
- data/README.md +2 -0
- data/README.rdoc +3 -0
- data/Rakefile +28 -0
- data/browsing_history.gemspec +34 -0
- data/dump.rdb +1 -0
- data/lib/browsing_history/browser.rb +66 -0
- data/lib/browsing_history/configuration.rb +38 -0
- data/lib/browsing_history/error.rb +2 -0
- data/lib/browsing_history/historizable.rb +26 -0
- data/lib/browsing_history/history.rb +85 -0
- data/lib/browsing_history/storage.rb +44 -0
- data/lib/browsing_history/storages/active_record.rb +12 -0
- data/lib/browsing_history/storages/base.rb +40 -0
- data/lib/browsing_history/storages/redis.rb +113 -0
- data/lib/browsing_history/version.rb +3 -0
- data/lib/browsing_history.rb +12 -0
- data/lib/generators/active_record/migration_generator.rb +12 -0
- data/lib/generators/active_record/templates/migration.rb +15 -0
- data/lib/tasks/browsing_history_tasks.rake +4 -0
- data/spec/browsing_history/browser_spec.rb +156 -0
- data/spec/browsing_history/configration_spec.rb +16 -0
- data/spec/browsing_history/fixtures/article.rb +10 -0
- data/spec/browsing_history/fixtures/user.rb +10 -0
- data/spec/browsing_history/historizable_spec.rb +27 -0
- data/spec/browsing_history/history_spec.rb +273 -0
- data/spec/browsing_history/storage_spec.rb +45 -0
- data/spec/browsing_history/storages/base_spec.rb +35 -0
- data/spec/browsing_history/storages/redis_spec.rb +141 -0
- data/spec/browsing_history_spec.rb +4 -0
- data/spec/generator_spec.rb +34 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/test_app/README.rdoc +28 -0
- data/spec/test_app/Rakefile +6 -0
- data/spec/test_app/app/assets/images/.keep +0 -0
- data/spec/test_app/app/assets/javascripts/application.js +13 -0
- data/spec/test_app/app/assets/stylesheets/application.css +15 -0
- data/spec/test_app/app/controllers/application_controller.rb +10 -0
- data/spec/test_app/app/controllers/articles_controller.rb +17 -0
- data/spec/test_app/app/controllers/concerns/.keep +0 -0
- data/spec/test_app/app/helpers/application_helper.rb +2 -0
- data/spec/test_app/app/mailers/.keep +0 -0
- data/spec/test_app/app/models/.keep +0 -0
- data/spec/test_app/app/models/concerns/.keep +0 -0
- data/spec/test_app/app/views/layouts/application.html.erb +14 -0
- data/spec/test_app/bin/bundle +3 -0
- data/spec/test_app/bin/rails +4 -0
- data/spec/test_app/bin/rake +4 -0
- data/spec/test_app/bin/setup +29 -0
- data/spec/test_app/config/application.rb +19 -0
- data/spec/test_app/config/boot.rb +5 -0
- data/spec/test_app/config/database.yml +12 -0
- data/spec/test_app/config/environment.rb +5 -0
- data/spec/test_app/config/environments/development.rb +18 -0
- data/spec/test_app/config/environments/test.rb +21 -0
- data/spec/test_app/config/initializers/assets.rb +11 -0
- data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
- data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/test_app/config/initializers/inflections.rb +16 -0
- data/spec/test_app/config/initializers/mime_types.rb +4 -0
- data/spec/test_app/config/initializers/session_store.rb +3 -0
- data/spec/test_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/test_app/config/locales/en.yml +23 -0
- data/spec/test_app/config/routes.rb +2 -0
- data/spec/test_app/config/secrets.yml +5 -0
- data/spec/test_app/config.ru +4 -0
- data/spec/test_app/db/development.sqlite3 +0 -0
- data/spec/test_app/db/test.sqlite3 +0 -0
- data/spec/test_app/lib/assets/.keep +0 -0
- data/spec/test_app/log/.keep +0 -0
- data/spec/test_app/log/development.log +0 -0
- data/spec/test_app/log/test.log +0 -0
- data/spec/test_app/public/404.html +67 -0
- data/spec/test_app/public/422.html +67 -0
- data/spec/test_app/public/500.html +66 -0
- data/spec/test_app/public/favicon.ico +0 -0
- metadata +342 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'browsing_history/storages/base'
|
2
|
+
|
3
|
+
module BrowsingHistory::Storage
|
4
|
+
class InvalidStorage < BrowsingHistory::Error; end
|
5
|
+
class StoragesNotSet < BrowsingHistory::Error; end
|
6
|
+
|
7
|
+
def self.included(klass)
|
8
|
+
klass.extend ConfigureMethods
|
9
|
+
klass.init_storages
|
10
|
+
end
|
11
|
+
|
12
|
+
module ConfigureMethods
|
13
|
+
attr_reader :current_storage_type
|
14
|
+
|
15
|
+
def current_storage
|
16
|
+
storages[current_storage_type]
|
17
|
+
end
|
18
|
+
|
19
|
+
def storages
|
20
|
+
@storages ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def attach_storage(storage_type, **opts)
|
24
|
+
storage = storages[storage_type]
|
25
|
+
|
26
|
+
if storage
|
27
|
+
storage.connect(opts)
|
28
|
+
@current_storage_type = storage_type
|
29
|
+
else
|
30
|
+
raise InvalidStorage
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def init_storages
|
35
|
+
BrowsingHistory.storage_types.each do |key|
|
36
|
+
storages[key] =
|
37
|
+
"BrowsingHistory::Storages::#{key.to_s.camelize}".constantize
|
38
|
+
end
|
39
|
+
|
40
|
+
raise StoragesNotSet if storages.empty?
|
41
|
+
attach_storage(storages.keys.first)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module BrowsingHistory::Storages
|
2
|
+
IOMethods = %i(fetch add update clear count interface).freeze
|
3
|
+
|
4
|
+
class NotOverideError < BrowsingHistory::Error; end
|
5
|
+
|
6
|
+
class Base
|
7
|
+
attr_accessor :browser, :historizable, :options
|
8
|
+
|
9
|
+
def initialize(browser, historizable)
|
10
|
+
@browser = browser
|
11
|
+
@historizable = historizable
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method, *args)
|
15
|
+
if method.in?(IOMethods)
|
16
|
+
raise NotOverideError
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def connect
|
24
|
+
IOMethods.each do |method|
|
25
|
+
define_singleton_method(method) do |browser, historizable, *_args|
|
26
|
+
opts = _args.dup.extract_options!.merge(default_options)
|
27
|
+
new(browser, historizable).send(method, **opts)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_options
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'browsing_history/storages/redis'
|
39
|
+
require 'browsing_history/storages/active_record'
|
40
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis-objects'
|
3
|
+
require 'redis-namespace'
|
4
|
+
|
5
|
+
class BrowsingHistory::Storages::Redis < BrowsingHistory::Storages::Base
|
6
|
+
def fetch(opts)
|
7
|
+
if opts[:between]
|
8
|
+
fetch_between(**opts)
|
9
|
+
elsif opts[:at]
|
10
|
+
history.at(opts[:at])
|
11
|
+
else
|
12
|
+
limit = opts[:limit].try(:-, 1) || opts.delete(:end)
|
13
|
+
history.revrange(opts.delete(:start), limit, **opts)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(_opts)
|
18
|
+
history.add(historizable, score)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear(opts)
|
25
|
+
if opts[:expiration]
|
26
|
+
clear_expiration(opts)
|
27
|
+
elsif opts[:all]
|
28
|
+
clear_all
|
29
|
+
else
|
30
|
+
history.delete(historizable)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def count(opts)
|
35
|
+
if opts[:between]
|
36
|
+
count_between(opts)
|
37
|
+
else
|
38
|
+
history.range_size(
|
39
|
+
history.score(history.first),
|
40
|
+
history.score(history.last)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def interface(_opts)
|
46
|
+
history
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def history
|
52
|
+
key = %(\
|
53
|
+
#{browser.browser_type}/\
|
54
|
+
#{browser.browser_id}/\
|
55
|
+
#{historizable.historizable_type}\
|
56
|
+
).delete(' ')
|
57
|
+
|
58
|
+
self.class.set.new(key, marshal: true)
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch_between(opts)
|
62
|
+
start_date, end_date = parse_between(opts.delete(:between))
|
63
|
+
|
64
|
+
history.revrangebyscore(start_date, end_date, **opts)
|
65
|
+
end
|
66
|
+
|
67
|
+
def clear_expiration(opts)
|
68
|
+
min_score = history.score(history.first)
|
69
|
+
|
70
|
+
history.remrangebyscore(min_score, score(opts[:expiration]))
|
71
|
+
end
|
72
|
+
|
73
|
+
def clear_all
|
74
|
+
history.remrangebyrank(0, -1)
|
75
|
+
end
|
76
|
+
|
77
|
+
def count_between(opts)
|
78
|
+
end_date, start_date = parse_between(opts.delete(:between))
|
79
|
+
|
80
|
+
history.range_size(start_date, end_date)
|
81
|
+
end
|
82
|
+
|
83
|
+
def score(time = nil)
|
84
|
+
time ? time.to_f : Time.now.to_f
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_between(range)
|
88
|
+
[range.first.to_f, range.last.to_f]
|
89
|
+
end
|
90
|
+
|
91
|
+
class << self
|
92
|
+
attr_reader :redis, :set
|
93
|
+
|
94
|
+
def connect(opts = {})
|
95
|
+
@redis = Redis::Namespace.new(
|
96
|
+
BrowsingHistory.namespace,
|
97
|
+
redis: Redis.new(default_connect_options.merge(opts))
|
98
|
+
)
|
99
|
+
Redis::Objects.redis = @redis
|
100
|
+
@set = Redis::SortedSet
|
101
|
+
|
102
|
+
super()
|
103
|
+
end
|
104
|
+
|
105
|
+
def default_options
|
106
|
+
{ start: 0, end: -1 }
|
107
|
+
end
|
108
|
+
|
109
|
+
def default_connect_options
|
110
|
+
{ host: '127.0.0.1', port: 6379, db: 0 }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
|
4
|
+
module BrowsingHistory
|
5
|
+
require 'browsing_history/error'
|
6
|
+
require 'browsing_history/configuration'
|
7
|
+
extend Configuration
|
8
|
+
|
9
|
+
require 'browsing_history/history'
|
10
|
+
require 'browsing_history/browser'
|
11
|
+
require 'browsing_history/historizable'
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/actions'
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
class BrowsingHistory::MigrationGenerator < ActiveRecord::Generators::Base
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
7
|
+
|
8
|
+
argument :name, type: :string, default: 'browsing_histories'
|
9
|
+
def generate_files
|
10
|
+
migration_template 'migration.rb', "db/migrate/create_#{name}.rb"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateBrowsingHistories < ActiveRecord::Migration
|
2
|
+
# Create table
|
3
|
+
def up
|
4
|
+
create_table :browsing_histories do |t|
|
5
|
+
t.references :browser, polymorphic: true, index: true
|
6
|
+
t.references :historizable, polymorphic: true, index: true
|
7
|
+
t.text :object, null: false
|
8
|
+
t.timestamps null: false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Drop table
|
13
|
+
def down
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fixtures/user'
|
3
|
+
require 'fixtures/article'
|
4
|
+
|
5
|
+
describe BrowsingHistory::Browser do
|
6
|
+
articles_size = 50
|
7
|
+
|
8
|
+
let(:user) { User.new }
|
9
|
+
let(:article) { Article.new }
|
10
|
+
let(:articles) do
|
11
|
+
articles_size.times.map.with_index(1) { |i| Article.new(i) }
|
12
|
+
end
|
13
|
+
let(:histories) { user.browsing_histories }
|
14
|
+
|
15
|
+
describe 'respond_to' do
|
16
|
+
context 'instance' do
|
17
|
+
subject { user }
|
18
|
+
|
19
|
+
it { is_expected.to respond_to(:browsing_histories) }
|
20
|
+
it { is_expected.to respond_to(:browser_id) }
|
21
|
+
it { is_expected.to respond_to(:browser_type) }
|
22
|
+
it { is_expected.to respond_to(:browser_class) }
|
23
|
+
it { is_expected.to respond_to(:browser_instance?) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'singleton' do
|
27
|
+
subject { user.class }
|
28
|
+
|
29
|
+
it { is_expected.to respond_to(:browser_id) }
|
30
|
+
it { is_expected.to respond_to(:browser_type) }
|
31
|
+
it { is_expected.to respond_to(:browser_class) }
|
32
|
+
it { is_expected.to respond_to(:browser_instance?) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'assosiation' do
|
37
|
+
describe 'respond_to' do
|
38
|
+
subject { user.browsing_histories }
|
39
|
+
|
40
|
+
it { is_expected.to respond_to(:recent) }
|
41
|
+
it { is_expected.to respond_to(:previous) }
|
42
|
+
it { is_expected.to respond_to(:add) }
|
43
|
+
it { is_expected.to respond_to(:<<) }
|
44
|
+
it { is_expected.to respond_to(:count) }
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#initialize' do
|
48
|
+
subject { user.browsing_histories }
|
49
|
+
it { is_expected.to be_a("#{described_class}::Association".constantize) }
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#recent' do
|
53
|
+
context 'default' do
|
54
|
+
it 'should default empty' do
|
55
|
+
expect(histories.recent(article)).to be_empty
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when histories exist' do
|
60
|
+
before(:each) do
|
61
|
+
articles.each { |article| histories << article }
|
62
|
+
end
|
63
|
+
let(:recent_articles) { histories.recent(article, limit: articles.size) }
|
64
|
+
|
65
|
+
it 'should equal original' do
|
66
|
+
recent_articles.each_with_index do |article, i|
|
67
|
+
original = articles.reverse[i]
|
68
|
+
|
69
|
+
expect(article.id).to eq(original.id)
|
70
|
+
expect(article.title).to eq(original.title)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#previous' do
|
77
|
+
context 'default' do
|
78
|
+
it 'should default empty' do
|
79
|
+
expect(histories.previous(article, Time.now..1.day.ago)).to be_empty
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'when histories exist' do
|
84
|
+
before do
|
85
|
+
articles.each { |article| histories << article }
|
86
|
+
end
|
87
|
+
|
88
|
+
let(:recent_article) { Article.new(100) }
|
89
|
+
let(:previous_article) do
|
90
|
+
histories.previous(article, Time.now..5.seconds.ago)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return 1 historizable' do
|
94
|
+
sleep(6)
|
95
|
+
histories << recent_article
|
96
|
+
|
97
|
+
expect(previous_article.size).to eq(1)
|
98
|
+
expect(previous_article.first.id).to eq(recent_article.id)
|
99
|
+
expect(previous_article.first.title).to eq(recent_article.title)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#add' do
|
105
|
+
context 'add' do
|
106
|
+
it 'should return historizable' do
|
107
|
+
expect(histories.add(article)).to be_truthy
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'before add historizable' do
|
111
|
+
before { histories.add(article) }
|
112
|
+
|
113
|
+
let(:saved_article) { histories.recent(article).first }
|
114
|
+
it 'should equal original' do
|
115
|
+
expect(saved_article.id).to eq(article.id)
|
116
|
+
expect(saved_article.title).to eq(article.title)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context '<<' do
|
122
|
+
it 'should return historizable' do
|
123
|
+
expect(histories << article).to be_truthy
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'before << historizable' do
|
127
|
+
before { histories << article }
|
128
|
+
|
129
|
+
let(:saved_article) { histories.recent(article).first }
|
130
|
+
it 'should equal original' do
|
131
|
+
expect(saved_article.id).to eq(article.id)
|
132
|
+
expect(saved_article.title).to eq(article.title)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '#count' do
|
138
|
+
context 'default' do
|
139
|
+
it 'should default empty' do
|
140
|
+
expect(histories.count(article)).to eq(0)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'when histories exist' do
|
145
|
+
before do
|
146
|
+
articles.each { |article| histories << article }
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should return historizables count' do
|
150
|
+
expect(histories.count(article)).to eq(articles.size)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BrowsingHistory::Configuration do
|
4
|
+
let(:storage_types) { %i(redis active_record) }
|
5
|
+
let(:storage_type) { storage_types.first }
|
6
|
+
|
7
|
+
describe '#configure' do
|
8
|
+
it 'should have default attributes' do
|
9
|
+
BrowsingHistory.configure do |configuration|
|
10
|
+
expect(configuration.storage_types).to eql(storage_types)
|
11
|
+
expect(configuration.storage_type).to eql(storage_type)
|
12
|
+
expect(configuration.namespace).to eql('browsing_history')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fixtures/user'
|
3
|
+
require 'fixtures/article'
|
4
|
+
|
5
|
+
describe BrowsingHistory::Browser do
|
6
|
+
let(:article) { Article.new }
|
7
|
+
|
8
|
+
describe 'respond_to' do
|
9
|
+
context 'instance' do
|
10
|
+
subject { article }
|
11
|
+
|
12
|
+
it { is_expected.to respond_to(:historizable_id) }
|
13
|
+
it { is_expected.to respond_to(:historizable_type) }
|
14
|
+
it { is_expected.to respond_to(:historizable_class) }
|
15
|
+
it { is_expected.to respond_to(:historizable_instance?) }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'singleton' do
|
19
|
+
subject { article.class }
|
20
|
+
|
21
|
+
it { is_expected.to respond_to(:historizable_id) }
|
22
|
+
it { is_expected.to respond_to(:historizable_type) }
|
23
|
+
it { is_expected.to respond_to(:historizable_class) }
|
24
|
+
it { is_expected.to respond_to(:historizable_instance?) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|