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
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
data/.rspec
ADDED
data/.travis.yml
ADDED
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
data/Guardfile
ADDED
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,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,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
|