metricky 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e086c84c42383acedfc1764f23062f6641aeb60
4
+ data.tar.gz: f0593fe336c19a310c77e5b369752ae86945fa3f
5
+ SHA512:
6
+ metadata.gz: 2a3899d310416542e06a3b670686c3124508e1324efd4447f653cce260c7160b7b55b116fc3695ea4148776a3d360615ed04ade87dea38ffb1c623289f57e2a2
7
+ data.tar.gz: 726d8a8d650ec84b9b714720717b9d9c94c18438c7e10041811de51444314ce628cdd56b839f91220b26bf80011df67b76bd0e9046eabb143a12a802ed75a123
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Josh Brody
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # Metricky
2
+ Display metrics about your database in your application. Depends on the awesome [Chartkick](https://github.com/ankane/chartkick) and [Groupdate](https://github.com/ankane/groupdate).
3
+
4
+ ## Usage
5
+
6
+ Make this in Ruby:
7
+
8
+ <img src="https://i.imgur.com/PQhFyAE.png" alt="Metricky example">
9
+
10
+ In your view where you want to display the metric:
11
+
12
+ ```erbruby
13
+ render_metric :users
14
+ ```
15
+
16
+ In `app/metrics/users_metric.rb`
17
+
18
+ ```ruby
19
+ class UsersMetric < Metricky::Base
20
+ def scope
21
+ User
22
+ end
23
+
24
+ def type
25
+ :count
26
+ end
27
+
28
+ def columns
29
+ :id
30
+ end
31
+
32
+ def trend
33
+ :day
34
+ end
35
+ end
36
+ ```
37
+
38
+ ## Installation
39
+ Add this line to your application's Gemfile:
40
+
41
+ ```ruby
42
+ gem 'metricky'
43
+ ```
44
+
45
+ And then execute:
46
+ ```bash
47
+ $ bundle
48
+ ```
49
+
50
+ Or install it yourself as:
51
+ ```bash
52
+ $ gem install metricky
53
+ ```
54
+
55
+ Then drop in Chartkick into your `application.js` (or similar):
56
+
57
+ ```javascript
58
+ //= require chartkick
59
+ ```
60
+
61
+ ## Customizing
62
+
63
+ ~~Blatantly ripped from~~ Super-inspired by Laravel Nova's metric system.
64
+
65
+ ### Value to calculate
66
+
67
+ In your metric, define columns:
68
+
69
+ ```ruby
70
+ def columns
71
+ :id
72
+ end
73
+ ```
74
+
75
+ ### Grouping
76
+
77
+ In your metric, define what trend:
78
+
79
+ ```ruby
80
+ def trend
81
+ :day
82
+ end
83
+ ```
84
+
85
+ This can be any one of `Groupdate::PERIODS`
86
+
87
+ Define what column should be used:
88
+
89
+ ```ruby
90
+ def trend_column
91
+ :created_at
92
+ end
93
+ ```
94
+
95
+ ### Value type
96
+
97
+ In your metric, define what type:
98
+
99
+ ```ruby
100
+ def type
101
+ :count
102
+ end
103
+ ```
104
+
105
+ This can be any one of `:min, :max, :sum, :count`
106
+
107
+ ### Ranges
108
+
109
+ In your metric, define what ranges as a class method:
110
+
111
+ ```ruby
112
+ def self.ranges
113
+ {
114
+ 'all' => 'All',
115
+ '30' => '30 Days',
116
+ '60' => '60 Days',
117
+ '365' => '365 Days',
118
+ }
119
+ end
120
+ ```
121
+
122
+ And then their corresponding values (instance-level):
123
+
124
+ ```ruby
125
+ def range_value
126
+ case range
127
+ when 'all'
128
+ nil
129
+ when '30'
130
+ 30.days.ago
131
+ when '60'
132
+ 60.days.ago
133
+ when '365'
134
+ 365.days.ago
135
+ end
136
+ end
137
+ ```
138
+
139
+ This is used with a `where` query against the `range_column`.
140
+
141
+ ```ruby
142
+ def range_column
143
+ :created_at
144
+ end
145
+ ```
146
+
147
+ ### Partial
148
+
149
+ In your metric, define the partial path:
150
+
151
+ ```ruby
152
+ def to_partial_path
153
+ "shared/metric"
154
+ end
155
+ ```
156
+
157
+ Take a look at `app/views/metricky/_metric.html.erb` for implementation.
158
+
159
+ ## Contributing
160
+ Add stuff.
161
+
162
+ ## License
163
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Metricky'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,24 @@
1
+ <div class="card card-body">
2
+ <%= metric_form_for metric do |f| %>
3
+ <div class="header-elements-inline">
4
+ <div>
5
+ <h5><%= metric.title %></h5>
6
+ </div>
7
+ <% if metric.class.ranges.any? %>
8
+ <div>
9
+ <%= f.select :range, metric.class.ranges.invert, { selected: metric.range }, { class: "form-control-sm", name: "#{metric.form_name}[range]", onchange: "this.form.submit()" } %>
10
+ </div>
11
+ <% end %>
12
+ </div>
13
+ <div class="media">
14
+ <div class="media-body">
15
+ <% if metric.results.is_a?(Hash) %>
16
+ <%= metricky_chart(metric) %>
17
+ <% else %>
18
+ <h3 class="font-weight-semibold mb-0"><%= metric.results %></h3>
19
+ <% end %>
20
+ <span class="text-uppercase font-size-sm text-muted"><%= metric.subtitle %></span>
21
+ </div>
22
+ </div>
23
+ <% end %>
24
+ </div>
@@ -0,0 +1,173 @@
1
+ module Metricky
2
+ class Base
3
+ VALID_TYPES = [:sum, :max, :min, :average, :count].freeze
4
+ attr_reader :params
5
+ def initialize(params)
6
+ @params = params
7
+ @query = nil
8
+ end
9
+
10
+ # Must be one of Chartkick charts
11
+ # line_chart, pie_chart, columnchart, bar_chart, areachart, geo_chart, timeline
12
+ def chart
13
+ :column_chart
14
+ end
15
+
16
+ # What partial is rendered.
17
+ def to_partial_path
18
+ '/metricky/metric'
19
+ end
20
+
21
+ def noun
22
+ case type.to_sym
23
+ when :count
24
+ "total number of"
25
+ when :average
26
+ "average #{columns}"
27
+ when :sum
28
+ "total"
29
+ when :min
30
+ "minimum"
31
+ when :max
32
+ "maximum"
33
+ end
34
+ end
35
+
36
+ # List of ranges and their values. Used (inverted) on the form.
37
+ def self.ranges
38
+ {
39
+ 'all' => 'All',
40
+ 'Today' => 'Today',
41
+ '30' => '30 Days',
42
+ '60' => '60 Days',
43
+ '365' => '365 Days',
44
+ 'WTD' => 'WTD',
45
+ 'MTD' => 'MTD',
46
+ 'YTD' => 'YTD',
47
+ }
48
+ end
49
+
50
+ # Actual result of the metric
51
+ def results
52
+ assets
53
+ end
54
+
55
+ def self.metric_name
56
+ name.demodulize.sub(/Metric$/, "")
57
+ end
58
+
59
+ def title
60
+ self.class.metric_name
61
+ end
62
+
63
+ def subtitle
64
+ "#{noun} #{scope.model_name.human.pluralize}"
65
+ end
66
+
67
+ # Form key
68
+ def form_name
69
+ "#{uri_key}_metric"
70
+ end
71
+
72
+ # Param key
73
+ def uri_key
74
+ self.class.metric_name.tableize
75
+ end
76
+
77
+ # Converts range string to a Ruby object
78
+ def range_value
79
+ case range
80
+ when nil
81
+ nil
82
+ when 'all'
83
+ nil
84
+ when '30'
85
+ 30.days.ago
86
+ when '60'
87
+ 60.days.ago
88
+ when '365'
89
+ 365.days.ago
90
+ when 'WTD'
91
+ DateTime.now.beginning_of_week
92
+ when 'MTD'
93
+ DateTime.now.beginning_of_month
94
+ when 'YTD'
95
+ DateTime.now.beginning_of_year
96
+ when 'Today'
97
+ DateTime.now.beginning_of_day
98
+ else
99
+ raise TypeError, "unknown range_value for range #{range}. Please define it on range_value"
100
+ end
101
+ end
102
+
103
+ # What ActiveRecord class (or scoped class) is being used for the metric
104
+ def scope
105
+ raise NotImplementedError, "please add a scope to your metric."
106
+ end
107
+
108
+ # What kind of metric we're pulling.
109
+ #
110
+ # Must be one of :sum, :max, :min, :average, :count
111
+ def type
112
+ :count
113
+ end
114
+
115
+ # Column(s) to perform the calculation on.
116
+ # [:total_in_cents, :department]
117
+ def columns
118
+ 'id'
119
+ end
120
+
121
+ # How it's grouped. Leave nil if no grouping
122
+ #
123
+ # [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week,
124
+ # :hour_of_day, :minute_of_hour, :day_of_month, :month_of_year]
125
+ def trend
126
+ nil
127
+ end
128
+
129
+ # What column to specify for the range calculation. Normally `created_at`
130
+ def range_column
131
+ 'created_at'
132
+ end
133
+
134
+ # What column to specify for the trend calculation. Normally `created_at`
135
+ def trend_column
136
+ 'created_at'
137
+ end
138
+
139
+ def range
140
+ params.dig(form_name, :range)
141
+ end
142
+
143
+ private
144
+
145
+ def trend?
146
+ trend.present?
147
+ end
148
+
149
+ def assets
150
+ if range_value != nil
151
+ @query = scope.where("#{range_column} > ?", range_value)
152
+ else
153
+ @query = scope
154
+ end
155
+ if trend? && valid_trend?
156
+ @query = @query.group_by_period(trend, trend_column)
157
+ end
158
+ @query = @query.send(type, *columns)
159
+ @query
160
+ end
161
+
162
+ def valid_trend?
163
+ return true if Groupdate::PERIODS.include?(trend.to_sym)
164
+ raise NameError, "trend must be one of #{Groupdate::PERIODS}. It is #{trend}."
165
+ end
166
+
167
+ def check_type
168
+ unless VALID_TYPES.include?(type.to_sym)
169
+ raise NameError, "#{type} must be one of :sum, :max, :min, :average, :count"
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,7 @@
1
+ require 'rails'
2
+
3
+ module Metricky
4
+ class Engine < Rails::Engine
5
+ isolate_namespace Metricky
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ module Metricky
2
+ module Helper
3
+ def metric_form_for(metric)
4
+ form_for("#{request.path}", as: metric.form_name.to_sym, method: :get) do |f|
5
+ yield(f)
6
+ end
7
+ end
8
+
9
+ def render_metric(metric_name)
10
+ if metric_name.is_a?(String) || metric_name.is_a?(Symbol)
11
+ metric_name = metric_name.to_s
12
+ if metric_name.end_with?("Metric")
13
+ metric = metric_name.safe_constantize
14
+ else
15
+ klass_name = "#{metric_name.to_s}Metric".camelize
16
+ metric = "Metrics::#{klass_name}".safe_constantize
17
+ end
18
+ raise NameError, "#{metric_name} does not exist" unless metric
19
+ else
20
+ metric = metric_name
21
+ end
22
+ metric = metric.new(params)
23
+ render metric, metric: metric
24
+ end
25
+
26
+ def metricky_chart(metric)
27
+ send(metric.chart, metric.results)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module Metricky
2
+ VERSION = '0.1.0'
3
+ end
data/lib/metricky.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "groupdate"
2
+ require "chartkick"
3
+ require "metricky/base"
4
+ require "metricky/helper"
5
+
6
+ require "metricky/engine"
7
+
8
+ module Metricky
9
+ end
10
+
11
+ ActiveSupport.on_load(:action_view) do
12
+ include Metricky::Helper
13
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :metricky do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metricky
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Brody
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: groupdate
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: chartkick
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Metricky
56
+ email:
57
+ - josh@josh.mn
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - app/views/metricky/_metric.html.erb
66
+ - lib/metricky.rb
67
+ - lib/metricky/base.rb
68
+ - lib/metricky/engine.rb
69
+ - lib/metricky/helper.rb
70
+ - lib/metricky/version.rb
71
+ - lib/tasks/metricky_tasks.rake
72
+ homepage: https://github.com/joshmn/metricky
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 2.6.13
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: A simple stats.
96
+ test_files: []