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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CONTRIBUTING.md +23 -0
  6. data/Gemfile +4 -0
  7. data/Guardfile +8 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +204 -0
  10. data/Rakefile +19 -0
  11. data/lib/tricle.rb +4 -0
  12. data/lib/tricle/abstract_method_error.rb +8 -0
  13. data/lib/tricle/email_helper.rb +66 -0
  14. data/lib/tricle/mail_preview.rb +10 -0
  15. data/lib/tricle/mailer.rb +75 -0
  16. data/lib/tricle/metric.rb +23 -0
  17. data/lib/tricle/presenters/group.rb +20 -0
  18. data/lib/tricle/presenters/list.rb +27 -0
  19. data/lib/tricle/presenters/metric.rb +51 -0
  20. data/lib/tricle/presenters/report.rb +40 -0
  21. data/lib/tricle/presenters/section.rb +9 -0
  22. data/lib/tricle/range_data.rb +27 -0
  23. data/lib/tricle/tasks.rb +19 -0
  24. data/lib/tricle/templates/email.css +47 -0
  25. data/lib/tricle/templates/email.html.erb +45 -0
  26. data/lib/tricle/templates/email.text.erb +2 -0
  27. data/lib/tricle/version.rb +3 -0
  28. data/screenshot.png +0 -0
  29. data/spec/app/group_test_mailer.rb +14 -0
  30. data/spec/app/list_test_mailer.rb +10 -0
  31. data/spec/app/test_mailer.rb +12 -0
  32. data/spec/app/test_metric.rb +39 -0
  33. data/spec/app/test_metric_with_long_name.rb +4 -0
  34. data/spec/app/uber_test_mailer.rb +20 -0
  35. data/spec/config/timecop.rb +7 -0
  36. data/spec/fixture_generator +18 -0
  37. data/spec/fixtures/weeks.csv +13 -0
  38. data/spec/presenters/group_spec.rb +17 -0
  39. data/spec/presenters/metric_spec.rb +42 -0
  40. data/spec/presenters/report_spec.rb +19 -0
  41. data/spec/spec_helper.rb +35 -0
  42. data/spec/unit/abstract_method_error_spec.rb +16 -0
  43. data/spec/unit/email_helper_spec.rb +43 -0
  44. data/spec/unit/mail_preview_spec.rb +15 -0
  45. data/spec/unit/mailer_spec.rb +66 -0
  46. data/spec/unit/metric_spec.rb +18 -0
  47. data/spec/unit/range_data_spec.rb +24 -0
  48. data/spec/unit/test_metric_spec.rb +20 -0
  49. data/tricle.gemspec +37 -0
  50. 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,9 @@
1
+ require_relative '../abstract_method_error'
2
+
3
+ module Tricle
4
+ class Section
5
+ def title
6
+ raise Tricle::AbstractMethodError.new
7
+ end
8
+ end
9
+ 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
@@ -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>
@@ -0,0 +1,2 @@
1
+ You should be seeing the HTML email. If this seems to be in error, please file a bug:
2
+ https://github.com/artsy/tricle/issues
@@ -0,0 +1,3 @@
1
+ module Tricle
2
+ VERSION = "0.1.0"
3
+ end
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,10 @@
1
+ require_relative '../../lib/tricle/mailer'
2
+ require_relative 'test_metric'
3
+
4
+ class ListTestMailer < Tricle::Mailer
5
+ default(to: 'recipient1@test.com', from: 'sender@test.com')
6
+
7
+ list TestMetric do |val|
8
+ sprintf('%.1f', val)
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../../lib/tricle/mailer'
2
+ require_relative 'test_metric'
3
+
4
+ class TestMailer < Tricle::Mailer
5
+ default(
6
+ to: ['recipient1@test.com', 'recipient2@test.com'],
7
+ from: 'sender@test.com'
8
+ )
9
+
10
+ metric TestMetric
11
+
12
+ 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,4 @@
1
+ require_relative 'test_metric'
2
+
3
+ class TestMetricWithLongName < TestMetric
4
+ 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