tally 1.0.0.beta1
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/README.md +217 -0
- data/Rakefile +32 -0
- data/app/assets/config/tally_manifest.js +2 -0
- data/app/assets/javascripts/tally/application.js +15 -0
- data/app/assets/stylesheets/tally/application.css +15 -0
- data/app/controllers/tally/application_controller.rb +17 -0
- data/app/controllers/tally/days_controller.rb +19 -0
- data/app/controllers/tally/keys_controller.rb +19 -0
- data/app/controllers/tally/records_controller.rb +21 -0
- data/app/helpers/tally/application_helper.rb +4 -0
- data/app/helpers/tally/increment_helper.rb +15 -0
- data/app/jobs/tally/application_job.rb +4 -0
- data/app/jobs/tally/calculator_runner_job.rb +12 -0
- data/app/models/tally/application_record.rb +7 -0
- data/app/models/tally/record.rb +30 -0
- data/app/presenters/tally/record_presenter.rb +29 -0
- data/bin/annotate +29 -0
- data/bin/appraisal +29 -0
- data/bin/bundle +114 -0
- data/bin/coderay +29 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/nokogiri +29 -0
- data/bin/pry +29 -0
- data/bin/rackup +29 -0
- data/bin/rails +14 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/sprockets +29 -0
- data/bin/thor +29 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20181016172358_create_tally_records.rb +15 -0
- data/lib/tally.rb +77 -0
- data/lib/tally/archiver.rb +64 -0
- data/lib/tally/calculator.rb +30 -0
- data/lib/tally/calculator_runner.rb +56 -0
- data/lib/tally/calculators.rb +29 -0
- data/lib/tally/countable.rb +23 -0
- data/lib/tally/daily.rb +64 -0
- data/lib/tally/engine.rb +28 -0
- data/lib/tally/increment.rb +25 -0
- data/lib/tally/key_finder.rb +123 -0
- data/lib/tally/key_finder/entry.rb +66 -0
- data/lib/tally/keyable.rb +55 -0
- data/lib/tally/record_searcher.rb +110 -0
- data/lib/tally/sweeper.rb +46 -0
- data/lib/tally/version.rb +5 -0
- data/lib/tasks/tally_tasks.rake +16 -0
- metadata +256 -0
data/bin/rubocop
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rubocop", "rubocop")
|
data/bin/ruby-parse
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'ruby-parse' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("parser", "ruby-parse")
|
data/bin/ruby-rewrite
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'ruby-rewrite' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("parser", "ruby-rewrite")
|
data/bin/sprockets
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'sprockets' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("sprockets", "sprockets")
|
data/bin/thor
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'thor' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("thor", "thor")
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Tally::Engine.routes.draw do
|
2
|
+
|
3
|
+
get "/days/:type/:id", to: "days#index", as: :recordable_days
|
4
|
+
resources :days, only: :index
|
5
|
+
|
6
|
+
get "/keys/:type/:id", to: "keys#index", as: :recordable_keys
|
7
|
+
resources :keys, only: :index
|
8
|
+
|
9
|
+
get "/records/:type/:id", to: "records#index", as: :recordable_records
|
10
|
+
resources :records, only: :index
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateTallyRecords < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :tally_records do |t|
|
4
|
+
t.date :day
|
5
|
+
t.bigint :recordable_id
|
6
|
+
t.string :recordable_type
|
7
|
+
t.string :key
|
8
|
+
t.integer :value, default: 0
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :tally_records, [ :day, :key, :recordable_id, :recordable_type ], name: "index_tally_records_on_day", unique: true
|
14
|
+
end
|
15
|
+
end
|
data/lib/tally.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tally/engine"
|
4
|
+
require "tally/version"
|
5
|
+
require "active_support/dependencies/autoload"
|
6
|
+
require "kaminari/activerecord"
|
7
|
+
|
8
|
+
require "redis"
|
9
|
+
|
10
|
+
begin
|
11
|
+
# attempt to load sidekiq if it is installed
|
12
|
+
# if not, just use plain Redis + ActiveJob
|
13
|
+
require "sidekiq"
|
14
|
+
rescue LoadError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
module Tally
|
19
|
+
|
20
|
+
extend ActiveSupport::Autoload
|
21
|
+
include ActiveSupport::Configurable
|
22
|
+
|
23
|
+
autoload :Archiver
|
24
|
+
autoload :Calculator
|
25
|
+
autoload :CalculatorRunner
|
26
|
+
autoload :Countable
|
27
|
+
autoload :Daily
|
28
|
+
autoload :Increment
|
29
|
+
autoload :Keyable
|
30
|
+
autoload :KeyFinder
|
31
|
+
autoload :RecordSearcher
|
32
|
+
autoload :Sweeper
|
33
|
+
|
34
|
+
eager_autoload do
|
35
|
+
autoload :Calculators
|
36
|
+
end
|
37
|
+
|
38
|
+
extend Calculators
|
39
|
+
|
40
|
+
configure do |config|
|
41
|
+
config.prefix = "tally"
|
42
|
+
config.date_format = "%Y-%m-%d"
|
43
|
+
|
44
|
+
# Amount of time a key lives by default
|
45
|
+
config.ttl = 4.days
|
46
|
+
|
47
|
+
# Archivers get queued into the background with ActiveJob by default
|
48
|
+
# Set to :now to run inline
|
49
|
+
config.perform_calculators = :later
|
50
|
+
end
|
51
|
+
|
52
|
+
# If sidekiq is available, piggyback on its pooling
|
53
|
+
#
|
54
|
+
# Otherwise, just use redis directly
|
55
|
+
def self.redis(&block)
|
56
|
+
raise ArgumentError, "requires a block" unless block_given?
|
57
|
+
|
58
|
+
if defined?(Sidekiq)
|
59
|
+
Sidekiq.redis(&block)
|
60
|
+
else
|
61
|
+
block.call(redis_connection)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.redis_connection
|
66
|
+
@redis_connection ||= Redis.current
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.redis_connection=(connection)
|
70
|
+
@redis_connection = connection
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.increment(*args)
|
74
|
+
Increment.public_send(:increment, *args)
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Tally
|
2
|
+
class Archiver
|
3
|
+
|
4
|
+
def initialize(key: nil, day: nil, record: nil, type: nil)
|
5
|
+
@key = key
|
6
|
+
@day = day
|
7
|
+
@record = record
|
8
|
+
@type = type
|
9
|
+
end
|
10
|
+
|
11
|
+
def archive!
|
12
|
+
remove_existing_records
|
13
|
+
|
14
|
+
finder.entries.each do |entry|
|
15
|
+
next if entry.type.present? && !entry.record
|
16
|
+
|
17
|
+
record = if entry.record
|
18
|
+
Record.find_or_initialize_by(day: entry.date, key: entry.key, recordable: entry.record)
|
19
|
+
else
|
20
|
+
Record.find_or_initialize_by(day: entry.date, key: entry.key, recordable: nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
record.value = entry.value
|
24
|
+
record.save
|
25
|
+
end
|
26
|
+
|
27
|
+
enqueue_registered_calculators
|
28
|
+
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def day
|
33
|
+
@day ||= Time.current.utc.to_date
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.archive!(*args)
|
37
|
+
new(*args).archive!
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def enqueue_registered_calculators
|
43
|
+
day_str = day.strftime("%Y-%m-%d")
|
44
|
+
calculate_method = Tally.config.perform_calculators == :now ? :perform_now : :perform_later
|
45
|
+
|
46
|
+
Tally.calculators.each do |class_name|
|
47
|
+
CalculatorRunnerJob.public_send(calculate_method, class_name, day_str)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def finder
|
52
|
+
@finder ||= Daily.new(key: @key, day: @day, record: @record, type: @type)
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_existing_records
|
56
|
+
return if @key.present?
|
57
|
+
return if @record.present?
|
58
|
+
return if @type.present?
|
59
|
+
|
60
|
+
Record.where(day: day).delete_all
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Tally
|
4
|
+
module Calculator
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
attr_reader :day
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(day = Date.today)
|
13
|
+
@day = day
|
14
|
+
end
|
15
|
+
|
16
|
+
# Override in sub class, this is what gets called when the calculator
|
17
|
+
# is run. This method is run in the background so it can take a while
|
18
|
+
# if needed to summarize data.
|
19
|
+
def call
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def record_scope
|
26
|
+
Record.where(day: day).includes(:recordable)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Tally
|
2
|
+
class CalculatorRunner
|
3
|
+
|
4
|
+
attr_reader :date
|
5
|
+
|
6
|
+
def initialize(class_name, date)
|
7
|
+
@date = date
|
8
|
+
@class_name = class_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def klass
|
12
|
+
@klass ||= @class_name.to_s.safe_constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
# loop through each value and save in db
|
16
|
+
def save
|
17
|
+
return false unless valid?
|
18
|
+
|
19
|
+
values.each do |attributes|
|
20
|
+
create_record(attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def values
|
27
|
+
return [] unless valid?
|
28
|
+
|
29
|
+
@values ||= [ klass.new(date).call ].flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
klass.present? && date.present?
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def create_record(attributes)
|
39
|
+
finder = { day: date, key: attributes[:key] }
|
40
|
+
|
41
|
+
id = attributes.delete(:id)
|
42
|
+
type = attributes.delete(:type)
|
43
|
+
|
44
|
+
if id && type
|
45
|
+
finder[:recordable_id] = id
|
46
|
+
finder[:recordable_type] = type.to_s.classify
|
47
|
+
end
|
48
|
+
|
49
|
+
record = Record.find_or_initialize_by(finder)
|
50
|
+
|
51
|
+
record.attributes = attributes
|
52
|
+
record.save
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tally
|
2
|
+
module Calculators
|
3
|
+
|
4
|
+
def calculators
|
5
|
+
@calculators ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def register_calculator(*class_name)
|
9
|
+
@calculators ||= []
|
10
|
+
|
11
|
+
class_name.each do |class_name|
|
12
|
+
unless @calculators.include?(class_name.to_s)
|
13
|
+
@calculators.push(class_name.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def unregister_calculator(*class_names)
|
21
|
+
@calculators ||= []
|
22
|
+
|
23
|
+
class_names = class_names.map(&:to_s)
|
24
|
+
|
25
|
+
@calculators.delete_if { |n| class_names.include?(n) }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Tally
|
4
|
+
module Countable
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def increment_tally(key, by = 1)
|
9
|
+
return if new_record?
|
10
|
+
|
11
|
+
Tally.increment(key, self, by)
|
12
|
+
end
|
13
|
+
|
14
|
+
def tally_records(search_params = {})
|
15
|
+
if search_params.present?
|
16
|
+
RecordSearcher.search(search_params.merge(record: self))
|
17
|
+
else
|
18
|
+
Record.where(recordable: self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|