hit_counter 0.1.0 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- data/.github/ISSUE_TEMPLATE/task.md +21 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/test.yml +35 -0
- data/.gitignore +8 -0
- data/.simplecov +15 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +34 -0
- data/Gemfile +3 -13
- data/LICENSE.txt +17 -11
- data/README.md +121 -0
- data/Rakefile +6 -44
- data/SECURITY.md +16 -0
- data/certs/ivanoblomov.pem +25 -0
- data/config/initializers/mime_types.rb +3 -0
- data/config/mongoid.yml +6 -0
- data/hit_counter.gemspec +30 -85
- data/lib/hit_counter.rb +82 -72
- data/lib/version.rb +6 -0
- data/public/images/digits/celtic/0.png +0 -0
- data/public/images/digits/celtic/1.png +0 -0
- data/public/images/digits/celtic/2.png +0 -0
- data/public/images/digits/celtic/3.png +0 -0
- data/public/images/digits/celtic/4.png +0 -0
- data/public/images/digits/celtic/5.png +0 -0
- data/public/images/digits/celtic/6.png +0 -0
- data/public/images/digits/celtic/7.png +0 -0
- data/public/images/digits/celtic/8.png +0 -0
- data/public/images/digits/celtic/9.png +0 -0
- data/public/images/digits/odometer/0.png +0 -0
- data/public/images/digits/odometer/1.png +0 -0
- data/public/images/digits/odometer/2.png +0 -0
- data/public/images/digits/odometer/3.png +0 -0
- data/public/images/digits/odometer/4.png +0 -0
- data/public/images/digits/odometer/5.png +0 -0
- data/public/images/digits/odometer/6.png +0 -0
- data/public/images/digits/odometer/7.png +0 -0
- data/public/images/digits/odometer/8.png +0 -0
- data/public/images/digits/odometer/9.png +0 -0
- data/public/images/digits/scout/0.png +0 -0
- data/public/images/digits/scout/1.png +0 -0
- data/public/images/digits/scout/2.png +0 -0
- data/public/images/digits/scout/3.png +0 -0
- data/public/images/digits/scout/4.png +0 -0
- data/public/images/digits/scout/5.png +0 -0
- data/public/images/digits/scout/6.png +0 -0
- data/public/images/digits/scout/7.png +0 -0
- data/public/images/digits/scout/8.png +0 -0
- data/public/images/digits/scout/9.png +0 -0
- data/spec/lib/hit_counter_spec.rb +104 -70
- data/spec/spec_helper.rb +10 -3
- data.tar.gz.sig +0 -0
- metadata +262 -147
- metadata.gz.sig +0 -0
- data/README.rdoc +0 -82
- data/VERSION +0 -1
- data/public/images/digits/celtic/0.gif +0 -0
- data/public/images/digits/celtic/1.gif +0 -0
- data/public/images/digits/celtic/2.gif +0 -0
- data/public/images/digits/celtic/3.gif +0 -0
- data/public/images/digits/celtic/4.gif +0 -0
- data/public/images/digits/celtic/5.gif +0 -0
- data/public/images/digits/celtic/6.gif +0 -0
- data/public/images/digits/celtic/7.gif +0 -0
- data/public/images/digits/celtic/8.gif +0 -0
- data/public/images/digits/celtic/9.gif +0 -0
- data/public/images/digits/odometer/0.gif +0 -0
- data/public/images/digits/odometer/1.gif +0 -0
- data/public/images/digits/odometer/2.gif +0 -0
- data/public/images/digits/odometer/3.gif +0 -0
- data/public/images/digits/odometer/4.gif +0 -0
- data/public/images/digits/odometer/5.gif +0 -0
- data/public/images/digits/odometer/6.gif +0 -0
- data/public/images/digits/odometer/7.gif +0 -0
- data/public/images/digits/odometer/8.gif +0 -0
- data/public/images/digits/odometer/9.gif +0 -0
- data/public/images/digits/scout/0.gif +0 -0
- data/public/images/digits/scout/1.gif +0 -0
- data/public/images/digits/scout/2.gif +0 -0
- data/public/images/digits/scout/3.gif +0 -0
- data/public/images/digits/scout/4.gif +0 -0
- data/public/images/digits/scout/5.gif +0 -0
- data/public/images/digits/scout/6.gif +0 -0
- data/public/images/digits/scout/7.gif +0 -0
- data/public/images/digits/scout/8.gif +0 -0
- data/public/images/digits/scout/9.gif +0 -0
data/lib/hit_counter.rb
CHANGED
@@ -1,137 +1,147 @@
|
|
1
|
-
# Copyright
|
2
|
-
#
|
3
|
-
#
|
4
|
-
|
5
|
-
# the Free Software Foundation, either version 3 of the License, or
|
6
|
-
# (at your option) any later version.
|
7
|
-
#
|
8
|
-
# This program is distributed in the hope that it will be useful,
|
9
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
-
# GNU General Public License for more details.
|
12
|
-
#
|
13
|
-
# You should have received a copy of the GNU General Public License
|
14
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
1
|
+
# Copyright the HitCounter contributors.
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
15
5
|
require 'rubygems'
|
16
6
|
require 'bundler/setup'
|
17
7
|
Bundler.require :default
|
18
8
|
|
9
|
+
# Ruby version of that old 90s chestnut, <BLINK>the web-site hit counter</BLINK>
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# hc = HitCounter.get 'cnn.com'
|
13
|
+
# hc.increment
|
19
14
|
class HitCounter
|
20
15
|
include Mongoid::Document
|
21
16
|
include Mongoid::Timestamps
|
22
17
|
|
23
|
-
# Mongo Config
|
18
|
+
# Mongo Config ===============================================================
|
19
|
+
field :hits, type: Integer, default: 0
|
24
20
|
field :url
|
25
|
-
field :hits, :type => Integer, :default => 0
|
26
21
|
|
27
22
|
# Validates the <code>HitCounter</code>'s URL.
|
28
23
|
#
|
29
|
-
# If the URI library can parse the value and the scheme is valid, then we
|
24
|
+
# If the URI library can parse the value and the scheme is valid, then we
|
25
|
+
# assume the url is valid.
|
30
26
|
class UrlValidator < ActiveModel::EachValidator
|
31
|
-
def validate_each
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue Addressable::URI::InvalidURIError
|
39
|
-
record.errors[attribute] << 'Invalid URL'
|
40
|
-
end
|
27
|
+
def validate_each(record, attribute, value)
|
28
|
+
uri = Addressable::URI.parse value
|
29
|
+
raise Addressable::URI::InvalidURIError unless %w[http https].include?(
|
30
|
+
uri.scheme
|
31
|
+
)
|
32
|
+
rescue Addressable::URI::InvalidURIError
|
33
|
+
record.errors.add attribute, 'Invalid URL'
|
41
34
|
end
|
42
35
|
end
|
43
36
|
|
44
|
-
validates :
|
37
|
+
validates :hits, numericality: { only_integer: true }
|
38
|
+
validates :url, presence: true, url: true, uniqueness: true
|
45
39
|
|
46
|
-
# Class methods
|
40
|
+
# Class methods ==============================================================
|
47
41
|
|
48
|
-
# Returns a <code>HitCounter</code> matching the specified URL. The HitCounter
|
49
|
-
# matching one is found. In the latter case, the hits
|
42
|
+
# Returns a <code>HitCounter</code> matching the specified URL. The HitCounter
|
43
|
+
# is created if no matching one is found. In the latter case, the hits
|
44
|
+
# argument specifies the starting count.
|
50
45
|
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
args
|
59
|
-
|
46
|
+
# @param url [String] the URL for the site being counted
|
47
|
+
# @param hits [Integer] the number of hits to start with
|
48
|
+
# @return [HitCounter] the site's HitCounter
|
49
|
+
# @example
|
50
|
+
# hc = HitCounter.get 'cnn.com'
|
51
|
+
# hc = HitCounter.get 'cnn.com', 100
|
52
|
+
def self.get(url, hits = 0)
|
53
|
+
args = { url: normalize_url(url) }
|
54
|
+
args[:hits] = hits unless where(conditions: args).exists?
|
55
|
+
find_or_create_by args
|
60
56
|
end
|
61
57
|
|
62
|
-
# Instance methods: Overrides
|
58
|
+
# Instance methods: Overrides ================================================
|
59
|
+
|
60
|
+
# Sets the number of hits.
|
61
|
+
#
|
62
|
+
# @param value [Integer] the initial number of hits
|
63
|
+
# @return [Integer] the number of hits
|
64
|
+
# @example
|
65
|
+
# hc.hits = 100
|
66
|
+
def hits=(value)
|
67
|
+
self[:hits] = value.to_i
|
68
|
+
end
|
63
69
|
|
64
70
|
# Sets the URL to be tracked. The http prefix is optional.
|
65
71
|
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
def url=
|
71
|
-
self[:url] = HitCounter.normalize_url value
|
72
|
+
# @param value [String] the URL for the site being counted
|
73
|
+
# @return [String] the normalized URL
|
74
|
+
# @example
|
75
|
+
# hc.url = 'cnn.com'
|
76
|
+
def url=(value)
|
77
|
+
self[:url] = HitCounter.send(:normalize_url, value)
|
72
78
|
end
|
73
79
|
|
74
|
-
# Instance methods
|
80
|
+
# Instance methods ===========================================================
|
75
81
|
|
76
82
|
# Returns the hit count as a PNG image, using the specified style:
|
77
83
|
# 1 odometer
|
78
84
|
# 2 scout
|
79
85
|
# 3 Celtic
|
80
86
|
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
def image
|
86
|
-
image = HitCounter.cat_image
|
87
|
+
# @param style_number [String] the image style
|
88
|
+
# @return [Magick::Image] the current hit count as a PNG image
|
89
|
+
# @example
|
90
|
+
# hc.image = 1
|
91
|
+
def image(style_number)
|
92
|
+
image = HitCounter.send(:cat_image, hits.to_s, HitCounter.send(:normalize_style_number, style_number))
|
87
93
|
image.format = 'png'
|
88
94
|
image
|
89
95
|
end
|
90
96
|
|
91
97
|
# Increments the hit count and saves the HitCounter.
|
92
98
|
#
|
93
|
-
#
|
94
|
-
#
|
99
|
+
# @return [true] if the save was successful
|
100
|
+
# @example
|
101
|
+
# hc.increment
|
95
102
|
def increment
|
96
103
|
self.hits += 1
|
97
|
-
|
104
|
+
save
|
98
105
|
end
|
99
106
|
|
100
|
-
|
101
|
-
|
102
|
-
STYLES = ['odometer', 'scout', 'celtic']
|
107
|
+
STYLES = %w[odometer scout celtic].freeze
|
103
108
|
|
104
|
-
def self.cat_image
|
109
|
+
private_class_method def self.cat_image(number, style_index, images = Magick::ImageList.new)
|
105
110
|
return images.append(false) if number.blank?
|
106
|
-
|
111
|
+
|
112
|
+
cat_image(number[1..-1], style_index, images <<
|
113
|
+
Magick::Image.read("#{Rails.root}/public/images/digits/"\
|
114
|
+
"#{STYLES[style_index]}/#{number[0..0]}.png").first)
|
107
115
|
end
|
108
116
|
|
109
|
-
def self.normalize_style_number
|
117
|
+
private_class_method def self.normalize_style_number(value)
|
110
118
|
value = value.to_i
|
111
|
-
value -= 1 if value
|
119
|
+
value -= 1 if value.positive?
|
112
120
|
|
113
|
-
if value >= 0 && value < STYLES.size
|
114
|
-
|
115
|
-
|
116
|
-
value % 3
|
117
|
-
end
|
121
|
+
return value if value >= 0 && value < STYLES.size
|
122
|
+
|
123
|
+
value % 3
|
118
124
|
end
|
119
125
|
|
120
|
-
def self.normalize_url
|
126
|
+
private_class_method def self.normalize_url(value)
|
121
127
|
value !~ %r{^http://} ? "http://#{value}" : value
|
122
128
|
end
|
123
129
|
end
|
124
130
|
|
125
131
|
if defined? Rails
|
132
|
+
# Overriding Rails to include install task
|
126
133
|
class Railtie < Rails::Railtie
|
127
134
|
rake_tasks do
|
128
135
|
namespace :hit_counter do
|
129
136
|
desc 'Install HitCounter into your app.'
|
130
137
|
task :install do
|
131
138
|
puts 'Installing required image files...'
|
132
|
-
system "rsync -ruv #{Gem.searcher.find('hit_counter').full_gem_path}
|
139
|
+
system "rsync -ruv #{Gem.searcher.find('hit_counter').full_gem_path}"\
|
140
|
+
'/config .'
|
141
|
+
system "rsync -ruv #{Gem.searcher.find('hit_counter').full_gem_path}"\
|
142
|
+
'/public .'
|
133
143
|
end
|
134
144
|
end
|
135
145
|
end
|
136
146
|
end
|
137
|
-
end
|
147
|
+
end
|
data/lib/version.rb
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -1,109 +1,143 @@
|
|
1
|
-
|
1
|
+
# Copyright the HitCounter contributors.
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
# frozen_string_literal: true
|
2
4
|
|
5
|
+
require "#{File.dirname(__FILE__)}/../spec_helper"
|
6
|
+
|
7
|
+
JAVASCRIPT_HACK = %q(eval("console.log('All your base belong to us!')"))
|
8
|
+
# rubocop:disable Metrics/BlockLength
|
3
9
|
describe HitCounter do
|
4
|
-
before
|
5
|
-
|
6
|
-
|
7
|
-
end
|
10
|
+
before(:all) { described_class.delete_all }
|
11
|
+
|
12
|
+
subject { described_class.get 'www.google.com' }
|
8
13
|
|
9
|
-
describe '
|
10
|
-
context '
|
11
|
-
|
12
|
-
|
14
|
+
describe '.get' do
|
15
|
+
context 'with existing URL' do
|
16
|
+
let!(:hit_counter2) { described_class.get subject.url, 10 }
|
17
|
+
|
18
|
+
describe '.count' do
|
19
|
+
it { expect(described_class.count).to eq 1 }
|
13
20
|
end
|
14
21
|
|
15
22
|
context 'with starting count 10' do
|
16
|
-
describe 'hits' do
|
17
|
-
|
23
|
+
describe '#hits' do
|
24
|
+
it { expect(hit_counter2.hits).to eq 10 }
|
18
25
|
end
|
19
26
|
end
|
20
27
|
end
|
28
|
+
context 'with new URL' do
|
29
|
+
before { described_class.get 'www.cnn.com' }
|
21
30
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
describe 'document count' do
|
26
|
-
specify { HitCounter.count.should == 2 }
|
31
|
+
describe '.count' do
|
32
|
+
it { expect(described_class.count).to eq 2 }
|
27
33
|
end
|
28
34
|
|
35
|
+
context 'with blank starting count' do
|
36
|
+
describe '#hits' do
|
37
|
+
it do
|
38
|
+
expect(described_class.get('www.nytimes.com', nil).hits).to eq 0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
29
42
|
context 'with starting count 10' do
|
30
|
-
describe 'hits' do
|
31
|
-
|
43
|
+
describe '#hits' do
|
44
|
+
it { expect(described_class.get('online.wsj.com', 10).hits).to eq 10 }
|
32
45
|
end
|
33
46
|
end
|
34
47
|
end
|
35
|
-
|
48
|
+
context 'with JavaScript' do
|
49
|
+
before { described_class.delete_all }
|
36
50
|
|
37
|
-
|
38
|
-
subject { @hit_counter.hits }
|
51
|
+
subject { described_class.get(JAVASCRIPT_HACK) }
|
39
52
|
|
40
|
-
|
53
|
+
it do
|
54
|
+
subject.save
|
55
|
+
expect(subject).to be_invalid
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
describe '#hits' do
|
60
|
+
it { expect(subject.hits).to eq 0 }
|
41
61
|
|
42
62
|
context 'when incremented' do
|
43
|
-
before {
|
63
|
+
before { subject.increment }
|
44
64
|
|
45
|
-
|
65
|
+
it { expect(subject.hits).to eq 1 }
|
46
66
|
end
|
47
67
|
end
|
68
|
+
describe '#hits=' do
|
69
|
+
context 'with JavaScript' do
|
70
|
+
before { subject.hits = JAVASCRIPT_HACK }
|
48
71
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
it { should be_a Magick::Image }
|
53
|
-
pending 'should reflect hits'
|
72
|
+
it { expect(subject.hits).to eq 0 }
|
73
|
+
end
|
54
74
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
it 'should return 0' do
|
59
|
-
HitCounter.normalize_style_number(nil).should == 0
|
60
|
-
end
|
75
|
+
describe '#image' do
|
76
|
+
context 'with a valid :style_number' do
|
77
|
+
it { expect(subject.image('1').filename).to eq "./public/images/digits/odometer/#{subject.hits}.png" }
|
61
78
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
79
|
+
context 'with JavaScript' do
|
80
|
+
it {
|
81
|
+
expect(subject.image(JAVASCRIPT_HACK).filename).to eq "./public/images/digits/odometer/#{subject.hits}.png"
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
describe '#url=' do
|
86
|
+
context 'with JavaScript' do
|
87
|
+
before do
|
88
|
+
subject.url = JAVASCRIPT_HACK
|
89
|
+
subject.save
|
66
90
|
end
|
67
91
|
|
68
|
-
it
|
69
|
-
HitCounter.normalize_style_number('3').should == 2
|
70
|
-
end
|
92
|
+
it { expect(subject).to be_invalid }
|
71
93
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
94
|
+
end
|
95
|
+
describe '.normalize_style_number' do
|
96
|
+
context 'with nil' do
|
97
|
+
it { expect(described_class.send(:normalize_style_number, nil)).to be_zero }
|
98
|
+
end
|
99
|
+
context 'with empty string' do
|
100
|
+
it { expect(described_class.send(:normalize_style_number, '')).to be_zero }
|
101
|
+
end
|
102
|
+
context 'with JavaScript' do
|
103
|
+
it { expect(described_class.send(:normalize_style_number, JAVASCRIPT_HACK)).to be_zero }
|
104
|
+
end
|
105
|
+
context 'with number in range' do
|
106
|
+
{
|
107
|
+
'1' => 0,
|
108
|
+
'3' => 2
|
109
|
+
}.each do |input, output|
|
110
|
+
context "with '#{input}'" do
|
111
|
+
it do
|
112
|
+
expect(described_class.send(:normalize_style_number, input)).to eq output
|
82
113
|
end
|
83
114
|
end
|
84
115
|
end
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
116
|
+
end
|
117
|
+
context 'with number outside range' do
|
118
|
+
{
|
119
|
+
'-31' => 2,
|
120
|
+
'-1' => 2,
|
121
|
+
'4' => 0,
|
122
|
+
'34' => 0
|
123
|
+
}.each do |input, output|
|
124
|
+
context "with '#{input}'" do
|
125
|
+
it do
|
126
|
+
expect(described_class.send(:normalize_style_number, input)).to eq output
|
94
127
|
end
|
95
128
|
end
|
96
129
|
end
|
97
130
|
end
|
98
131
|
end
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
132
|
+
describe '.normalize_url' do
|
133
|
+
{
|
134
|
+
'cnn.com' => 'http://cnn.com',
|
135
|
+
'http://www.nytimes.com' => 'http://www.nytimes.com'
|
136
|
+
}.each do |input, output|
|
137
|
+
context "with '#{input}'" do
|
138
|
+
it { expect(described_class.send(:normalize_url, input)).to eq output }
|
139
|
+
end
|
107
140
|
end
|
108
141
|
end
|
109
|
-
end
|
142
|
+
end
|
143
|
+
# rubocop:enable Metrics/BlockLength
|
data/spec/spec_helper.rb
CHANGED
@@ -1,14 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simplecov'
|
1
4
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
5
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
require 'addressable/uri'
|
7
|
+
require 'mongoid'
|
8
|
+
require 'RMagick'
|
3
9
|
require 'rspec'
|
10
|
+
|
4
11
|
require 'hit_counter'
|
5
12
|
|
6
|
-
Mongoid
|
7
|
-
|
13
|
+
Mongoid.load! 'config/mongoid.yml', :test
|
14
|
+
Mongo::Logger.logger.level = ::Logger::WARN
|
8
15
|
|
9
16
|
# Stub Rails root during tests.
|
10
17
|
module Rails
|
11
18
|
def self.root
|
12
19
|
'.'
|
13
20
|
end
|
14
|
-
end
|
21
|
+
end
|
data.tar.gz.sig
ADDED
Binary file
|