cohortly 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/app/#assets# +0 -0
- data/app/controllers/cohortly/metrics_controller.rb +9 -2
- data/app/controllers/cohortly/reports_controller.rb +10 -6
- data/app/models/cohortly/metric.rb +65 -12
- data/app/models/cohortly/report.rb +55 -26
- data/app/models/cohortly/report_meta.rb +8 -0
- data/app/models/cohortly/stored_procedures.rb +54 -0
- data/app/views/cohortly/metrics/_groups.html.erb +9 -0
- data/app/views/cohortly/metrics/_tags.html.erb +11 -6
- data/app/views/cohortly/metrics/index.html.erb +8 -1
- data/app/views/cohortly/reports/index.html.erb +6 -26
- data/app/views/layouts/cohortly/#application.html.erb# +236 -0
- data/app/views/layouts/cohortly/application.html.erb +81 -1
- data/lib/cohortly/engine.rb +3 -0
- data/lib/cohortly/tag_config.rb +11 -2
- data/lib/cohortly/version.rb +1 -1
- data/lib/tasks/run_reports.rake +15 -3
- data/test/cohortly_test.rb +147 -39
- data/test/dummy/config/initializers/cohortly_config.rb +3 -1
- data/test/dummy/log/development.log +2562 -0
- data/test/dummy/log/test.log +1198 -0
- data/test/dummy/tmp/pids/server.pid +1 -0
- metadata +9 -2
data/app/#assets#
ADDED
File without changes
|
@@ -4,11 +4,18 @@ class Cohortly::MetricsController < Cohortly::CohortlyController
|
|
4
4
|
|
5
5
|
scope = Cohortly::Metric.sort(:created_at.desc)
|
6
6
|
if params[:cohortly_metric] && params[:cohortly_metric][:tags]
|
7
|
-
scope = scope.where(:tags => { :$
|
7
|
+
scope = scope.where(:tags => { :$in => @metric_search.tags })
|
8
8
|
end
|
9
|
-
if
|
9
|
+
if params[:cohortly_metric] && params[:cohortly_metric][:groups]
|
10
|
+
groups = params[:cohortly_metric][:groups]
|
11
|
+
scope = scope.where(:$where => "function() { return #{ groups.collect {|x| 'this.tags.indexOf("' + x + '") >= 0' }.join(' || ') }; }" )
|
12
|
+
end
|
13
|
+
if !@metric_search.user_id.blank?
|
10
14
|
scope = scope.where(:user_id => @metric_search.user_id)
|
11
15
|
end
|
16
|
+
if !@metric_search.username.blank?
|
17
|
+
scope = scope.where(:username => @metric_search.username)
|
18
|
+
end
|
12
19
|
@metrics = scope.paginate(:per_page => 200, :page => params[:page])
|
13
20
|
end
|
14
21
|
end
|
@@ -1,12 +1,16 @@
|
|
1
1
|
class Cohortly::ReportsController < Cohortly::CohortlyController
|
2
2
|
def index
|
3
|
+
Cohortly::Metric.send :attr_accessor, :groups
|
3
4
|
@metric_search = Cohortly::Metric.new(params[:cohortly_metric])
|
4
5
|
tags = @metric_search.tags.any? ? @metric_search.tags : nil
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
@report = Cohortly::Report.new(
|
6
|
+
groups = @metric_search.groups
|
7
|
+
@report_name = Cohortly::Metric.report_table_name(tags, groups, true)
|
8
|
+
# run this in background would be better
|
9
|
+
Cohortly::Metric.weekly_cohort_chart_for_tag(tags, groups)
|
10
|
+
@report = Cohortly::Report.new( tags, groups, true )
|
11
|
+
respond_to do |format|
|
12
|
+
format.html
|
13
|
+
format.js { render :json => @report }
|
14
|
+
end
|
10
15
|
end
|
11
|
-
|
12
16
|
end
|
@@ -10,7 +10,7 @@ module Cohortly
|
|
10
10
|
key :controller, String
|
11
11
|
key :action, String
|
12
12
|
timestamps!
|
13
|
-
|
13
|
+
|
14
14
|
ensure_index :tags
|
15
15
|
|
16
16
|
def self.store!(args)
|
@@ -24,21 +24,62 @@ module Cohortly
|
|
24
24
|
data[:tags] += data[:add_tags] if data[:add_tags]
|
25
25
|
create(data)
|
26
26
|
end
|
27
|
-
|
28
|
-
def self.
|
29
|
-
query = {}
|
30
|
-
query = {
|
31
|
-
|
27
|
+
|
28
|
+
def self.cohort_chart(tags = nil, groups = nil, weekly = false)
|
29
|
+
query = { }
|
30
|
+
query = {:tags => { :$in => tags} } if tags
|
31
|
+
if groups
|
32
|
+
query[:$where] = "function() { return #{ groups.collect {|x| 'this.tags.indexOf("' + x + '") >= 0' }.join(' || ') }; }"
|
33
|
+
end
|
34
|
+
collection_name = self.report_table_name(tags, groups, weekly)
|
35
|
+
# incremental map_reduce pattern
|
36
|
+
meta = Cohortly::ReportMeta.find_or_create_by_collection_name(collection_name)
|
37
|
+
query[:created_at] = { :$gt => meta.last_update_on.utc } if meta.last_update_on
|
38
|
+
self.collection.map_reduce(weekly ? self.week_map : self.month_map,
|
32
39
|
self.reduce,
|
33
|
-
{ :out =>
|
40
|
+
{ :out => meta.last_update_on ? { :reduce => collection_name } : collection_name,
|
34
41
|
:raw => true,
|
35
|
-
:query => query})
|
42
|
+
:query => query})
|
43
|
+
meta.last_update_on = Time.now.utc
|
44
|
+
meta.save
|
36
45
|
end
|
37
|
-
|
38
|
-
def self.
|
39
|
-
|
46
|
+
|
47
|
+
def self.cohort_chart_for_tag(tags = nil, groups = nil)
|
48
|
+
self.cohort_chart(tags, groups, false)
|
40
49
|
end
|
41
50
|
|
51
|
+
def self.weekly_cohort_chart_for_tag(tags = nil, groups = nil)
|
52
|
+
self.cohort_chart(tags, groups, true)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.report_table_name(tags = nil, groups = nil, weekly = true)
|
56
|
+
"cohortly_report#{ tags ? "-tags=#{ tags.sort.join(':') }" : '' }#{ groups ? "-groups=#{ groups.sort.join(':') }" : '' }#{ weekly ? '-weekly' : '-monthly'}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.report_name_to_args(name)
|
60
|
+
name = name.gsub(/^cohortly_report/, '')
|
61
|
+
if name =~ /-weekly$/
|
62
|
+
weekly = true
|
63
|
+
name = name.gsub(/-weekly$/, '')
|
64
|
+
else
|
65
|
+
weekly = false
|
66
|
+
name = name.gsub(/-monthly$/, '')
|
67
|
+
end
|
68
|
+
tags = nil
|
69
|
+
groups = nil
|
70
|
+
if name.length > 0
|
71
|
+
name = name.gsub(/-tags/, 'tags')
|
72
|
+
tags, groups = name.split('-')
|
73
|
+
tags = tags.gsub(/tags=/, '').split(':')
|
74
|
+
tags = tags.any? ? tags : nil
|
75
|
+
if groups
|
76
|
+
groups = groups.gsub(/groups=/,'').split(':')
|
77
|
+
groups = groups.any? ? groups : nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
[tags, groups, weekly]
|
81
|
+
end
|
82
|
+
|
42
83
|
def self.month_map
|
43
84
|
<<-JS
|
44
85
|
function() {
|
@@ -58,6 +99,19 @@ module Cohortly
|
|
58
99
|
JS
|
59
100
|
end
|
60
101
|
|
102
|
+
def self.week_map
|
103
|
+
<<-JS
|
104
|
+
function() {
|
105
|
+
var start_date = week_key(this.user_start_date);
|
106
|
+
var happened_on = week_key(this.created_at);
|
107
|
+
var payload = {};
|
108
|
+
payload[happened_on] = {};
|
109
|
+
payload[happened_on][this.user_id] = 1;
|
110
|
+
emit( start_date, payload );
|
111
|
+
}
|
112
|
+
JS
|
113
|
+
end
|
114
|
+
|
61
115
|
def self.reduce
|
62
116
|
<<-JS
|
63
117
|
function(key, values) {
|
@@ -77,6 +131,5 @@ module Cohortly
|
|
77
131
|
}
|
78
132
|
JS
|
79
133
|
end
|
80
|
-
|
81
134
|
end
|
82
135
|
end
|
@@ -1,60 +1,78 @@
|
|
1
1
|
module Cohortly
|
2
2
|
class Report
|
3
3
|
# this is the reduced collection
|
4
|
-
attr_accessor :collection
|
5
|
-
def initialize(
|
6
|
-
self.collection =
|
4
|
+
attr_accessor :collection, :weekly, :key_pattern, :groups, :tags
|
5
|
+
def initialize( tags = nil, groups = nil, weekly = true )
|
6
|
+
self.collection = Cohortly::Metric.report_table_name(tags, groups, weekly)
|
7
|
+
self.groups = groups
|
8
|
+
self.tags = tags
|
9
|
+
self.weekly = weekly
|
10
|
+
self.key_pattern = self.weekly ? "%Y-%W" : "%Y-%m"
|
7
11
|
end
|
8
12
|
|
9
13
|
def data
|
10
14
|
@data ||= (Cohortly::Metric.database[self.collection].find().collect {|x| x}).sort_by {|x| x['_id'] }
|
11
15
|
end
|
12
16
|
|
13
|
-
def
|
17
|
+
def fix_data_lines
|
18
|
+
data.each do |line|
|
19
|
+
period_cohorts_from(line['_id']).collect do |key|
|
20
|
+
if line["value"][key].nil?
|
21
|
+
line["value"][key] = { }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def offset
|
28
|
+
self.weekly ? 1.week : 1.month
|
29
|
+
end
|
30
|
+
|
31
|
+
def start_key
|
14
32
|
data.first['_id']
|
15
33
|
end
|
16
34
|
|
17
|
-
def
|
18
|
-
|
35
|
+
def end_key
|
36
|
+
time_to_key(Time.now.utc)
|
19
37
|
end
|
20
38
|
|
21
|
-
def
|
22
|
-
time.strftime(
|
39
|
+
def time_to_key(time)
|
40
|
+
time.strftime(self.key_pattern)
|
23
41
|
end
|
24
42
|
|
25
|
-
def
|
26
|
-
|
27
|
-
Time.utc(year.to_i, month.to_i)
|
43
|
+
def key_to_time(report_key)
|
44
|
+
DateTime.strptime(report_key, self.key_pattern).to_time.utc
|
28
45
|
end
|
29
46
|
|
30
|
-
def user_count_in_cohort(
|
31
|
-
|
32
|
-
|
33
|
-
|
47
|
+
def user_count_in_cohort(report_key)
|
48
|
+
params = { :user_start_date => { :$gt => key_to_time(report_key),
|
49
|
+
:$lt => (key_to_time(report_key) + self.offset)}}
|
50
|
+
params[:tags] = { :$in => groups } if self.groups
|
51
|
+
Cohortly::Metric.collection.distinct(:user_id, params).length
|
34
52
|
end
|
35
53
|
|
36
|
-
def
|
54
|
+
def period_cohorts
|
37
55
|
return [] unless data.first
|
38
|
-
start_time =
|
39
|
-
end_time =
|
56
|
+
start_time = key_to_time(start_key)
|
57
|
+
end_time = key_to_time(end_key)
|
40
58
|
cur_time = start_time
|
41
59
|
res = []
|
42
60
|
while(cur_time <= end_time) do
|
43
|
-
res <<
|
44
|
-
cur_time +=
|
61
|
+
res << time_to_key(cur_time)
|
62
|
+
cur_time += self.offset
|
45
63
|
end
|
46
64
|
res
|
47
65
|
end
|
48
|
-
|
49
|
-
def
|
50
|
-
index =
|
51
|
-
|
66
|
+
|
67
|
+
def period_cohorts_from(cohort_key)
|
68
|
+
index = period_cohorts.index(cohort_key)
|
69
|
+
period_cohorts[index..-1]
|
52
70
|
end
|
53
71
|
|
54
72
|
def report_line(cohort_key)
|
55
73
|
line = data.find {|x| x['_id'] == cohort_key}
|
56
74
|
return [] unless line
|
57
|
-
|
75
|
+
period_cohorts_from(cohort_key).collect do |key|
|
58
76
|
if line["value"][key]
|
59
77
|
line["value"][key].keys.length
|
60
78
|
else
|
@@ -70,9 +88,20 @@ module Cohortly
|
|
70
88
|
end
|
71
89
|
|
72
90
|
def report_totals
|
73
|
-
|
91
|
+
period_cohorts.collect do |cohort_key|
|
74
92
|
report_line(cohort_key)
|
75
93
|
end
|
76
94
|
end
|
95
|
+
|
96
|
+
def base_n
|
97
|
+
@base_n ||= self.period_cohorts.inject({ }) { |accum, key| accum[key] = user_count_in_cohort(key); accum }
|
98
|
+
end
|
99
|
+
|
100
|
+
def as_json(*args)
|
101
|
+
fix_data_lines
|
102
|
+
{ :data => data,
|
103
|
+
:base_n => base_n
|
104
|
+
}
|
105
|
+
end
|
77
106
|
end
|
78
107
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Cohortly
|
2
|
+
class StoredProcedures
|
3
|
+
PROCS = [:day_of_year,
|
4
|
+
:week_of_year,
|
5
|
+
:week_key]
|
6
|
+
|
7
|
+
def self.eval_js(javascript)
|
8
|
+
Cohortly::Metric.database.eval(javascript)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.day_of_year
|
12
|
+
<<-JS
|
13
|
+
function(date) {
|
14
|
+
var ms = date - new Date('' + date.getUTCFullYear() + '/1/1 UTC');
|
15
|
+
return parseInt(ms / 60000 / 60 / 24, 10) + 1;
|
16
|
+
}
|
17
|
+
JS
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.week_of_year
|
21
|
+
<<-JS
|
22
|
+
function(date) {
|
23
|
+
var doy = day_of_year(date);
|
24
|
+
var dow = date.getUTCDay();
|
25
|
+
dow = ((dow === 0) ? 7 : dow);
|
26
|
+
var rdow = 7 - dow;
|
27
|
+
var woy = parseInt((doy + rdow) / 7, 10);
|
28
|
+
return woy;
|
29
|
+
}
|
30
|
+
JS
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.week_key
|
34
|
+
<<-JS
|
35
|
+
function(date) {
|
36
|
+
var year = date.getYear() + 1900;
|
37
|
+
var week = week_of_year(date);
|
38
|
+
if(week < 10) { week = '0' + week }
|
39
|
+
return year + '-' + week;
|
40
|
+
}
|
41
|
+
JS
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.store_procedures()
|
45
|
+
PROCS.each do |proc_name|
|
46
|
+
Cohortly::Metric.database.add_stored_function(proc_name.to_s, self.send(proc_name))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.execute(proc_name, *args)
|
51
|
+
Cohortly::Metric.database.eval(self.send(proc_name), *args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<div class="groups">
|
2
|
+
<% Cohortly::TagConfig.all_groups.each do |group| %>
|
3
|
+
<span class"group">
|
4
|
+
<%= check_box_tag 'cohortly_metric[groups][]', group,
|
5
|
+
params[:cohortly_metric] && params[:cohortly_metric][:groups] &&
|
6
|
+
params[:cohortly_metric][:groups].include?(group) %><label><%= group %></label>
|
7
|
+
</span>
|
8
|
+
<% end %>
|
9
|
+
</div>
|
@@ -1,9 +1,14 @@
|
|
1
1
|
<div class="tags">
|
2
|
-
<% Cohortly::Metric.collection.distinct(:tags).
|
3
|
-
<
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
<% (Cohortly::Metric.collection.distinct(:tags) - Cohortly::TagConfig.all_groups).each_slice(8) do |tags| %>
|
3
|
+
<div class="tag_group" style="float:left; padding: 10px;">
|
4
|
+
<% tags.each do |tag| %>
|
5
|
+
<div class"tag">
|
6
|
+
<%= check_box_tag 'cohortly_metric[tags][]', tag,
|
7
|
+
params[:cohortly_metric] && params[:cohortly_metric][:tags] &&
|
8
|
+
params[:cohortly_metric][:tags].include?(tag) %><label><%= tag %></label>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
11
|
+
</div>
|
8
12
|
<% end %>
|
13
|
+
<div style="clear:both;"></div>
|
9
14
|
</div>
|
@@ -3,9 +3,16 @@
|
|
3
3
|
<%= form_for(@metric_search, :url => cohortly_metrics_path, :html => {
|
4
4
|
:method =>:get }) do |f| %>
|
5
5
|
<div class="field">
|
6
|
-
<%= f.label
|
6
|
+
<%= f.label 'User id' %>
|
7
7
|
<%= f.text_field :user_id %>
|
8
8
|
</div>
|
9
|
+
<div class="field">
|
10
|
+
<%= f.label :username %>
|
11
|
+
<%= f.text_field :username %>
|
12
|
+
</div>
|
13
|
+
<h3>Groups</h3>
|
14
|
+
<%= render :partial => 'cohortly/metrics/groups' %>
|
15
|
+
<h3>Tags</h3>
|
9
16
|
<%= render :partial => 'cohortly/metrics/tags' %>
|
10
17
|
<%= f.submit "filter" %>
|
11
18
|
<% end %>
|
@@ -1,32 +1,12 @@
|
|
1
1
|
<h2>Cohort Reports</h2>
|
2
2
|
|
3
3
|
<%= form_for @metric_search, :url => cohortly_reports_path, :html =>
|
4
|
-
{:method => :get} do |f| %>
|
4
|
+
{:method => :get, :class => 'cohortly_report_form'} do |f| %>
|
5
|
+
<h3>Groups</h3>
|
6
|
+
<%= render :partial => 'cohortly/metrics/groups' %>
|
7
|
+
<h3>Tags</h3>
|
5
8
|
<%= render :partial => 'cohortly/metrics/tags' %>
|
6
9
|
<%= f.submit 'Get Report'%>
|
10
|
+
<div class="result_table">
|
11
|
+
</div>
|
7
12
|
<% end %>
|
8
|
-
|
9
|
-
|
10
|
-
<table class="one-column-emphasis">
|
11
|
-
<colgroup>
|
12
|
-
<col class="oce-first">
|
13
|
-
</colgroup>
|
14
|
-
<thead>
|
15
|
-
<tr>
|
16
|
-
<th>Month</th>
|
17
|
-
<th>N</th>
|
18
|
-
<% 12.times do |month| %>
|
19
|
-
<th>M<%= month + 1 %></th>
|
20
|
-
<% end %>
|
21
|
-
</tr>
|
22
|
-
</thead>
|
23
|
-
<% @report.month_cohorts.each do |cohort_key| %>
|
24
|
-
<tr class="<%= cycle('odd', 'even') %>">
|
25
|
-
<td><%= cohort_key %></td>
|
26
|
-
<% @report.percent_line(cohort_key)[0,13].each do |val| %>
|
27
|
-
<td style="text-align: right;"><%= val %></td>
|
28
|
-
<% end %>
|
29
|
-
</tr>
|
30
|
-
<% end %>
|
31
|
-
</table>
|
32
|
-
|
@@ -0,0 +1,236 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Cohortly</title>
|
5
|
+
<%= csrf_meta_tag %>
|
6
|
+
|
7
|
+
<%= javascript_include_tag 'https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js' %>
|
8
|
+
<%= javascript_include_tag 'http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.1.7/underscore-min.js' %>
|
9
|
+
<%= javascript_include_tag 'http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.5.3/backbone-min.js' %>
|
10
|
+
|
11
|
+
<script type="text/javascript">
|
12
|
+
var Cohortly = {};
|
13
|
+
Cohortly.Report = Backbone.Model.extend({
|
14
|
+
|
15
|
+
});
|
16
|
+
Cohortly.ReportQuery = Backbone.Model.extend({});
|
17
|
+
Cohortly.ReportView = Backbone.View.extend({
|
18
|
+
initialize: function() {
|
19
|
+
_.bindAll(this, 'submitQuery', 'render', 'render_rows', 'render_row', 'render_cells');
|
20
|
+
this.model = new Cohortly.Report();
|
21
|
+
this.model.bind('change', this.render)
|
22
|
+
},
|
23
|
+
events: {
|
24
|
+
'click input[type=submit]': 'submitQuery'
|
25
|
+
},
|
26
|
+
submitQuery: function(e) {
|
27
|
+
$.get( '/cohortly/reports.js?' + $(this.el).serialize(), _(function(data) {
|
28
|
+
this.model.set(data);
|
29
|
+
}).bind(this), 'json')
|
30
|
+
e.preventDefault();
|
31
|
+
},
|
32
|
+
render: function() {
|
33
|
+
$('.result_table', this.el).html(
|
34
|
+
['<table class="one-column-emphasis">',
|
35
|
+
'<colgroup><col class="oce-first"></colgroup>',
|
36
|
+
this.render_header(),
|
37
|
+
this.render_rows(),
|
38
|
+
'</table>'
|
39
|
+
].join(''));
|
40
|
+
},
|
41
|
+
render_header: function() {
|
42
|
+
return [
|
43
|
+
'<thead><tr>',
|
44
|
+
'<th>Month</th>',
|
45
|
+
'<th>N</th>',
|
46
|
+
_.range(1,13).map(function(x){ return '<th>M' + x + '</th>'; }).join(''),
|
47
|
+
'</tr></thead>'
|
48
|
+
].join('');
|
49
|
+
},
|
50
|
+
render_rows: function() {
|
51
|
+
return _(this.model.get('data')).map(this.render_row).join('');
|
52
|
+
},
|
53
|
+
render_row: function(row) {
|
54
|
+
var row_key = row['_id'];
|
55
|
+
var base_n = this.model.get('base_n')[row_key];
|
56
|
+
return [
|
57
|
+
'<tr>',
|
58
|
+
'<td>' + row['_id']+ '</td>',
|
59
|
+
'<td>' + base_n + '</td>',
|
60
|
+
this.render_cells(row, base_n),
|
61
|
+
'</tr>'
|
62
|
+
].join('');
|
63
|
+
},
|
64
|
+
render_cells: function(row, base_n) {
|
65
|
+
return _(_(row.value).keys()).sortBy(function(x){return x}).slice(0,12).map(function(key) {
|
66
|
+
var num_users = _(row.value[key]).keys().length;
|
67
|
+
var percent = (base_n > 0) ? (num_users / base_n) : 0;
|
68
|
+
return [
|
69
|
+
'<td style="text-align:right;">',
|
70
|
+
Math.floor(percent * 100),
|
71
|
+
'</td>'
|
72
|
+
].join('');
|
73
|
+
}).join('');
|
74
|
+
}
|
75
|
+
|
76
|
+
});
|
77
|
+
|
78
|
+
$(function() {
|
79
|
+
Cohortly.report_view = new Cohortly.ReportView({ el: $('form') });
|
80
|
+
});
|
81
|
+
</script>
|
82
|
+
|
83
|
+
<style>
|
84
|
+
|
85
|
+
body {
|
86
|
+
font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
|
87
|
+
font-size: 12px;
|
88
|
+
margin: 0;
|
89
|
+
color: #666699;
|
90
|
+
}
|
91
|
+
form {
|
92
|
+
|
93
|
+
}
|
94
|
+
.field, .tags {
|
95
|
+
margin-bottom: 1em;
|
96
|
+
}
|
97
|
+
.field label {
|
98
|
+
display:block;
|
99
|
+
}
|
100
|
+
.header {
|
101
|
+
background-color: #E8EDFF;
|
102
|
+
height: 50px;
|
103
|
+
}
|
104
|
+
.header h1 {
|
105
|
+
padding-top: 12px;
|
106
|
+
margin: 0;
|
107
|
+
color: #669;
|
108
|
+
}
|
109
|
+
.container {
|
110
|
+
position: relative;
|
111
|
+
margin:0 auto;
|
112
|
+
width: 960px;
|
113
|
+
}
|
114
|
+
.hor-zebra {
|
115
|
+
border-collapse: collapse;
|
116
|
+
font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
|
117
|
+
font-size: 12px;
|
118
|
+
text-align: left;
|
119
|
+
width: 960px;
|
120
|
+
}
|
121
|
+
.hor-zebra th {
|
122
|
+
color: #003399;
|
123
|
+
font-size: 14px;
|
124
|
+
font-weight: normal;
|
125
|
+
padding: 10px 8px;
|
126
|
+
}
|
127
|
+
.hor-zebra td {
|
128
|
+
color: #666699;
|
129
|
+
padding: 8px;
|
130
|
+
}
|
131
|
+
.hor-zebra .odd {
|
132
|
+
background: none repeat scroll 0 0 #E8EDFF;
|
133
|
+
}
|
134
|
+
|
135
|
+
.one-column-emphasis {
|
136
|
+
border-collapse: collapse;
|
137
|
+
font-family: "Lucida Sans Unicode","Lucida Grande",Sans-Serif;
|
138
|
+
font-size: 12px;
|
139
|
+
text-align: left;
|
140
|
+
position: relative;
|
141
|
+
left: -4px;
|
142
|
+
margin-top: 20px;
|
143
|
+
}
|
144
|
+
.one-column-emphasis th {
|
145
|
+
color: #003399;
|
146
|
+
font-size: 14px;
|
147
|
+
font-weight: normal;
|
148
|
+
padding: 12px 15px;
|
149
|
+
}
|
150
|
+
.one-column-emphasis td {
|
151
|
+
border-top: 1px solid #E8EDFF;
|
152
|
+
color: #666699;
|
153
|
+
padding: 10px 15px;
|
154
|
+
}
|
155
|
+
.oce-first {
|
156
|
+
background: none repeat scroll 0 0 #D0DAFD;
|
157
|
+
border-left: 10px solid transparent;
|
158
|
+
border-right: 10px solid transparent;
|
159
|
+
}
|
160
|
+
.one-column-emphasis tr:hover td {
|
161
|
+
background: none repeat scroll 0 0 #EFF2FF;
|
162
|
+
color: #333399;
|
163
|
+
}
|
164
|
+
.navigation {
|
165
|
+
position: absolute;
|
166
|
+
bottom: -3px;
|
167
|
+
right: 0;
|
168
|
+
}
|
169
|
+
|
170
|
+
|
171
|
+
.navigation ul {
|
172
|
+
list-style: none;
|
173
|
+
margin: 0;
|
174
|
+
padding: 0;
|
175
|
+
}
|
176
|
+
|
177
|
+
|
178
|
+
.navigation ul li {
|
179
|
+
display: inline;
|
180
|
+
margin-right: .75em;
|
181
|
+
background-color: #FFF;
|
182
|
+
color: #669;
|
183
|
+
border-radius: 4px;
|
184
|
+
}
|
185
|
+
|
186
|
+
.navigation ul li.last {
|
187
|
+
margin-right: 0;
|
188
|
+
}
|
189
|
+
.navigation li {
|
190
|
+
padding: 5px 1em;
|
191
|
+
}
|
192
|
+
.navigation li a {
|
193
|
+
color: #669;
|
194
|
+
text-decoration: none;
|
195
|
+
}
|
196
|
+
.paginate {
|
197
|
+
margin-top: 1em;
|
198
|
+
margin-bottom: 1em;
|
199
|
+
text-align: right;
|
200
|
+
}
|
201
|
+
|
202
|
+
.paginate a, span {
|
203
|
+
|
204
|
+
text-decoration: none;
|
205
|
+
padding-left: 5px;
|
206
|
+
color: #666699;
|
207
|
+
}
|
208
|
+
.paginate span {
|
209
|
+
color: red;
|
210
|
+
}
|
211
|
+
</style>
|
212
|
+
|
213
|
+
</head>
|
214
|
+
<body>
|
215
|
+
<div class="header">
|
216
|
+
<div class="container">
|
217
|
+
<h1>Cohortly</h1>
|
218
|
+
<div class="navigation">
|
219
|
+
<ul>
|
220
|
+
<li><span><%= link_to "Metrics", cohortly_metrics_path %></span></li>
|
221
|
+
<li><%= link_to "Cohort Reports", cohortly_reports_path %></span></li>
|
222
|
+
</ul>
|
223
|
+
</div>
|
224
|
+
</div>
|
225
|
+
</div>
|
226
|
+
<div class="content">
|
227
|
+
<div class="container">
|
228
|
+
<%= yield %>
|
229
|
+
</div>
|
230
|
+
</div>
|
231
|
+
<div class="footer">
|
232
|
+
<div class="container">
|
233
|
+
</div>
|
234
|
+
</div>
|
235
|
+
</body>
|
236
|
+
</html>
|