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