tally 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +217 -0
  3. data/Rakefile +32 -0
  4. data/app/assets/config/tally_manifest.js +2 -0
  5. data/app/assets/javascripts/tally/application.js +15 -0
  6. data/app/assets/stylesheets/tally/application.css +15 -0
  7. data/app/controllers/tally/application_controller.rb +17 -0
  8. data/app/controllers/tally/days_controller.rb +19 -0
  9. data/app/controllers/tally/keys_controller.rb +19 -0
  10. data/app/controllers/tally/records_controller.rb +21 -0
  11. data/app/helpers/tally/application_helper.rb +4 -0
  12. data/app/helpers/tally/increment_helper.rb +15 -0
  13. data/app/jobs/tally/application_job.rb +4 -0
  14. data/app/jobs/tally/calculator_runner_job.rb +12 -0
  15. data/app/models/tally/application_record.rb +7 -0
  16. data/app/models/tally/record.rb +30 -0
  17. data/app/presenters/tally/record_presenter.rb +29 -0
  18. data/bin/annotate +29 -0
  19. data/bin/appraisal +29 -0
  20. data/bin/bundle +114 -0
  21. data/bin/coderay +29 -0
  22. data/bin/htmldiff +29 -0
  23. data/bin/ldiff +29 -0
  24. data/bin/nokogiri +29 -0
  25. data/bin/pry +29 -0
  26. data/bin/rackup +29 -0
  27. data/bin/rails +14 -0
  28. data/bin/rake +29 -0
  29. data/bin/rspec +29 -0
  30. data/bin/rubocop +29 -0
  31. data/bin/ruby-parse +29 -0
  32. data/bin/ruby-rewrite +29 -0
  33. data/bin/sprockets +29 -0
  34. data/bin/thor +29 -0
  35. data/config/routes.rb +12 -0
  36. data/db/migrate/20181016172358_create_tally_records.rb +15 -0
  37. data/lib/tally.rb +77 -0
  38. data/lib/tally/archiver.rb +64 -0
  39. data/lib/tally/calculator.rb +30 -0
  40. data/lib/tally/calculator_runner.rb +56 -0
  41. data/lib/tally/calculators.rb +29 -0
  42. data/lib/tally/countable.rb +23 -0
  43. data/lib/tally/daily.rb +64 -0
  44. data/lib/tally/engine.rb +28 -0
  45. data/lib/tally/increment.rb +25 -0
  46. data/lib/tally/key_finder.rb +123 -0
  47. data/lib/tally/key_finder/entry.rb +66 -0
  48. data/lib/tally/keyable.rb +55 -0
  49. data/lib/tally/record_searcher.rb +110 -0
  50. data/lib/tally/sweeper.rb +46 -0
  51. data/lib/tally/version.rb +5 -0
  52. data/lib/tasks/tally_tasks.rake +16 -0
  53. metadata +256 -0
@@ -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")
@@ -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")
@@ -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")
@@ -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")
@@ -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")
@@ -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
@@ -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