paulie_lombardo 1.0.0
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/lib/generators/paulie_lombardo/active_record/active_record_generator.rb +42 -0
- data/lib/generators/paulie_lombardo/install/install_generator.rb +13 -0
- data/lib/generators/paulie_lombardo/mongoid/mongoid_generator.rb +13 -0
- data/lib/generators/paulie_lombardo/sequel/sequel_generator.rb +13 -0
- data/lib/paulie_lombardo/consigliere.rb +46 -0
- data/lib/paulie_lombardo/controller.rb +13 -0
- data/lib/paulie_lombardo/dossier.rb +118 -0
- data/lib/paulie_lombardo/model.rb +18 -0
- data/lib/paulie_lombardo/paulie_note.rb +22 -0
- data/lib/paulie_lombardo/paulie_static_page.rb +51 -0
- data/lib/paulie_lombardo/railtie.rb +19 -0
- data/lib/paulie_lombardo/version.rb +5 -0
- data/lib/paulie_lombardo.rb +25 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fb56753c7d4d30bcaa0d23a1e7ee369f5f5e4d9eca7c273a6b0f19075826894b
|
|
4
|
+
data.tar.gz: 31773ebb1fc6ce4596166bebdef54ea91f1fd13a56f947493d19853355dda7cf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fa1f3c6fbb13b13de3d86febd773a4e7cda4c761930ea414c43d4e6eb5a786e4936ecc7000d71e721cb31a8ea633b282c120b7ec8465b1e49e796050641313bb
|
|
7
|
+
data.tar.gz: 6b96d4d26951d97abb621a8d0f514ca7273c63423f0324a30a6f4715f536ae4b9c03d9d5f3506b7ff90cd5e9ca1914231dc65284810a305270a545e1a50af6fe
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
module PaulieLombardo
|
|
6
|
+
module Generators
|
|
7
|
+
# Creating the migration file for the paulie_notes table.
|
|
8
|
+
class ActiveRecordGenerator < Rails::Generators::Base
|
|
9
|
+
# Provides ActiveRecord implementation of #next_migration_number for #migration_template
|
|
10
|
+
include ::ActiveRecord::Generators::Migration
|
|
11
|
+
|
|
12
|
+
argument :tables, type: :array, default: []
|
|
13
|
+
|
|
14
|
+
source_root File.join(File.dirname(__FILE__), "templates")
|
|
15
|
+
|
|
16
|
+
def validate_tables_exist
|
|
17
|
+
return if tables.empty?
|
|
18
|
+
|
|
19
|
+
connection = ActiveRecord::Base.connection
|
|
20
|
+
|
|
21
|
+
tables.each do |table_name|
|
|
22
|
+
name = table_name.tableize
|
|
23
|
+
|
|
24
|
+
unless connection.table_exists?(name)
|
|
25
|
+
msg = set_color("Error: Table '#{name}' does not exist in the database.", :red, :bold)
|
|
26
|
+
raise Thor::Error, msg
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_migration_file
|
|
32
|
+
migration_template "create_paulie_notes_table.rb.erb", "db/migrate/create_paulie_notes.rb"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_counter_cache_fields_to_tables
|
|
36
|
+
return if tables.empty?
|
|
37
|
+
|
|
38
|
+
migration_template "add_paulie_notes_count_to_tables.rb.erb", "db/migrate/add_paulie_notes_count.rb"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
module Generators
|
|
5
|
+
# Primary setup generator for installing the gem into a Rails app
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
argument :tables, type: :array, default: []
|
|
8
|
+
|
|
9
|
+
# Invokes the configured ORM generator (e.g., ActiveRecord) to create migrations
|
|
10
|
+
hook_for :orm, required: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
module Generators
|
|
5
|
+
# Stub generator for Mongoid ORM implementation
|
|
6
|
+
class MongoidGenerator < Rails::Generators::Base
|
|
7
|
+
def create_migration_file
|
|
8
|
+
say_status :warning, "Mongoid support requires implementation for paulie_lombardo.", :yellow
|
|
9
|
+
say " Currently, automatic document/migration generation for Mongoid is not supported.", :yellow
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
module Generators
|
|
5
|
+
# Stub generator for Sequel ORM implementation
|
|
6
|
+
class SequelGenerator < Rails::Generators::Base
|
|
7
|
+
def create_migration_file
|
|
8
|
+
say_status :warning, "Sequel support requires implementation for paulie_lombardo.", :yellow
|
|
9
|
+
say " Currently, automatic migration generation for Sequel is not supported.", :yellow
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
# The decision-making authority that decides
|
|
5
|
+
# whether a page view request should be approved and logged.
|
|
6
|
+
class Consigliere
|
|
7
|
+
def initialize(dossier)
|
|
8
|
+
@dossier = dossier
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def approve?
|
|
12
|
+
no_prior_record?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Checks if there are no previous records for the given visitor.
|
|
18
|
+
#
|
|
19
|
+
# Automatically handles collision/fingerprint overlaps
|
|
20
|
+
# multiple records with the same request_hash can coexist if they belong to different session_hashes
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
# true if no record exists and a new one should be created
|
|
24
|
+
# false if a record already exists
|
|
25
|
+
def no_prior_record?
|
|
26
|
+
# 1. Highest priority: if user is logged in, check purely by user_id
|
|
27
|
+
return PaulieNote.where(@dossier.user_profile).none? if @dossier.user_id.present?
|
|
28
|
+
|
|
29
|
+
# 2. If session_hash is blank, simply check existence via request_hash
|
|
30
|
+
return PaulieNote.where(@dossier.request_profile).none? if @dossier.session_hash.blank?
|
|
31
|
+
|
|
32
|
+
# 3. When session_hash present - returns false early if a matching record exists
|
|
33
|
+
return false if PaulieNote.where(@dossier.session_profile).exists?
|
|
34
|
+
|
|
35
|
+
# Finds a session-less record matching the request profile and links the current session_hash.
|
|
36
|
+
return false if session_attached?
|
|
37
|
+
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def session_attached?
|
|
42
|
+
note = PaulieNote.find_by(@dossier.request_profile)
|
|
43
|
+
note&.update(session_hash: @dossier.session_hash)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
# Adds controller methods
|
|
5
|
+
module Controller
|
|
6
|
+
# Creates a new paulie_note record
|
|
7
|
+
def paulie_note(*target)
|
|
8
|
+
PaulieLombardo.bouncer(self, *target) do |target, data|
|
|
9
|
+
target.paulie_notes.create!(data)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
# Build paulie_note attributes
|
|
5
|
+
class Dossier
|
|
6
|
+
attr_reader :data, :user_id, :interval, :target
|
|
7
|
+
|
|
8
|
+
def initialize(controller, target, scope = nil)
|
|
9
|
+
@request = controller.request
|
|
10
|
+
@session = controller.session
|
|
11
|
+
@user_id = user_id_from_warden
|
|
12
|
+
@target = identify_subject(target)
|
|
13
|
+
@interval = configured_interval(scope)
|
|
14
|
+
|
|
15
|
+
build
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def request_hash
|
|
19
|
+
data[:request_hash]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def session_hash
|
|
23
|
+
data[:session_hash]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def request_profile
|
|
27
|
+
base_profile(request_hash: request_hash, session_hash: nil)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def session_profile
|
|
31
|
+
base_profile(session_hash: session_hash)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def user_profile
|
|
35
|
+
base_profile(user_id: user_id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
UUID_NAMESPACE = "9cac5761-c7b1-4c75-be24-1d4db88ac468"
|
|
39
|
+
private_constant :UUID_NAMESPACE
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :request
|
|
44
|
+
|
|
45
|
+
def build
|
|
46
|
+
@data = {
|
|
47
|
+
session_hash: generate_uuid(@session.id),
|
|
48
|
+
request_hash: generate_uuid(raw_string),
|
|
49
|
+
referrer: request.referrer
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@data[:user_id] = user_id if user_id.present?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def generate_uuid(value)
|
|
56
|
+
return if value.blank?
|
|
57
|
+
|
|
58
|
+
Digest::UUID.uuid_v5(UUID_NAMESPACE, value.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def raw_string
|
|
62
|
+
["request_hash",
|
|
63
|
+
request.user_agent,
|
|
64
|
+
request.accept_language,
|
|
65
|
+
mask_ip,
|
|
66
|
+
http_sec_ch_ua,
|
|
67
|
+
http_sec_ch_ua_platform,
|
|
68
|
+
http_sec_ch_ua_mobile].compact.join("/")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def mask_ip
|
|
72
|
+
addr = IPAddr.new(request.remote_ip)
|
|
73
|
+
|
|
74
|
+
if addr.ipv4?
|
|
75
|
+
addr.mask(24).to_s
|
|
76
|
+
else
|
|
77
|
+
addr.mask(48).to_s
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def http_sec_ch_ua
|
|
82
|
+
request.headers["HTTP_SEC_CH_UA"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def http_sec_ch_ua_platform
|
|
86
|
+
request.headers["HTTP_SEC_CH_UA_PLATFORM"]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def http_sec_ch_ua_mobile
|
|
90
|
+
request.headers["HTTP_SEC_CH_UA_MOBILE"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def user_id_from_warden
|
|
94
|
+
warden = request.env["warden"]
|
|
95
|
+
return nil unless warden
|
|
96
|
+
|
|
97
|
+
warden.user(:user)&.id
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def configured_interval(scope)
|
|
101
|
+
return scope if @target.is_a?(PaulieStaticPage)
|
|
102
|
+
return unless @target.respond_to?(:paulie_notes_options)
|
|
103
|
+
|
|
104
|
+
@target.paulie_notes_options[:interval]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def identify_subject(target)
|
|
108
|
+
target.is_a?(ActiveRecord::Base) ? target : PaulieStaticPage.new(target)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def base_profile(attributes)
|
|
112
|
+
attributes[:noteable_type] = @target.class.polymorphic_name
|
|
113
|
+
attributes[:noteable_id] = @target.id.to_s
|
|
114
|
+
attributes[:viewed_on] = Time.current.utc.to_date if interval == :daily
|
|
115
|
+
attributes
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
# Adds 'has_paulie_notes' association to ActiveRecord models
|
|
5
|
+
module Model
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
# rubocop:disable-next Naming/PredicatePrefix
|
|
10
|
+
def has_paulie_notes(interval: nil)
|
|
11
|
+
class_attribute :paulie_notes_options, default: {}
|
|
12
|
+
self.paulie_notes_options = { interval: interval }.freeze
|
|
13
|
+
|
|
14
|
+
has_many :paulie_notes, as: :noteable, dependent: :delete_all
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Represents an individual note record associated polymorphically with any model.
|
|
4
|
+
class PaulieNote < ActiveRecord::Base
|
|
5
|
+
belongs_to :noteable, polymorphic: true, counter_cache: true
|
|
6
|
+
|
|
7
|
+
before_create :set_viewed_on
|
|
8
|
+
|
|
9
|
+
def noteable
|
|
10
|
+
if noteable_type == PaulieStaticPage.polymorphic_name
|
|
11
|
+
PaulieStaticPage.new(noteable_id)
|
|
12
|
+
else
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def set_viewed_on
|
|
20
|
+
self.viewed_on ||= Time.current.utc.to_date
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Represents a non-ActiveRecord target for polymorphic `PaulieNote` associations
|
|
4
|
+
class PaulieStaticPage
|
|
5
|
+
attr_reader :id
|
|
6
|
+
|
|
7
|
+
def initialize(name)
|
|
8
|
+
@id = name.to_s.underscore
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.polymorphic_name
|
|
12
|
+
name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Emulates `has_many :paulie_notes` creation for non-ActiveRecord target
|
|
16
|
+
class AssociationProxy
|
|
17
|
+
def initialize(target)
|
|
18
|
+
@target = target
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create!(attributes = {})
|
|
22
|
+
PaulieNote.create!(
|
|
23
|
+
attributes.merge(
|
|
24
|
+
noteable_type: @target.class.polymorphic_name,
|
|
25
|
+
noteable_id: @target.id
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def paulie_notes
|
|
32
|
+
AssociationProxy.new(self)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def paulie_notes_count
|
|
36
|
+
PaulieNote.where(noteable_type: self.class.polymorphic_name, noteable_id: id).count
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Stubs to prevent SQL execution and NoMethodError for the counter_cache column
|
|
40
|
+
#
|
|
41
|
+
# rubocop:disable-next Style/SingleLineMethods
|
|
42
|
+
# rubocop:disable-next Naming/PredicatePrefix
|
|
43
|
+
class << self
|
|
44
|
+
def unscoped; self; end
|
|
45
|
+
def where!(*); self; end
|
|
46
|
+
def primary_key; :id; end
|
|
47
|
+
def has_query_constraints?; false; end
|
|
48
|
+
def composite_primary_key?; false; end
|
|
49
|
+
def update_counters(*_args); 0 end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaulieLombardo
|
|
4
|
+
# Railtie integration to automatically load Controller and Model concerns
|
|
5
|
+
# into Rails framework components upon initialization.
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer "paulie_lombardo.controller" do
|
|
8
|
+
ActiveSupport.on_load(:action_controller) do
|
|
9
|
+
include PaulieLombardo::Controller
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "paulie_lombardo.active_record" do
|
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
|
15
|
+
include PaulieLombardo::Model
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "paulie_lombardo/paulie_static_page"
|
|
4
|
+
require "paulie_lombardo/dossier"
|
|
5
|
+
require "paulie_lombardo/controller"
|
|
6
|
+
require "paulie_lombardo/paulie_note"
|
|
7
|
+
require "paulie_lombardo/model"
|
|
8
|
+
require "paulie_lombardo/consigliere"
|
|
9
|
+
require "paulie_lombardo/railtie"
|
|
10
|
+
|
|
11
|
+
# Main entry point for the PaulieLombardo
|
|
12
|
+
module PaulieLombardo
|
|
13
|
+
def self.bouncer(*)
|
|
14
|
+
dossier = Dossier.new(*)
|
|
15
|
+
consigliere = Consigliere.new(dossier)
|
|
16
|
+
|
|
17
|
+
paulie_note = yield(dossier.target, dossier.data) if consigliere.approve?
|
|
18
|
+
|
|
19
|
+
if paulie_note.present?
|
|
20
|
+
paulie_note.noteable
|
|
21
|
+
else
|
|
22
|
+
dossier.target
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: paulie_lombardo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sasha Stadnyk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.0.0
|
|
26
|
+
description: |
|
|
27
|
+
Paulie is the silent mobster standing at the entrance
|
|
28
|
+
of your page, closely watching every visitor and logging it to the database"
|
|
29
|
+
email: stadniklksndr@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/generators/paulie_lombardo/active_record/active_record_generator.rb
|
|
35
|
+
- lib/generators/paulie_lombardo/install/install_generator.rb
|
|
36
|
+
- lib/generators/paulie_lombardo/mongoid/mongoid_generator.rb
|
|
37
|
+
- lib/generators/paulie_lombardo/sequel/sequel_generator.rb
|
|
38
|
+
- lib/paulie_lombardo.rb
|
|
39
|
+
- lib/paulie_lombardo/consigliere.rb
|
|
40
|
+
- lib/paulie_lombardo/controller.rb
|
|
41
|
+
- lib/paulie_lombardo/dossier.rb
|
|
42
|
+
- lib/paulie_lombardo/model.rb
|
|
43
|
+
- lib/paulie_lombardo/paulie_note.rb
|
|
44
|
+
- lib/paulie_lombardo/paulie_static_page.rb
|
|
45
|
+
- lib/paulie_lombardo/railtie.rb
|
|
46
|
+
- lib/paulie_lombardo/version.rb
|
|
47
|
+
homepage: https://github.com/stadniklksndr/paulie_lombardo
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
rubygems_mfa_required: 'true'
|
|
52
|
+
source_code_uri: https://github.com/stadniklksndr/paulie_lombardo
|
|
53
|
+
changelog_uri: https://github.com/stadniklksndr/paulie_lombardo/blob/master/CHANGELOG.md
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '4.0'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.19
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: Ruby on Rails page view counter
|
|
71
|
+
test_files: []
|