tricle 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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CONTRIBUTING.md +23 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +204 -0
- data/Rakefile +19 -0
- data/lib/tricle.rb +4 -0
- data/lib/tricle/abstract_method_error.rb +8 -0
- data/lib/tricle/email_helper.rb +66 -0
- data/lib/tricle/mail_preview.rb +10 -0
- data/lib/tricle/mailer.rb +75 -0
- data/lib/tricle/metric.rb +23 -0
- data/lib/tricle/presenters/group.rb +20 -0
- data/lib/tricle/presenters/list.rb +27 -0
- data/lib/tricle/presenters/metric.rb +51 -0
- data/lib/tricle/presenters/report.rb +40 -0
- data/lib/tricle/presenters/section.rb +9 -0
- data/lib/tricle/range_data.rb +27 -0
- data/lib/tricle/tasks.rb +19 -0
- data/lib/tricle/templates/email.css +47 -0
- data/lib/tricle/templates/email.html.erb +45 -0
- data/lib/tricle/templates/email.text.erb +2 -0
- data/lib/tricle/version.rb +3 -0
- data/screenshot.png +0 -0
- data/spec/app/group_test_mailer.rb +14 -0
- data/spec/app/list_test_mailer.rb +10 -0
- data/spec/app/test_mailer.rb +12 -0
- data/spec/app/test_metric.rb +39 -0
- data/spec/app/test_metric_with_long_name.rb +4 -0
- data/spec/app/uber_test_mailer.rb +20 -0
- data/spec/config/timecop.rb +7 -0
- data/spec/fixture_generator +18 -0
- data/spec/fixtures/weeks.csv +13 -0
- data/spec/presenters/group_spec.rb +17 -0
- data/spec/presenters/metric_spec.rb +42 -0
- data/spec/presenters/report_spec.rb +19 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/unit/abstract_method_error_spec.rb +16 -0
- data/spec/unit/email_helper_spec.rb +43 -0
- data/spec/unit/mail_preview_spec.rb +15 -0
- data/spec/unit/mailer_spec.rb +66 -0
- data/spec/unit/metric_spec.rb +18 -0
- data/spec/unit/range_data_spec.rb +24 -0
- data/spec/unit/test_metric_spec.rb +20 -0
- data/tricle.gemspec +37 -0
- metadata +324 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_support/core_ext/date/calculations'
|
2
|
+
require 'active_support/core_ext/numeric/time'
|
3
|
+
require_relative 'abstract_method_error'
|
4
|
+
|
5
|
+
module Tricle
|
6
|
+
class Metric
|
7
|
+
def title
|
8
|
+
self.class.name.titleize
|
9
|
+
end
|
10
|
+
|
11
|
+
def size_for_range(start_at, end_at)
|
12
|
+
self.items_for_range(start_at, end_at).size
|
13
|
+
end
|
14
|
+
|
15
|
+
def total
|
16
|
+
raise Tricle::AbstractMethodError.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def items_for_range(start_at, end_at)
|
20
|
+
raise Tricle::AbstractMethodError.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'metric'
|
2
|
+
require_relative 'section'
|
3
|
+
|
4
|
+
module Tricle
|
5
|
+
module Presenters
|
6
|
+
class Group < Section
|
7
|
+
attr_reader :metric_presenters, :title
|
8
|
+
|
9
|
+
def initialize(title=nil)
|
10
|
+
@title = title
|
11
|
+
@metric_presenters = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_metric(klass)
|
15
|
+
presenter = Tricle::Presenters::Metric.new(klass)
|
16
|
+
self.metric_presenters << presenter
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'section'
|
2
|
+
|
3
|
+
module Tricle
|
4
|
+
class List < Section
|
5
|
+
attr_reader :block, :metric
|
6
|
+
|
7
|
+
def initialize(klass, &block)
|
8
|
+
@metric = klass.new
|
9
|
+
@block = block
|
10
|
+
end
|
11
|
+
|
12
|
+
def title
|
13
|
+
self.metric.title
|
14
|
+
end
|
15
|
+
|
16
|
+
def items_markup(start_at, end_at)
|
17
|
+
markup = ''
|
18
|
+
items = self.metric.items_for_range(start_at, end_at)
|
19
|
+
items.each do |item|
|
20
|
+
val = self.block.call(item)
|
21
|
+
markup << %{<tr><td class="list-item" colspan="4">#{val}</td></tr>}
|
22
|
+
end
|
23
|
+
|
24
|
+
markup
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tricle
|
2
|
+
module Presenters
|
3
|
+
class Metric
|
4
|
+
attr_reader :metric, :now
|
5
|
+
|
6
|
+
def initialize(klass)
|
7
|
+
@metric = klass.new
|
8
|
+
# TODO allow Time to be passed in so it can be frozen
|
9
|
+
@now = Time.now
|
10
|
+
end
|
11
|
+
|
12
|
+
def title
|
13
|
+
self.metric.title
|
14
|
+
end
|
15
|
+
|
16
|
+
def total
|
17
|
+
self.metric.total
|
18
|
+
end
|
19
|
+
|
20
|
+
def days_ago(n)
|
21
|
+
start_at = self.now.beginning_of_day.days_ago(n)
|
22
|
+
end_at = start_at + 1.day
|
23
|
+
self.metric.size_for_range(start_at, end_at)
|
24
|
+
end
|
25
|
+
|
26
|
+
def yesterday
|
27
|
+
self.days_ago(1)
|
28
|
+
end
|
29
|
+
|
30
|
+
def weeks_ago(n)
|
31
|
+
start_at = self.now.beginning_of_week.weeks_ago(n)
|
32
|
+
end_at = start_at + 7.days
|
33
|
+
self.metric.size_for_range(start_at, end_at)
|
34
|
+
end
|
35
|
+
|
36
|
+
def last_week
|
37
|
+
self.weeks_ago(1)
|
38
|
+
end
|
39
|
+
|
40
|
+
def weeks_average(past_num_weeks)
|
41
|
+
weeks_range = 1..past_num_weeks
|
42
|
+
total = weeks_range.reduce(0){|sum, n| sum + self.weeks_ago(n) }
|
43
|
+
total.to_f / past_num_weeks
|
44
|
+
end
|
45
|
+
|
46
|
+
def week_average_this_quarter
|
47
|
+
self.weeks_average(13)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'group'
|
2
|
+
require_relative 'list'
|
3
|
+
|
4
|
+
# internal representation of the data displayed in the Mailer
|
5
|
+
module Tricle
|
6
|
+
module Presenters
|
7
|
+
class Report
|
8
|
+
attr_reader :sections
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@sections = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_section(section)
|
15
|
+
self.sections << section
|
16
|
+
section
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_group(title=nil)
|
20
|
+
group = Tricle::Presenters::Group.new(title)
|
21
|
+
self.add_section(group)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_metric(klass)
|
25
|
+
last_section = self.sections.last
|
26
|
+
unless last_section.is_a?(Tricle::Presenters::Group)
|
27
|
+
last_section = self.add_group
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO don't assume they want to add this metric to the last group?
|
31
|
+
last_section.add_metric(klass)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_list(klass, &block)
|
35
|
+
list = Tricle::List.new(klass, &block)
|
36
|
+
self.add_section(list)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# TODO add this class to the README?
|
2
|
+
module Tricle
|
3
|
+
class RangeData
|
4
|
+
def initialize
|
5
|
+
@data = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def add(key, val)
|
9
|
+
@data[key] ||= []
|
10
|
+
@data[key] << val
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_items
|
14
|
+
@data.values.flatten
|
15
|
+
end
|
16
|
+
|
17
|
+
def items_for_range(low, high)
|
18
|
+
@data.reduce([]) { |memo, (key, values)|
|
19
|
+
if key >= low && key < high
|
20
|
+
memo + values
|
21
|
+
else
|
22
|
+
memo
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/tricle/tasks.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'rake'
|
3
|
+
require_relative '../tricle'
|
4
|
+
|
5
|
+
namespace :tricle do
|
6
|
+
desc "Start a local server to preview your mailers"
|
7
|
+
task :preview do
|
8
|
+
require_relative 'mail_preview'
|
9
|
+
|
10
|
+
Rack::Server.start app: Tricle::MailPreview
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :emails do
|
14
|
+
desc "Send all emails"
|
15
|
+
task :send do
|
16
|
+
Tricle::Mailer.send_all
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
table {
|
2
|
+
border-collapse: collapse;
|
3
|
+
font-size: 16px;
|
4
|
+
line-height: 1.2;
|
5
|
+
}
|
6
|
+
|
7
|
+
th {
|
8
|
+
padding-left: 10px;
|
9
|
+
padding-right: 10px;
|
10
|
+
}
|
11
|
+
|
12
|
+
.section-heading {
|
13
|
+
font-size: 22px;
|
14
|
+
text-align: left;
|
15
|
+
}
|
16
|
+
|
17
|
+
th,
|
18
|
+
td {
|
19
|
+
padding: 10px;
|
20
|
+
text-align: right;
|
21
|
+
}
|
22
|
+
|
23
|
+
.date-range {
|
24
|
+
font-size: 13px;
|
25
|
+
}
|
26
|
+
|
27
|
+
td {
|
28
|
+
border: 1px solid #c7c7c7;
|
29
|
+
}
|
30
|
+
|
31
|
+
.list-item {
|
32
|
+
text-align: left;
|
33
|
+
}
|
34
|
+
|
35
|
+
td div:first-child {
|
36
|
+
font-size: 20px;
|
37
|
+
}
|
38
|
+
|
39
|
+
.positive {
|
40
|
+
background-color: #e4ffea;
|
41
|
+
color: green;
|
42
|
+
}
|
43
|
+
|
44
|
+
.negative {
|
45
|
+
background-color: #ffe3e4;
|
46
|
+
color: #c21717;
|
47
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<table>
|
8
|
+
<% @report.sections.each do |section| %>
|
9
|
+
<tr>
|
10
|
+
<th class="section-heading" colspan="4"><%= section.title %></th>
|
11
|
+
</tr>
|
12
|
+
<% if section.is_a?(Tricle::List) %>
|
13
|
+
<%= list_markup(section) %>
|
14
|
+
<% else %>
|
15
|
+
<tr>
|
16
|
+
<th><!--Title--></th>
|
17
|
+
<th>
|
18
|
+
<div>Last week</div>
|
19
|
+
<%= last_week_dates_cell %>
|
20
|
+
</th>
|
21
|
+
<th>
|
22
|
+
<div>Previous week</div>
|
23
|
+
<%= previous_week_dates_cell %>
|
24
|
+
</th>
|
25
|
+
<th>
|
26
|
+
<div>Quarterly average</div>
|
27
|
+
<%= quarter_dates_cell %>
|
28
|
+
</th>
|
29
|
+
</tr>
|
30
|
+
<% section.metric_presenters.each do |metric| %>
|
31
|
+
<tr>
|
32
|
+
<th class="metric-title"><%= metric.title %></th>
|
33
|
+
<td>
|
34
|
+
<div><%= number_with_delimiter(metric.last_week) %></div>
|
35
|
+
<div><%= number_with_delimiter(metric.total) %> (total)</div>
|
36
|
+
</td>
|
37
|
+
<%= percent_change_cell(metric.last_week, metric.weeks_ago(2)) %>
|
38
|
+
<%= percent_change_cell(metric.last_week, metric.week_average_this_quarter) %>
|
39
|
+
</tr>
|
40
|
+
<% end %>
|
41
|
+
<% end %>
|
42
|
+
<% end %>
|
43
|
+
</table>
|
44
|
+
</body>
|
45
|
+
</html>
|
data/screenshot.png
ADDED
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../../lib/tricle/mailer'
|
2
|
+
require_relative 'test_metric'
|
3
|
+
require_relative 'test_metric_with_long_name'
|
4
|
+
|
5
|
+
class GroupTestMailer < Tricle::Mailer
|
6
|
+
default(to: 'recipient1@test.com', from: 'sender@test.com')
|
7
|
+
|
8
|
+
group "Test Group 1" do
|
9
|
+
metric TestMetric
|
10
|
+
end
|
11
|
+
group "Test Group 2" do
|
12
|
+
metric TestMetricWithLongName
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require_relative '../../lib/tricle/metric'
|
3
|
+
require_relative '../../lib/tricle/range_data'
|
4
|
+
|
5
|
+
class TestMetric < Tricle::Metric
|
6
|
+
attr_accessor :data_by_start_on
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
self.load_data
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_data
|
14
|
+
filename = File.join(File.dirname(__FILE__), '..', 'fixtures', 'weeks.csv')
|
15
|
+
data = CSV.read(filename)
|
16
|
+
|
17
|
+
self.data_by_start_on = Tricle::RangeData.new
|
18
|
+
|
19
|
+
data.each do |row|
|
20
|
+
start_on = Date.parse(row[0])
|
21
|
+
val = row[2].to_i
|
22
|
+
self.data_by_start_on.add(start_on, val)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def size_for_range(start_at, end_at)
|
27
|
+
self.items_for_range(start_at, end_at).reduce(&:+)
|
28
|
+
end
|
29
|
+
|
30
|
+
def items_for_range(start_at, end_at)
|
31
|
+
start_on = start_at.to_date
|
32
|
+
end_on = end_at.to_date
|
33
|
+
self.data_by_start_on.items_for_range(start_on, end_on)
|
34
|
+
end
|
35
|
+
|
36
|
+
def total
|
37
|
+
self.data_by_start_on.all_items.reduce(&:+)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative '../../lib/tricle/mailer'
|
2
|
+
require_relative 'test_metric'
|
3
|
+
require_relative 'test_metric_with_long_name'
|
4
|
+
|
5
|
+
class UberTestMailer < Tricle::Mailer
|
6
|
+
default(to: 'recipient1@test.com', from: 'sender@test.com')
|
7
|
+
|
8
|
+
metric TestMetric
|
9
|
+
|
10
|
+
group "Test Group 1" do
|
11
|
+
metric TestMetric
|
12
|
+
end
|
13
|
+
group "Test Group 2" do
|
14
|
+
metric TestMetricWithLongName
|
15
|
+
end
|
16
|
+
|
17
|
+
list TestMetric do |val|
|
18
|
+
sprintf('%.1f', val)
|
19
|
+
end
|
20
|
+
end
|