tailor_made 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +6 -1
- data/README.md +126 -7
- data/lib/generators/tailor_made/dashboard/USAGE +8 -0
- data/lib/generators/tailor_made/dashboard/dashboard_generator.rb +44 -0
- data/lib/generators/tailor_made/dashboard/templates/controller.rb.erb +22 -0
- data/lib/generators/tailor_made/dashboard/templates/index.html.erb +158 -0
- data/lib/generators/tailor_made/dashboard/templates/query.rb.erb +18 -0
- data/lib/groupdate/group_alias.rb +5 -0
- data/lib/groupdate/relation_builder.rb +191 -0
- data/lib/tailor_made.rb +7 -0
- data/lib/tailor_made/methods.rb +71 -19
- data/lib/tailor_made/query.rb +34 -17
- data/lib/tailor_made/version.rb +1 -1
- data/tailor_made.gemspec +2 -0
- metadata +38 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9f46aa45e613f20e586a3f4ad9a87fbdc185c3a9d969885118556cc0c6f595f
|
4
|
+
data.tar.gz: 4025cca62e8d7565631f52e47b6be75f8c04ac42512f35138ca4f752716e5f22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 986def9764595200ed0d96a67d2816925ad881a1358beb85fb78561ed6b8bb97745792898ca0d5231ad82d1ab262bfbaa983de20b47927eee73ecc3dc04b6bdb
|
7
|
+
data.tar.gz: 7fe081b943bceb130ecd5f7b772a461c4f71fd190db4019169e01e87a528a0ebcec89cf867a12400bdc4ff31c4b5ed2486ab95b4af7e93edce0db5a77533f878
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
## TailorMade 0.2.0 (March 23, 2019) ##
|
2
|
+
|
3
|
+
* Add generators.
|
4
|
+
|
5
|
+
For example:
|
6
|
+
|
7
|
+
```
|
8
|
+
bin/rails g tailor_made:dashboard Ahoy::Visits
|
9
|
+
```
|
10
|
+
|
11
|
+
Generates a controller, query class and adds views.
|
12
|
+
|
13
|
+
*Pedro Carmona*
|
14
|
+
|
15
|
+
* Fix metaprogramming.
|
16
|
+
|
17
|
+
Was creating the arrays in the base query class, moved declaration to children.
|
18
|
+
|
19
|
+
*Pedro Carmona*
|
20
|
+
|
21
|
+
|
22
|
+
## TailorMade 0.1.0 (March 17, 2019) ##
|
23
|
+
|
24
|
+
* Initial source code
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tailor_made (0.
|
4
|
+
tailor_made (0.2.0)
|
5
5
|
activerecord (>= 4.2)
|
6
|
+
groupdate (~> 4.1.1)
|
7
|
+
pagy (>= 2.1.2)
|
6
8
|
railties (>= 4.2)
|
7
9
|
|
8
10
|
GEM
|
@@ -57,6 +59,8 @@ GEM
|
|
57
59
|
erubi (1.8.0)
|
58
60
|
globalid (0.4.2)
|
59
61
|
activesupport (>= 4.2.0)
|
62
|
+
groupdate (4.1.1)
|
63
|
+
activesupport (>= 4.2)
|
60
64
|
i18n (1.6.0)
|
61
65
|
concurrent-ruby (~> 1.0)
|
62
66
|
loofah (2.2.3)
|
@@ -74,6 +78,7 @@ GEM
|
|
74
78
|
nio4r (2.3.1)
|
75
79
|
nokogiri (1.10.1)
|
76
80
|
mini_portile2 (~> 2.4.0)
|
81
|
+
pagy (2.1.3)
|
77
82
|
rack (2.0.6)
|
78
83
|
rack-test (1.1.0)
|
79
84
|
rack (>= 1.0, < 3)
|
data/README.md
CHANGED
@@ -2,8 +2,24 @@
|
|
2
2
|
|
3
3
|
Currently in development.
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
Business intelligence for humans. This gem allows to create dashboards, based on query objects and plot one measure of the grouped data. Makes it easy for people without sql knowledge to explore data. Uses active record.
|
6
|
+
|
7
|
+
You will:
|
8
|
+
|
9
|
+
- Build and edit reports in minutes ([view usage](#usage))
|
10
|
+
|
11
|
+
You should:
|
12
|
+
|
13
|
+
- Build reports on top of materialized views for performance
|
14
|
+
- Create a dedicated rails app for analytics (blazer, tailor_made, smashing, scenic).
|
15
|
+
- Test your dashboards data, for visibility when they start failing
|
16
|
+
|
17
|
+
You could:
|
18
|
+
|
19
|
+
- And custom external data for correlation (advertising campaign spent, etc)
|
20
|
+
- Add input tables - pull data to input tables, or let it be pushed
|
21
|
+
- Join data in a materialized view in scenic. Refresh daily
|
22
|
+
|
7
23
|
|
8
24
|
## Installation
|
9
25
|
|
@@ -21,16 +37,119 @@ Or install it yourself as:
|
|
21
37
|
|
22
38
|
$ gem install tailor_made
|
23
39
|
|
40
|
+
Add pagy in app/helpers/application_helper.rb
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
module ApplicationHelper
|
44
|
+
include Pagy::Frontend
|
45
|
+
```
|
46
|
+
|
47
|
+
Two ways:
|
48
|
+
|
49
|
+
1. Add assets gems:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# gem "selectize-rails" # and follow its instruction
|
53
|
+
# gem "chartkick" # and follow its instruction
|
54
|
+
# gem "flatpickr" # include also rangePlugin
|
55
|
+
```
|
56
|
+
Then you need to add statments to application.scss.
|
57
|
+
|
58
|
+
Otherwise:
|
59
|
+
|
60
|
+
2. Webpacker packages
|
61
|
+
|
62
|
+
```
|
63
|
+
$ yarn add chartkick chart.js flatpickr selectize
|
64
|
+
```
|
65
|
+
|
66
|
+
2. Webpacker: /app/javascript/packs/application.js
|
67
|
+
|
68
|
+
```js
|
69
|
+
// tailor_made
|
70
|
+
import jquery from 'jquery'
|
71
|
+
import Chartkick from 'chartkick'
|
72
|
+
import Chart from 'chart.js'
|
73
|
+
import 'flatpickr'
|
74
|
+
import rangePlugin from 'flatpickr/dist/plugins/rangePlugin'
|
75
|
+
import "flatpickr/dist/flatpickr.css";
|
76
|
+
import 'selectize'
|
77
|
+
import "selectize/dist/css/selectize.css";
|
78
|
+
import "selectize/dist/css/selectize.bootstrap3.css";
|
79
|
+
Chartkick.addAdapter(Chart)
|
80
|
+
window.Chartkick = Chartkick
|
81
|
+
window.rangePlugin = rangePlugin
|
82
|
+
window.jquery = jquery
|
83
|
+
window.$ = jquery
|
84
|
+
```
|
85
|
+
|
86
|
+
2. Webpack: /app/assets/stylesheets/application.scss
|
87
|
+
|
88
|
+
```scss
|
89
|
+
@import "flatpickr/dist/flatpickr.css";
|
90
|
+
@import "selectize/dist/css/selectize.css";
|
91
|
+
@import "selectize/dist/css/selectize.bootstrap3.css";
|
92
|
+
```
|
24
93
|
## Usage
|
25
94
|
|
26
|
-
|
95
|
+
Create your first dashboard:
|
27
96
|
|
28
|
-
|
97
|
+
$ bin/rails g tailor_made:dashboard Ahoy::Visit
|
29
98
|
|
30
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
31
99
|
|
32
|
-
|
100
|
+
Then you can add the following statments to your query `rails_root/app/queries/tailor_made/ahoy/visit_query.rb`:
|
101
|
+
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
module TailorMade
|
105
|
+
class Ahoy::VisitQuery < TailorMade::Query
|
106
|
+
# creates attr_accessors for dimensions, measures and filters
|
107
|
+
include TailorMade::Methods
|
108
|
+
|
109
|
+
datetime_dimension :started_at, permit: [:day, :day_of_week, :day_of_month, :week, :month_of_year]
|
110
|
+
dimension(
|
111
|
+
:device_type,
|
112
|
+
domain: -> { Ahoy::Visits.all.pluck("DISTINCT device_type") }
|
113
|
+
)
|
114
|
+
dimension :referring_domain
|
115
|
+
dimension :utm_campaign
|
116
|
+
dimension :utm_content
|
117
|
+
dimension :utm_medium
|
118
|
+
dimension :utm_source
|
119
|
+
dimension :utm_term
|
120
|
+
measure :users_count, formula: "COUNT(user_id)"
|
121
|
+
measure :visits_count, formula: "COUNT(id)"
|
122
|
+
|
123
|
+
def default_dimensions
|
124
|
+
[:device_type]
|
125
|
+
end
|
126
|
+
|
127
|
+
def default_measures
|
128
|
+
[:visits_count, :users_count]
|
129
|
+
end
|
130
|
+
|
131
|
+
def initialize(attributes={})
|
132
|
+
super
|
133
|
+
@started_at_starts_at ||= Date.today.beginning_of_month
|
134
|
+
@started_at_ends_at ||= Date.today
|
135
|
+
end
|
136
|
+
|
137
|
+
def from
|
138
|
+
::Ahoy::Visit.all
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
Visit `http://localhost:3000/tailor_made/ahoy/visits`.
|
145
|
+
|
33
146
|
|
34
147
|
## Contributing
|
35
148
|
|
36
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
149
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/pedrocarmona/tailor_made.
|
150
|
+
|
151
|
+
## TODO:
|
152
|
+
|
153
|
+
- [ ] fix plot group by (n+1 queries not required).
|
154
|
+
- [ ] plot and selectize in different request (caching, etc)
|
155
|
+
- [ ] show row with totals (unique dimensions, sum without grouping)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "rails/generators/named_base"
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
class TailorMade::DashboardGenerator < Rails::Generators::NamedBase
|
5
|
+
source_root File.expand_path('templates', __dir__)
|
6
|
+
|
7
|
+
class_option :namespace, type: :string, default: "tailor_made"
|
8
|
+
|
9
|
+
def create_resource_query
|
10
|
+
queries_dir = Rails.root.join("app/queries/#{namespace}")
|
11
|
+
FileUtils.mkdir_p(queries_dir) unless File.directory?(queries_dir)
|
12
|
+
destination = Rails.root.join(
|
13
|
+
"app/queries/#{namespace}/#{regular_file_path}_query.rb",
|
14
|
+
)
|
15
|
+
|
16
|
+
template("query.rb.erb", destination)
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_resource_controller
|
20
|
+
destination = Rails.root.join(
|
21
|
+
"app/controllers/#{namespace}/#{regular_file_path.pluralize}_controller.rb",
|
22
|
+
)
|
23
|
+
|
24
|
+
template("controller.rb.erb", destination)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_resource_view
|
28
|
+
destination = Rails.root.join(
|
29
|
+
"app/views/#{namespace}/#{regular_file_path.pluralize}/index.html.erb",
|
30
|
+
)
|
31
|
+
|
32
|
+
copy_file("index.html.erb", destination)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def namespace
|
38
|
+
options['namespace']
|
39
|
+
end
|
40
|
+
|
41
|
+
def regular_file_path
|
42
|
+
(regular_class_path + [file_name]).map!(&:camelize).join("::").underscore
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module <%= namespace.classify %>
|
2
|
+
class <%= class_name.pluralize %>Controller < ApplicationController
|
3
|
+
include Pagy::Backend
|
4
|
+
|
5
|
+
before_action :set_query
|
6
|
+
|
7
|
+
def index
|
8
|
+
@pagy, @records = pagy(@query.all)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def set_query
|
14
|
+
@query = <%= namespace.classify %>::<%= class_name %>Query.new(query_params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def query_params
|
18
|
+
return {} if params[:q].nil?
|
19
|
+
params[:q].permit(<%= namespace.classify %>::<%= class_name %>Query.permitted_attributes)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
<div class="row col-md-12 mr-sm-auto col-lg-12 pt-5">
|
2
|
+
<main role="main" class="col-md-9 mr-sm-auto col-lg-10 px-4">
|
3
|
+
<%#= pie_chart @query.plot %>
|
4
|
+
<%= send(@query.chart, @query.plot) %>
|
5
|
+
|
6
|
+
<div class="row justify-content-center border-bottom">
|
7
|
+
<h4 class="h4 text-center"><%= @query.plot_measure.to_s.titleize %></h4>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<div class="table-responsive mt-4">
|
11
|
+
<table class="table table-striped table-sm">
|
12
|
+
<thead>
|
13
|
+
<tr>
|
14
|
+
<% @query.table_columns.each do |column| %>
|
15
|
+
<th><%= column %></th>
|
16
|
+
<% end %>
|
17
|
+
</tr>
|
18
|
+
</thead>
|
19
|
+
<tbody>
|
20
|
+
<% @records.each do |row| %>
|
21
|
+
<tr>
|
22
|
+
<% @query.table_columns.each do |column| %>
|
23
|
+
<% cell = @query.tabelize(row, column) %>
|
24
|
+
<th>
|
25
|
+
<% if cell.is_a?(Array) %>
|
26
|
+
<%= link_to(cell[0], cell[1]) %>
|
27
|
+
<% else %>
|
28
|
+
<%= cell %>
|
29
|
+
<% end %>
|
30
|
+
</th>
|
31
|
+
<% end %>
|
32
|
+
</tr>
|
33
|
+
<% end %>
|
34
|
+
</tbody>
|
35
|
+
</table>
|
36
|
+
</div>
|
37
|
+
<%= pagy_bootstrap_nav(@pagy).html_safe %>
|
38
|
+
</main>
|
39
|
+
|
40
|
+
<nav class="col-md-3 col-lg-2 ml-sm-auto d-md-block bg-light sidebar">
|
41
|
+
<div class="sidebar-sticky">
|
42
|
+
<h4 class="mb-3 pt-3">Filters</h4>
|
43
|
+
<%= form_for(@query, as: :q, url: url_for(only_path: true), method: :get) do |f| %>
|
44
|
+
<div class="form-group">
|
45
|
+
<%= f.label :chart %>
|
46
|
+
<%=
|
47
|
+
f.select(
|
48
|
+
:chart,
|
49
|
+
options_for_select(
|
50
|
+
@query.options_for_select[:chart],
|
51
|
+
@query.chart
|
52
|
+
),
|
53
|
+
{},
|
54
|
+
{ :class => 'selectize', :onchange => 'if(this.value.length>0)this.form.submit();' }
|
55
|
+
)
|
56
|
+
%>
|
57
|
+
</div>
|
58
|
+
<div class="form-group">
|
59
|
+
<%= f.label :plot_measure %>
|
60
|
+
<%=
|
61
|
+
f.select(
|
62
|
+
:plot_measure,
|
63
|
+
options_for_select(
|
64
|
+
@query.options_for_select[:plot_measure],
|
65
|
+
@query.plot_measure
|
66
|
+
),
|
67
|
+
{},
|
68
|
+
{ :class => 'selectize', :onchange => 'if(this.value.length>0)this.form.submit();' }
|
69
|
+
)
|
70
|
+
%>
|
71
|
+
</div>
|
72
|
+
<hr class="mb-4">
|
73
|
+
<% @query.class.tailor_made_canonical_dimensions.each do |dimension| %>
|
74
|
+
<div class="form-group">
|
75
|
+
<%= f.label "#{dimension}".to_sym %>
|
76
|
+
<% if @query.options_for_select["#{dimension}".to_sym].nil? %>
|
77
|
+
<%= f.text_field "#{dimension}".to_sym, class: "form-control", :value => @query.send("#{dimension}".to_sym).try(:iso8601) %>
|
78
|
+
<% else %>
|
79
|
+
<%=
|
80
|
+
f.select(
|
81
|
+
:host,
|
82
|
+
options_for_select(@query.options_for_select["#{dimension}".to_sym], @query.send("#{dimension}".to_sym)),
|
83
|
+
{ include_blank: true },
|
84
|
+
{ :class => 'selectize' }
|
85
|
+
)
|
86
|
+
%>
|
87
|
+
<% end %>
|
88
|
+
</div>
|
89
|
+
<% end %>
|
90
|
+
|
91
|
+
<% @query.class.tailor_made_datetime_columns.each do |dimension| %>
|
92
|
+
<div class="form-group">
|
93
|
+
<%= f.label "#{dimension}_starts_at".to_sym, class: "label-control" %>
|
94
|
+
<%= f.text_field "#{dimension}_starts_at".to_sym, class: "form-control datepicker", :value => @query.send("#{dimension}_starts_at".to_sym).try(:iso8601), "data-date-locale" => "#{I18n.locale}" %>
|
95
|
+
</div>
|
96
|
+
<div class="form-group">
|
97
|
+
<%= f.label "#{dimension}_ends_at".to_sym, class: "label-control" %>
|
98
|
+
<%= f.text_field "#{dimension}_ends_at".to_sym, class: "form-control datepicker", :value => @query.send("#{dimension}_ends_at".to_sym).try(:iso8601), "data-date-locale" => "#{I18n.locale}" %>
|
99
|
+
</div>
|
100
|
+
<% end %>
|
101
|
+
|
102
|
+
<div class="form-group">
|
103
|
+
<%= f.label :dimensions %>
|
104
|
+
<%=
|
105
|
+
f.select(
|
106
|
+
:dimensions,
|
107
|
+
options_for_select(@query.options_for_select[:dimensions], nil),
|
108
|
+
{},
|
109
|
+
{ :multiple => true, :class => 'custom-selectize' }
|
110
|
+
)
|
111
|
+
%>
|
112
|
+
</div>
|
113
|
+
<div class="form-group">
|
114
|
+
<%= f.label :measures %>
|
115
|
+
<%=
|
116
|
+
f.select(
|
117
|
+
:measures,
|
118
|
+
options_for_select(@query.options_for_select[:measures], nil),
|
119
|
+
{},
|
120
|
+
{ :multiple => true, :class => 'custom-selectize' }
|
121
|
+
)
|
122
|
+
%>
|
123
|
+
</div>
|
124
|
+
<hr class="mb-4">
|
125
|
+
<div class="form-group">
|
126
|
+
<%= f.submit 'Search', class: "btn btn-primary btn-lg btn-block" %>
|
127
|
+
</div>
|
128
|
+
<% end %>
|
129
|
+
</div>
|
130
|
+
</nav>
|
131
|
+
</div>
|
132
|
+
|
133
|
+
<script type="text/javascript">
|
134
|
+
document.addEventListener('turbolinks:load', function() {
|
135
|
+
<% @query.class.tailor_made_datetime_columns.each do |dimension| %>
|
136
|
+
flatpickr("#q_<%= dimension.to_s %>_starts_at", {
|
137
|
+
enableTime: false,
|
138
|
+
"plugins": [
|
139
|
+
new rangePlugin({ input: "#q_<%= dimension.to_s %>_ends_at" })
|
140
|
+
]
|
141
|
+
});
|
142
|
+
<% end %>
|
143
|
+
|
144
|
+
$('.selectize').selectize({
|
145
|
+
create: true
|
146
|
+
});
|
147
|
+
|
148
|
+
$('#q_dimensions').selectize({
|
149
|
+
create: true,
|
150
|
+
items: <%= @query.dimensions.to_json.html_safe %>
|
151
|
+
});
|
152
|
+
|
153
|
+
$('#q_measures').selectize({
|
154
|
+
create: true,
|
155
|
+
items: <%= @query.measures.to_json.html_safe %>
|
156
|
+
});
|
157
|
+
})
|
158
|
+
</script>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module <%= namespace.classify %>
|
2
|
+
class <%= class_name %>Query < TailorMade::Query
|
3
|
+
# creates attr_accessors for dimensions, measures and filters
|
4
|
+
include TailorMade::Methods
|
5
|
+
|
6
|
+
def default_dimensions
|
7
|
+
fail(NotImplementedError)
|
8
|
+
end
|
9
|
+
|
10
|
+
def default_measures
|
11
|
+
fail(NotImplementedError)
|
12
|
+
end
|
13
|
+
|
14
|
+
def from
|
15
|
+
fail(NotImplementedError)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module Groupdate
|
2
|
+
class RelationBuilder
|
3
|
+
attr_reader :period, :column, :day_start, :week_start
|
4
|
+
|
5
|
+
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:)
|
6
|
+
@relation = relation
|
7
|
+
@alias_name = [column, period].join("_")
|
8
|
+
@column = resolve_column(relation, column)
|
9
|
+
@period = period
|
10
|
+
@time_zone = time_zone
|
11
|
+
@time_range = time_range
|
12
|
+
@week_start = week_start
|
13
|
+
@day_start = day_start
|
14
|
+
|
15
|
+
if relation.default_timezone == :local
|
16
|
+
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate
|
21
|
+
clause = group_clause
|
22
|
+
clause.extend(::Groupdate::GroupAlias)
|
23
|
+
clause.relation = @relation.arel_table
|
24
|
+
clause.name = @alias_name
|
25
|
+
@relation.group(clause).where(*where_clause)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def group_clause
|
31
|
+
time_zone = @time_zone.tzinfo.name
|
32
|
+
adapter_name = @relation.connection.adapter_name
|
33
|
+
query =
|
34
|
+
case adapter_name
|
35
|
+
when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
|
36
|
+
case period
|
37
|
+
when :day_of_week
|
38
|
+
["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
|
39
|
+
when :hour_of_day
|
40
|
+
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
41
|
+
when :minute_of_hour
|
42
|
+
["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
|
43
|
+
when :day_of_month
|
44
|
+
["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
45
|
+
when :month_of_year
|
46
|
+
["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
47
|
+
when :week
|
48
|
+
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
|
49
|
+
when :quarter
|
50
|
+
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
|
51
|
+
else
|
52
|
+
format =
|
53
|
+
case period
|
54
|
+
when :second
|
55
|
+
"%Y-%m-%d %H:%i:%S"
|
56
|
+
when :minute
|
57
|
+
"%Y-%m-%d %H:%i:00"
|
58
|
+
when :hour
|
59
|
+
"%Y-%m-%d %H:00:00"
|
60
|
+
when :day
|
61
|
+
"%Y-%m-%d 00:00:00"
|
62
|
+
when :month
|
63
|
+
"%Y-%m-01 00:00:00"
|
64
|
+
else # year
|
65
|
+
"%Y-01-01 00:00:00"
|
66
|
+
end
|
67
|
+
|
68
|
+
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
|
69
|
+
end
|
70
|
+
when "PostgreSQL", "PostGIS"
|
71
|
+
case period
|
72
|
+
when :day_of_week
|
73
|
+
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
74
|
+
when :hour_of_day
|
75
|
+
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
76
|
+
when :minute_of_hour
|
77
|
+
["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
78
|
+
when :day_of_month
|
79
|
+
["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
80
|
+
when :month_of_year
|
81
|
+
["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
82
|
+
when :week # start on Sunday, not PostgreSQL default Monday
|
83
|
+
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
84
|
+
else
|
85
|
+
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
86
|
+
end
|
87
|
+
when "SQLite"
|
88
|
+
raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
|
89
|
+
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
90
|
+
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
91
|
+
|
92
|
+
if period == :week
|
93
|
+
["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
|
94
|
+
else
|
95
|
+
format =
|
96
|
+
case period
|
97
|
+
when :hour_of_day
|
98
|
+
"%H"
|
99
|
+
when :minute_of_hour
|
100
|
+
"%M"
|
101
|
+
when :day_of_week
|
102
|
+
"%w"
|
103
|
+
when :day_of_month
|
104
|
+
"%d"
|
105
|
+
when :month_of_year
|
106
|
+
"%m"
|
107
|
+
when :second
|
108
|
+
"%Y-%m-%d %H:%M:%S UTC"
|
109
|
+
when :minute
|
110
|
+
"%Y-%m-%d %H:%M:00 UTC"
|
111
|
+
when :hour
|
112
|
+
"%Y-%m-%d %H:00:00 UTC"
|
113
|
+
when :day
|
114
|
+
"%Y-%m-%d 00:00:00 UTC"
|
115
|
+
when :month
|
116
|
+
"%Y-%m-01 00:00:00 UTC"
|
117
|
+
when :quarter
|
118
|
+
raise Groupdate::Error, "Quarter not supported for SQLite"
|
119
|
+
else # year
|
120
|
+
"%Y-01-01 00:00:00 UTC"
|
121
|
+
end
|
122
|
+
|
123
|
+
["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
|
124
|
+
end
|
125
|
+
when "Redshift"
|
126
|
+
case period
|
127
|
+
when :day_of_week
|
128
|
+
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
129
|
+
when :hour_of_day
|
130
|
+
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
131
|
+
when :minute_of_hour
|
132
|
+
["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
133
|
+
when :day_of_month
|
134
|
+
["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
135
|
+
when :month_of_year
|
136
|
+
["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
137
|
+
when :week # start on Sunday, not Redshift default Monday
|
138
|
+
# Redshift does not return timezone information; it
|
139
|
+
# always says it is in UTC time, so we must convert
|
140
|
+
# back to UTC to play properly with the rest of Groupdate.
|
141
|
+
#
|
142
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
143
|
+
else
|
144
|
+
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
145
|
+
end
|
146
|
+
else
|
147
|
+
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
148
|
+
end
|
149
|
+
|
150
|
+
if adapter_name == "MySQL" && period == :week
|
151
|
+
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
152
|
+
end
|
153
|
+
|
154
|
+
clause = @relation.send(:sanitize_sql_array, query)
|
155
|
+
|
156
|
+
# cleaner queries in logs
|
157
|
+
clause = clean_group_clause_postgresql(clause)
|
158
|
+
clean_group_clause_mysql(clause)
|
159
|
+
end
|
160
|
+
|
161
|
+
def clean_group_clause_postgresql(clause)
|
162
|
+
clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
|
163
|
+
end
|
164
|
+
|
165
|
+
def clean_group_clause_mysql(clause)
|
166
|
+
clause = clause.gsub("DATE_SUB(#{column}, INTERVAL 0 second)", "#{column}")
|
167
|
+
if clause.start_with?("DATE_ADD(") && clause.end_with?(", INTERVAL 0 second)")
|
168
|
+
clause = clause[9..-21]
|
169
|
+
end
|
170
|
+
clause
|
171
|
+
end
|
172
|
+
|
173
|
+
def where_clause
|
174
|
+
if @time_range.is_a?(Range)
|
175
|
+
op = @time_range.exclude_end? ? "<" : "<="
|
176
|
+
["#{column} >= ? AND #{column} #{op} ?", @time_range.first, @time_range.last]
|
177
|
+
else
|
178
|
+
["#{column} IS NOT NULL"]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# resolves eagerly
|
183
|
+
# need to convert both where_clause (easy)
|
184
|
+
# and group_clause (not easy) if want to avoid this
|
185
|
+
def resolve_column(relation, column)
|
186
|
+
node = relation.send(:relation).send(:arel_columns, [column]).first
|
187
|
+
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
188
|
+
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
data/lib/tailor_made.rb
CHANGED
@@ -1,4 +1,11 @@
|
|
1
1
|
require "tailor_made/version"
|
2
|
+
|
3
|
+
require "groupdate"
|
4
|
+
require "pagy"
|
5
|
+
require 'pagy/extras/bootstrap'
|
6
|
+
|
7
|
+
require "groupdate/group_alias" # mockey patch
|
8
|
+
|
2
9
|
require "tailor_made/group_alias"
|
3
10
|
require "tailor_made/relation_alias"
|
4
11
|
require "tailor_made/methods"
|
data/lib/tailor_made/methods.rb
CHANGED
@@ -1,42 +1,44 @@
|
|
1
1
|
module TailorMade
|
2
2
|
module Methods
|
3
3
|
def self.included(base) # :nodoc:
|
4
|
-
const_set('CANONICAL_DIMENSIONS', [])
|
5
|
-
const_set('CANONICAL_DOMAIN', {})
|
6
|
-
const_set('CANONICAL_ANCHORS', {})
|
7
|
-
const_set('TAILOR_MADE_MEASURES', [])
|
8
|
-
const_set('TAILOR_MADE_MEASURE_FORMULA', {})
|
9
|
-
const_set('DATETIME_COLUMNS', [])
|
10
|
-
const_set('DATETIME_DIMENSIONS', {})
|
11
|
-
const_set('TAILOR_MADE_MEASURES_DATETIME_PERMITED', {})
|
12
4
|
base.extend ClassMethods
|
13
5
|
end
|
14
6
|
|
15
7
|
module ClassMethods
|
16
8
|
def dimension(*attributes)
|
17
9
|
dimension = attributes[0]
|
18
|
-
return if
|
19
|
-
|
10
|
+
return if tailor_made_canonical_dimensions.include?(dimension)
|
11
|
+
tailor_made_canonical_dimensions << dimension
|
20
12
|
|
21
13
|
attr_accessor dimension
|
22
|
-
|
23
|
-
|
14
|
+
tailor_made_canonical_domain[dimension] = attributes[1][:domain] if attributes[1] && attributes[1][:domain]
|
15
|
+
tailor_made_canonical_anchors[dimension] = attributes[1][:anchor] if attributes[1] && attributes[1][:anchor]
|
16
|
+
end
|
17
|
+
|
18
|
+
def filter(*attributes)
|
19
|
+
filter = attributes[0]
|
20
|
+
return if tailor_made_filters.include?(filter)
|
21
|
+
TAILOR_MADE_FILTERS << filter
|
22
|
+
|
23
|
+
attr_accessor filter
|
24
|
+
tailor_made_canonical_domain[dimension] = attributes[1][:domain] if attributes[1] && attributes[1][:domain]
|
25
|
+
tailor_made_canonical_anchors[dimension] = attributes[1][:anchor] if attributes[1] && attributes[1][:anchor]
|
24
26
|
end
|
25
27
|
|
26
28
|
def measure(*attributes)
|
27
29
|
measure = attributes[0]
|
28
|
-
return if
|
29
|
-
|
30
|
+
return if tailor_made_measures.include?(measure)
|
31
|
+
tailor_made_measures << measure
|
30
32
|
|
31
33
|
if attributes[1] && attributes[1][:formula]
|
32
|
-
|
34
|
+
tailor_made_measure_formula[measure] = attributes[1][:formula]
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
36
38
|
def datetime_dimension(*attributes)
|
37
39
|
dimension = attributes[0]
|
38
|
-
return if
|
39
|
-
|
40
|
+
return if tailor_made_datetime_columns.include?(dimension)
|
41
|
+
tailor_made_datetime_columns << dimension
|
40
42
|
attr_accessor "#{dimension.to_s}_starts_at".to_sym
|
41
43
|
attr_accessor "#{dimension.to_s}_ends_at".to_sym
|
42
44
|
|
@@ -46,13 +48,63 @@ module TailorMade
|
|
46
48
|
permit = Groupdate::PERIODS
|
47
49
|
end
|
48
50
|
|
49
|
-
|
51
|
+
tailor_made_measures_datetime_permited[dimension] = permit
|
50
52
|
|
51
|
-
|
53
|
+
tailor_made_datetime_dimensions[dimension] = permit.map do |period|
|
52
54
|
[dimension, period].join("_").to_sym
|
53
55
|
end
|
54
56
|
# groups (day, month, year..)
|
55
57
|
end
|
58
|
+
|
59
|
+
def permitted_attributes
|
60
|
+
[
|
61
|
+
:chart,
|
62
|
+
:plot_measure,
|
63
|
+
measures: [],
|
64
|
+
dimensions: []
|
65
|
+
] +
|
66
|
+
tailor_made_datetime_columns.map { |a| "#{a.to_s}_starts_at".to_sym } +
|
67
|
+
tailor_made_datetime_columns.map { |a| "#{a.to_s}_ends_at".to_sym } +
|
68
|
+
tailor_made_canonical_dimensions +
|
69
|
+
tailor_made_filters
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
def tailor_made_canonical_dimensions
|
74
|
+
@tailor_made_canonical_dimensions ||= []
|
75
|
+
end
|
76
|
+
|
77
|
+
def tailor_made_canonical_domain
|
78
|
+
@tailor_made_canonical_domain ||= {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def tailor_made_canonical_anchors
|
82
|
+
@tailor_made_canonical_anchors ||= {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def tailor_made_measures
|
86
|
+
@tailor_made_measures ||= []
|
87
|
+
end
|
88
|
+
|
89
|
+
def tailor_made_measure_formula
|
90
|
+
@tailor_made_measure_formula ||= {}
|
91
|
+
end
|
92
|
+
|
93
|
+
def tailor_made_datetime_columns
|
94
|
+
@tailor_made_datetime_columns ||= []
|
95
|
+
end
|
96
|
+
|
97
|
+
def tailor_made_datetime_dimensions
|
98
|
+
@tailor_made_datetime_dimensions ||= {}
|
99
|
+
end
|
100
|
+
|
101
|
+
def tailor_made_measures_datetime_permited
|
102
|
+
@tailor_made_measures_datetime_permited ||= {}
|
103
|
+
end
|
104
|
+
|
105
|
+
def tailor_made_filters
|
106
|
+
@tailor_made_filters ||= []
|
107
|
+
end
|
56
108
|
end
|
57
109
|
end
|
58
110
|
end
|
data/lib/tailor_made/query.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
module TailorMade
|
2
2
|
class Query
|
3
3
|
include ActiveModel::Model
|
4
|
-
include TailorMade::Methods # creates accesors for dimensions and measures
|
5
4
|
|
6
5
|
attr_accessor :measures
|
7
6
|
attr_accessor :dimensions
|
@@ -30,7 +29,6 @@ module TailorMade
|
|
30
29
|
@plot_measure ||= measures.first
|
31
30
|
|
32
31
|
set_datetime_ranges
|
33
|
-
@scope = build_scope(from, dimensions)
|
34
32
|
end
|
35
33
|
|
36
34
|
def from
|
@@ -38,7 +36,21 @@ module TailorMade
|
|
38
36
|
end
|
39
37
|
|
40
38
|
def options_for_select
|
41
|
-
|
39
|
+
@options_for_select ||= begin
|
40
|
+
options = {
|
41
|
+
domain: self.class.tailor_made_canonical_dimensions + self.class.tailor_made_datetime_dimensions.values().flatten,
|
42
|
+
dimensions: self.class.tailor_made_canonical_dimensions + self.class.tailor_made_datetime_dimensions.values().flatten,
|
43
|
+
measures: self.class.tailor_made_measures,
|
44
|
+
plot_measure: self.class.tailor_made_measures,
|
45
|
+
chart: CHARTS
|
46
|
+
}
|
47
|
+
self.class.tailor_made_canonical_domain.each do |field|
|
48
|
+
if self.class.tailor_made_canonical_domain[field]
|
49
|
+
options = options.merge({ field => self.class.tailor_made_canonical_domain[field].call })
|
50
|
+
end
|
51
|
+
end
|
52
|
+
options
|
53
|
+
end
|
42
54
|
end
|
43
55
|
|
44
56
|
def chart
|
@@ -46,6 +58,7 @@ module TailorMade
|
|
46
58
|
end
|
47
59
|
|
48
60
|
def plot
|
61
|
+
scope = build_scope(from, dimensions)
|
49
62
|
result = scope.order(order).pluck(plot_formulas(dimensions, scope))
|
50
63
|
return result if dimensions.size < 2
|
51
64
|
result.map { |row| row[1...-1] }.uniq.map { |combination|
|
@@ -72,6 +85,7 @@ module TailorMade
|
|
72
85
|
end
|
73
86
|
|
74
87
|
def all
|
88
|
+
@scope = build_scope(from, dimensions)
|
75
89
|
scope.order(order).select(table_formulas)
|
76
90
|
end
|
77
91
|
|
@@ -84,16 +98,14 @@ module TailorMade
|
|
84
98
|
end
|
85
99
|
end
|
86
100
|
|
87
|
-
|
88
|
-
|
89
101
|
def tabelize(row, column)
|
90
|
-
if
|
102
|
+
if self.class.tailor_made_canonical_anchors[column].nil?
|
91
103
|
row.send(column)
|
92
104
|
else
|
93
|
-
if
|
94
|
-
|
105
|
+
if self.class.tailor_made_canonical_anchors[column].respond_to? :call
|
106
|
+
self.class.tailor_made_canonical_anchors[column] = self.class.tailor_made_canonical_anchors[column].call
|
95
107
|
end
|
96
|
-
result =
|
108
|
+
result = self.class.tailor_made_canonical_anchors[column][row.send(column)]
|
97
109
|
result = [row.send(column), result] if result
|
98
110
|
result ||= row.send(column)
|
99
111
|
end
|
@@ -106,7 +118,7 @@ module TailorMade
|
|
106
118
|
|
107
119
|
scope = build_datetime_dimensions_scope(scope, dimensions)
|
108
120
|
|
109
|
-
datetime_dimensions =
|
121
|
+
datetime_dimensions = self.class.tailor_made_datetime_dimensions.values().flatten
|
110
122
|
unless (dimensions - datetime_dimensions).empty?
|
111
123
|
scope = scope.group(dimensions - datetime_dimensions)
|
112
124
|
end
|
@@ -114,8 +126,13 @@ module TailorMade
|
|
114
126
|
end
|
115
127
|
|
116
128
|
def build_canonical_scope(scope)
|
117
|
-
|
118
|
-
|
129
|
+
self.class.tailor_made_canonical_dimensions.each do |dimension|
|
130
|
+
next if send(dimension).nil?
|
131
|
+
scope = scope.where(
|
132
|
+
':dimension LIKE :pattern',
|
133
|
+
dimension: dimension,
|
134
|
+
pattern: "%#{send(dimension)}%"
|
135
|
+
)
|
119
136
|
end
|
120
137
|
scope
|
121
138
|
end
|
@@ -123,7 +140,7 @@ module TailorMade
|
|
123
140
|
# Datetime
|
124
141
|
|
125
142
|
def build_datetime_dimensions_scope(scope, dimensions)
|
126
|
-
|
143
|
+
self.class.tailor_made_datetime_columns.each do |dimension|
|
127
144
|
scope = build_datetime_dimension_scope(scope, dimension, dimensions)
|
128
145
|
end
|
129
146
|
scope
|
@@ -132,7 +149,7 @@ module TailorMade
|
|
132
149
|
def build_datetime_dimension_scope(scope, dimension, dimensions)
|
133
150
|
starts_at = instance_variable_get("@#{dimension}_starts_at")
|
134
151
|
ends_at = instance_variable_get("@#{dimension}_ends_at")
|
135
|
-
permit =
|
152
|
+
permit = self.class.tailor_made_measures_datetime_permited[dimension]
|
136
153
|
|
137
154
|
if !starts_at.blank? && !ends_at.blank?
|
138
155
|
scope = scope.where(dimension.to_sym => starts_at..ends_at)
|
@@ -147,7 +164,7 @@ module TailorMade
|
|
147
164
|
|
148
165
|
def datetime_dimension_periods(datetime_dimension, dimensions)
|
149
166
|
dimensions.select { |dimension|
|
150
|
-
|
167
|
+
self.class.tailor_made_datetime_dimensions[datetime_dimension].include?(dimension)
|
151
168
|
}
|
152
169
|
end
|
153
170
|
|
@@ -156,7 +173,7 @@ module TailorMade
|
|
156
173
|
end
|
157
174
|
|
158
175
|
def set_datetime_ranges
|
159
|
-
|
176
|
+
self.class.tailor_made_datetime_columns.each do |dimension|
|
160
177
|
set_datetime_range(dimension)
|
161
178
|
end
|
162
179
|
end
|
@@ -311,7 +328,7 @@ module TailorMade
|
|
311
328
|
|
312
329
|
def measure_formulas(measures)
|
313
330
|
measures.map { |measure|
|
314
|
-
[
|
331
|
+
[self.class.tailor_made_measure_formula[measure], measure].join(" AS ")
|
315
332
|
}
|
316
333
|
end
|
317
334
|
|
data/lib/tailor_made/version.rb
CHANGED
data/tailor_made.gemspec
CHANGED
@@ -26,6 +26,8 @@ Gem::Specification.new do |spec|
|
|
26
26
|
|
27
27
|
spec.add_dependency "railties", ">= 4.2"
|
28
28
|
spec.add_dependency "activerecord", ">= 4.2"
|
29
|
+
spec.add_dependency "pagy", ">= 2.1.2"
|
30
|
+
spec.add_dependency "groupdate", "~> 4.1.1"
|
29
31
|
|
30
32
|
spec.add_development_dependency "bundler", "~> 1.16"
|
31
33
|
spec.add_development_dependency "rake", "~> 10.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tailor_made
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Carmona
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-03-
|
11
|
+
date: 2019-03-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -38,6 +38,34 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pagy
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.1.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.1.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: groupdate
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 4.1.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 4.1.1
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
70
|
name: bundler
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,6 +132,7 @@ files:
|
|
104
132
|
- ".gitignore"
|
105
133
|
- ".rspec"
|
106
134
|
- ".travis.yml"
|
135
|
+
- CHANGELOG.md
|
107
136
|
- Gemfile
|
108
137
|
- Gemfile.lock
|
109
138
|
- LICENSE.txt
|
@@ -111,6 +140,13 @@ files:
|
|
111
140
|
- Rakefile
|
112
141
|
- bin/console
|
113
142
|
- bin/setup
|
143
|
+
- lib/generators/tailor_made/dashboard/USAGE
|
144
|
+
- lib/generators/tailor_made/dashboard/dashboard_generator.rb
|
145
|
+
- lib/generators/tailor_made/dashboard/templates/controller.rb.erb
|
146
|
+
- lib/generators/tailor_made/dashboard/templates/index.html.erb
|
147
|
+
- lib/generators/tailor_made/dashboard/templates/query.rb.erb
|
148
|
+
- lib/groupdate/group_alias.rb
|
149
|
+
- lib/groupdate/relation_builder.rb
|
114
150
|
- lib/tailor_made.rb
|
115
151
|
- lib/tailor_made/group_alias.rb
|
116
152
|
- lib/tailor_made/methods.rb
|