tally 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|