tricle 0.1.0

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