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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8ae60bb0e6e671d65f0c1901458bdb6a2a8ea730
4
+ data.tar.gz: 4714ec2150c42c605587d3cea5fd05382a60f4ab
5
+ SHA512:
6
+ metadata.gz: b0b2c91ada726e91f976c1cb5ed6c421c9a8a8e4a8d6f633f599bd644e71aa80af59157835cbad37ad844952abe3d60a59e5fe272d1a92c672133d44d871e9c1
7
+ data.tar.gz: 365357668547fc809ace7212a7d2a9931850b8d69d97f0d85b7e0713b23c205cddcbee6ecb96596c92ae1caba151ba5786441594dc8492e26c279e41df122f5a
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,23 @@
1
+ # Contributing
2
+
3
+ To set up, run
4
+
5
+ ```bash
6
+ bundle
7
+ ```
8
+
9
+ To run tests,
10
+
11
+ ```bash
12
+ bundle exec rspec
13
+ # or, to run continuously:
14
+ bundle exec guard
15
+ ```
16
+
17
+ To see a live preview with dummy data:
18
+
19
+ ```bash
20
+ gem install shotgun
21
+ shotgun
22
+ open http://localhost:9393/test_mailer.html
23
+ ```
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tricle.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,8 @@
1
+ notification :gntp
2
+
3
+ guard :rspec, all_on_start: true do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/}) { 'spec' }
6
+ watch('spec/spec_helper.rb') { 'spec' }
7
+ watch(%r{^spec/}) { 'spec' }
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Aidan Feldman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # Tricle [![Build Status](https://travis-ci.org/artsy/tricle.png?branch=master)](https://travis-ci.org/artsy/tricle) [![Code Climate](https://codeclimate.com/github/artsy/tricle.png)](https://codeclimate.com/github/artsy/tricle)
2
+
3
+ Automated metrics reporting via email. It's datastore-agnostic, so you can query SQL, MongoDB, external APIs, etc. to generate the stats you need. See [here](https://github.com/afeld/tricle-afeld) for an example implementation ([live demo](http://tricle.afeld.me/weekly_metrics)).
4
+
5
+ ![screenshot](screenshot.png)
6
+
7
+ ## Installation
8
+
9
+ ### Gem
10
+
11
+ This gem can be used within an existing project (e.g. a Rails app), or standalone.
12
+
13
+ ```ruby
14
+ # Gemfile
15
+ gem 'tricle', '~> 0.1.0'
16
+
17
+ # Rakefile
18
+ require 'tricle/tasks'
19
+
20
+ # your/config/file.rb
21
+ # unless you already have ActionMailer set up
22
+ ActionMailer::Base.raise_delivery_errors = true
23
+ ActionMailer::Base.smtp_settings = {
24
+ # ...
25
+ }
26
+ ```
27
+
28
+ See [the ActionMailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for configuration details. Finally, execute:
29
+
30
+ ```bash
31
+ bundle
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Metrics
37
+
38
+ For each metric you want to report, create a new subclass of `Tricle::Metric` that implements `#size_for_range` and `#total`:
39
+
40
+ ```ruby
41
+ class MyMetric < Tricle::Metric
42
+
43
+ # Retrieve the value of this metric for the provided time period. Generally
44
+ # this will be the count/value added/removed. Not necessary if #items_for_range
45
+ # is defined.
46
+ #
47
+ # @param start_at [Time]
48
+ # @param end_at [Time] non-inclusive
49
+ # @return [Fixnum]
50
+ def size_for_range(start_at, end_at)
51
+ # ...
52
+ end
53
+
54
+ # Retrieve the cumulative value for this metric.
55
+ #
56
+ # @return [Fixnum] the grand total
57
+ def total
58
+ # ...
59
+ end
60
+
61
+ # Optional: only necessary if using `list` for this Metric within your Mailer.
62
+ #
63
+ # @param start_at [Time]
64
+ # @param end_at [Time] non-inclusive
65
+ # @return [Enumerator]
66
+ def items_for_range(start_at, end_at)
67
+ # ...
68
+ end
69
+
70
+ end
71
+ ```
72
+
73
+ ActiveRecord example:
74
+
75
+ ```ruby
76
+ # metrics/new_users.rb
77
+ class NewUsers < Tricle::Metric
78
+
79
+ def size_for_range(start_at, end_at)
80
+ self.size_for_range(start_at, end_at).size
81
+ end
82
+
83
+ def total
84
+ self.users.count
85
+ end
86
+
87
+ def items_for_range(start_at, end_at)
88
+ self.size_for_range(start_at, end_at)
89
+ end
90
+
91
+
92
+ private
93
+
94
+ def size_for_range(start_at, end_at)
95
+ self.users.where('created_at >= ? AND created_at < ?', start_at, end_at)
96
+ end
97
+
98
+ # You can add whatever helper methods in that class that you need.
99
+ def users
100
+ # non-deleted Users
101
+ User.where(deleted_at: nil)
102
+ end
103
+
104
+ end
105
+ ```
106
+
107
+ ### Mailers
108
+
109
+ Mailers specify how a particular set of Metrics should be sent. You can define one or multiple, to send different metrics to different groups of people.
110
+
111
+ ```ruby
112
+ class MyMailer < Tricle::Mailer
113
+
114
+ # accepts the same options as ActionMailer... see "Default Hash" at
115
+ # http://rubydoc.info/gems/actionmailer/ActionMailer/Base
116
+ default(
117
+ # ...
118
+ )
119
+
120
+ metric MyMetric1
121
+ metric MyMetric2
122
+ # ...
123
+
124
+ # optional: metrics can be grouped
125
+ group "Group 1 Name" do
126
+ metric MyMetric3
127
+ # ...
128
+ end
129
+ group "Group 2 Name" do
130
+ metric MyMetric4
131
+ # ...
132
+ end
133
+
134
+ # optional: list the items for the specified Metric
135
+ list MyMetric2 do |item|
136
+ # return the HTML string for each particular item
137
+ end
138
+ # ...
139
+
140
+ end
141
+ ```
142
+
143
+ e.g.
144
+
145
+ ```ruby
146
+ # mailers/weekly_insights.rb
147
+ class WeeklyInsights < Tricle::Mailer
148
+
149
+ default(
150
+ to: ['theteam@mycompany.com', 'theboss@mycompany.com'],
151
+ from: 'noreply@mycompany.com'
152
+ )
153
+
154
+ metric NewUsers
155
+
156
+ list NewUsers do |user|
157
+ <<-MARKUP
158
+ <h3>#{user.name}</h3>
159
+ <div>#{user.location}</div>
160
+ <a href="mailto:#{user.email}>#{user.email}</a>
161
+ MARKUP
162
+ end
163
+
164
+ end
165
+ ```
166
+
167
+ The subject line will be based on the Mailer class name.
168
+
169
+ ### Previewing
170
+
171
+ Since you'd probably like to preview your mailers before sending them, set up the `Tricle::MailPreview` Rack app (which uses [MailView](https://github.com/37signals/mail_view)).
172
+
173
+ #### Within a Rails app
174
+
175
+ ```ruby
176
+ # config/initializers/tricle.rb
177
+ require 'tricle/mail_preview'
178
+
179
+ # config/routes.rb
180
+ if Rails.env.development?
181
+ mount MailPreview => 'mail_view'
182
+ end
183
+ ```
184
+
185
+ and navigate to [localhost:3000/mail_view](http://localhost:3000/mail_view).
186
+
187
+ #### Standalone
188
+
189
+ ```bash
190
+ bundle exec rake tricle:preview
191
+ open http://localhost:8080
192
+ ```
193
+
194
+ ## Deploying
195
+
196
+ To send all Tricle emails, run
197
+
198
+ ```bash
199
+ rake tricle:emails:send
200
+ ```
201
+
202
+ ### Cron Setup
203
+
204
+ TODO
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ require_relative 'spec/config/timecop'
5
+
6
+ reset_time
7
+
8
+ # used for the preview
9
+ require_relative 'lib/tricle'
10
+ require_relative 'spec/app/group_test_mailer.rb'
11
+ require_relative 'spec/app/list_test_mailer.rb'
12
+ require_relative 'spec/app/test_mailer.rb'
13
+ require_relative 'spec/app/uber_test_mailer.rb'
14
+
15
+ require_relative 'lib/tricle/tasks'
16
+
17
+ RSpec::Core::RakeTask.new(:spec)
18
+
19
+ task :default => :spec
data/lib/tricle.rb ADDED
@@ -0,0 +1,4 @@
1
+ # only the files needed publicly
2
+ require_relative 'tricle/version'
3
+ require_relative 'tricle/metric'
4
+ require_relative 'tricle/mailer'
@@ -0,0 +1,8 @@
1
+ module Tricle
2
+ class AbstractMethodError < RuntimeError
3
+ def to_s
4
+ name = self.backtrace.first[/`(\w+)'/, 1]
5
+ "##{name} not implemented"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,66 @@
1
+ require 'active_support/core_ext/date/calculations'
2
+ require 'active_support/core_ext/numeric/time'
3
+
4
+ module Tricle
5
+ module EmailHelper
6
+ def weeks_ago(n)
7
+ Date.today.beginning_of_week.weeks_ago(n)
8
+ end
9
+
10
+ def format_date(date)
11
+ date.strftime('%-m/%-d/%y')
12
+ end
13
+
14
+ def number_with_delimiter(number)
15
+ # from http://stackoverflow.com/a/11466770/358804
16
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
17
+ end
18
+
19
+ def percent_change(new_val, old_val)
20
+ if old_val == 0
21
+ new_val >= 0 ? '+' : '-'
22
+ else
23
+ fraction = (new_val - old_val) / old_val.to_f
24
+ sprintf('%+.1f%', fraction * 100.0)
25
+ end
26
+ end
27
+
28
+ def percent_change_cell(new_val, old_val)
29
+ cls = (new_val >= old_val) ? 'positive' : 'negative'
30
+ pct_str = percent_change(new_val, old_val)
31
+ old_val_str = number_with_delimiter(old_val.round)
32
+ %[<td class="#{cls}"><div>#{pct_str}</div><div>#{old_val_str}</div></td>].html_safe
33
+ end
34
+
35
+ def dates_range_str(start_at, end_at)
36
+ "#{ self.format_date(start_at) } - #{ self.format_date(end_at) }"
37
+ end
38
+
39
+ def dates_cell(start_at, end_at)
40
+ range = dates_range_str(start_at, end_at)
41
+ %[<div class="date-range">(#{range})</div>].html_safe
42
+ end
43
+
44
+ def single_week_dates_cell(start_at)
45
+ dates_cell(start_at, start_at.end_of_week)
46
+ end
47
+
48
+ def last_week_dates_cell
49
+ single_week_dates_cell(weeks_ago(1))
50
+ end
51
+
52
+ def previous_week_dates_cell
53
+ single_week_dates_cell(weeks_ago(2))
54
+ end
55
+
56
+ def quarter_dates_cell
57
+ dates_cell(weeks_ago(13), weeks_ago(1).end_of_week)
58
+ end
59
+
60
+ def list_markup(list)
61
+ start_at = self.weeks_ago(1).to_time
62
+ end_at = start_at + 7.days
63
+ list.items_markup(start_at, end_at).html_safe
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,10 @@
1
+ require 'mail_view'
2
+ require_relative 'mailer'
3
+
4
+ module Tricle
5
+ class MailPreview < MailView
6
+ Tricle::Mailer.descendants.each do |klass|
7
+ define_method(klass.name.underscore) { klass.email }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,75 @@
1
+ require 'action_mailer'
2
+ require 'active_support/descendants_tracker'
3
+ require 'premailer'
4
+
5
+ require_relative 'email_helper'
6
+ require_relative 'presenters/report'
7
+
8
+
9
+ module Tricle
10
+ class Mailer < ActionMailer::Base
11
+ include ActiveSupport::DescendantsTracker
12
+
13
+ class_attribute :report
14
+ helper Tricle::EmailHelper
15
+ self.view_paths = File.dirname(__FILE__)
16
+
17
+ CSS = File.read(File.join(File.dirname(__FILE__), 'templates', 'email.css')).freeze
18
+
19
+
20
+ def subject
21
+ "Your #{self.class.name.titleize}"
22
+ end
23
+
24
+ def premailer(message)
25
+ # message.text_part.body = Premailer.new(message.text_part.body.to_s, with_html_string: true).to_plain_text
26
+ message.html_part.body = Premailer.new(message.html_part.body.to_s, css_string: CSS.dup, with_html_string: true).to_inline_css
27
+ message
28
+ end
29
+
30
+ def email(options = {})
31
+ options = {
32
+ subject: self.subject
33
+ }.merge(options)
34
+
35
+ @report = self.report
36
+
37
+ message = mail(options) do |format|
38
+ format.html { render 'templates/email' }
39
+ format.text { render 'templates/email' }
40
+ end
41
+
42
+ premailer(message)
43
+ end
44
+
45
+ class << self
46
+ def inherited(klass)
47
+ klass.report = Tricle::Presenters::Report.new
48
+ super(klass)
49
+ end
50
+
51
+ def group(title)
52
+ self.report.add_group(title)
53
+ yield if block_given?
54
+ end
55
+
56
+ def metric(klass)
57
+ self.report.add_metric(klass)
58
+ end
59
+
60
+ def list(klass, &block)
61
+ self.report.add_list(klass, &block)
62
+ end
63
+
64
+ def send_all
65
+ mailers = Tricle::Mailer.descendants
66
+ puts "Sending #{mailers.size} emails..."
67
+ mailers.each do |klass|
68
+ puts "Sending #{klass.name}..."
69
+ klass.email.deliver
70
+ end
71
+ puts "Done."
72
+ end
73
+ end
74
+ end
75
+ end