browsing_history 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|