audit_rails 1.1.10 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/app/assets/javascripts/audit_rails/application.js +4 -0
- data/app/assets/javascripts/audit_rails/d3/d3.tip.js +276 -0
- data/app/assets/javascripts/audit_rails/d3/d3.v3.js +9300 -0
- data/app/assets/javascripts/audit_rails/page-views.js +71 -0
- data/app/assets/javascripts/audit_rails/user-counts.js +72 -0
- data/app/assets/stylesheets/audit_rails/audit_rails.css +62 -0
- data/app/controllers/audit_rails/audits_controller.rb +22 -9
- data/app/helpers/audit_rails/audits_helper.rb +7 -2
- data/app/models/audit_rails/audit.rb +12 -3
- data/app/views/audit_rails/audits/_form.html.erb +3 -8
- data/app/views/audit_rails/audits/_form_elements.html.erb +20 -0
- data/app/views/audit_rails/audits/_list.html.erb +12 -0
- data/app/views/audit_rails/audits/_page_views.html.erb +7 -20
- data/app/views/audit_rails/audits/_user_clicks.html.erb +6 -21
- data/app/views/audit_rails/audits/analytics.html.erb +1 -1
- data/app/views/audit_rails/audits/index.html.erb +6 -26
- data/app/views/layouts/audit_rails/application.html.erb +1 -5
- data/config/routes.rb +0 -1
- data/db/migrate/20121213191242_create_audit_rails_audits.rb +1 -0
- data/lib/audit_rails/version.rb +1 -1
- data/spec/controllers/audit_rails/application_controller_spec.rb +7 -0
- data/spec/controllers/audit_rails/audits_controller_spec.rb +1 -1
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +1 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +44 -0
- data/spec/dummy/log/test.log +5977 -0
- data/spec/models/audit_rails/audit_spec.rb +2 -2
- metadata +8 -2
@@ -0,0 +1,71 @@
|
|
1
|
+
var data = JSON.parse( $('div#pageViewsCount').attr('data-visit-count'));
|
2
|
+
var right = d3.max([20, (900 - data.length * 50)]);
|
3
|
+
var margin = {top: 40, right: right, bottom: 100, left: 40},
|
4
|
+
width = 960 - margin.left - margin.right,
|
5
|
+
height = 360 - margin.top - margin.bottom;
|
6
|
+
|
7
|
+
var x = d3.scale.ordinal()
|
8
|
+
.rangeRoundBands([0, width], .1);
|
9
|
+
|
10
|
+
var y = d3.scale.linear()
|
11
|
+
.range([height, 0]);
|
12
|
+
|
13
|
+
var xAxis = d3.svg.axis()
|
14
|
+
.scale(x)
|
15
|
+
.orient("bottom");
|
16
|
+
|
17
|
+
var yAxis = d3.svg.axis()
|
18
|
+
.scale(y)
|
19
|
+
.orient("left");
|
20
|
+
|
21
|
+
var tip = d3.tip()
|
22
|
+
.attr('class', 'd3-tip')
|
23
|
+
.offset([-10, 0])
|
24
|
+
.html(function(d) {
|
25
|
+
return "<strong>Page Views:</strong> <span style='color:red'>" + d.count + "</span>";
|
26
|
+
})
|
27
|
+
|
28
|
+
var svg = d3.select("div#pageViewsCount").append("svg")
|
29
|
+
.attr("width", width + margin.left + margin.right)
|
30
|
+
.attr("height", height + margin.top + margin.bottom)
|
31
|
+
.append("g")
|
32
|
+
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
33
|
+
|
34
|
+
svg.call(tip);
|
35
|
+
|
36
|
+
x.domain(data.map(function(d) { return d.page; }));
|
37
|
+
y.domain([0, d3.max(data, function(d) { return d.count; })]);
|
38
|
+
|
39
|
+
svg.append("g")
|
40
|
+
.attr("class", "x axis")
|
41
|
+
.attr("transform", "translate(0," + height + ")")
|
42
|
+
.call(xAxis)
|
43
|
+
.selectAll("text")
|
44
|
+
.style("text-anchor", "end")
|
45
|
+
.attr("dx", "-.8em")
|
46
|
+
.attr("dy", ".15em")
|
47
|
+
.attr("transform", function(d) {
|
48
|
+
return "rotate(-65)"
|
49
|
+
});
|
50
|
+
|
51
|
+
svg.append("g")
|
52
|
+
.attr("class", "y axis")
|
53
|
+
.call(yAxis)
|
54
|
+
.append("text")
|
55
|
+
.attr("transform", "rotate(-90)")
|
56
|
+
.attr("y", -40)
|
57
|
+
.attr("x", -80)
|
58
|
+
.attr("dy", "0.71em")
|
59
|
+
.style("text-anchor", "end")
|
60
|
+
.text("Page Views");
|
61
|
+
|
62
|
+
svg.selectAll(".bar")
|
63
|
+
.data(data)
|
64
|
+
.enter().append("rect")
|
65
|
+
.attr("class", "bar")
|
66
|
+
.attr("x", function(d) { return x(d.page); })
|
67
|
+
.attr("width", x.rangeBand())
|
68
|
+
.attr("y", function(d) { return y(d.count); })
|
69
|
+
.attr("height", function(d) { return height - y(d.count); })
|
70
|
+
.on('mouseover', tip.show)
|
71
|
+
.on('mouseout', tip.hide);
|
@@ -0,0 +1,72 @@
|
|
1
|
+
var data = JSON.parse( $('div#userViewsCount').attr('data-visit-count'));
|
2
|
+
|
3
|
+
var right = d3.max([20, (900 - data.length * 50)]);
|
4
|
+
var margin = {top: 40, right: right, bottom: 100, left: 40},
|
5
|
+
width = 960 - margin.left - margin.right,
|
6
|
+
height = 360 - margin.top - margin.bottom;
|
7
|
+
|
8
|
+
var x = d3.scale.ordinal()
|
9
|
+
.rangeRoundBands([0, width], .1);
|
10
|
+
|
11
|
+
var y = d3.scale.linear()
|
12
|
+
.range([height, 0]);
|
13
|
+
|
14
|
+
var xAxis = d3.svg.axis()
|
15
|
+
.scale(x)
|
16
|
+
.orient("bottom");
|
17
|
+
|
18
|
+
var yAxis = d3.svg.axis()
|
19
|
+
.scale(y)
|
20
|
+
.orient("left");
|
21
|
+
|
22
|
+
var tip = d3.tip()
|
23
|
+
.attr('class', 'd3-tip')
|
24
|
+
.offset([-10, 0])
|
25
|
+
.html(function(d) {
|
26
|
+
return "<strong>Page Views:</strong> <span style='color:red'>" + d.count + "</span>";
|
27
|
+
})
|
28
|
+
|
29
|
+
var svg = d3.select("div#userViewsCount").append("svg")
|
30
|
+
.attr("width", width + margin.left + margin.right)
|
31
|
+
.attr("height", height + margin.top + margin.bottom)
|
32
|
+
.append("g")
|
33
|
+
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
34
|
+
|
35
|
+
svg.call(tip);
|
36
|
+
|
37
|
+
x.domain(data.map(function(d) { return d.user; }));
|
38
|
+
y.domain([0, d3.max(data, function(d) { return d.count; })]);
|
39
|
+
|
40
|
+
svg.append("g")
|
41
|
+
.attr("class", "x axis")
|
42
|
+
.attr("transform", "translate(0," + height + ")")
|
43
|
+
.call(xAxis)
|
44
|
+
.selectAll("text")
|
45
|
+
.style("text-anchor", "end")
|
46
|
+
.attr("dx", "-.8em")
|
47
|
+
.attr("dy", ".15em")
|
48
|
+
.attr("transform", function(d) {
|
49
|
+
return "rotate(-65)"
|
50
|
+
});
|
51
|
+
|
52
|
+
svg.append("g")
|
53
|
+
.attr("class", "y axis")
|
54
|
+
.call(yAxis)
|
55
|
+
.append("text")
|
56
|
+
.attr("transform", "rotate(-90)")
|
57
|
+
.attr("y", -40)
|
58
|
+
.attr("x", -80)
|
59
|
+
.attr("dy", "0.71em")
|
60
|
+
.style("text-anchor", "end")
|
61
|
+
.text("Page Views");
|
62
|
+
|
63
|
+
svg.selectAll(".bar")
|
64
|
+
.data(data)
|
65
|
+
.enter().append("rect")
|
66
|
+
.attr("class", "bar")
|
67
|
+
.attr("x", function(d) { return x(d.user); })
|
68
|
+
.attr("width", x.rangeBand())
|
69
|
+
.attr("y", function(d) { return y(d.count); })
|
70
|
+
.attr("height", function(d) { return height - y(d.count); })
|
71
|
+
.on('mouseover', tip.show)
|
72
|
+
.on('mouseout', tip.hide);
|
@@ -1,3 +1,65 @@
|
|
1
1
|
.left {
|
2
2
|
float: left;
|
3
3
|
}
|
4
|
+
.audit-row{
|
5
|
+
width: 60%;
|
6
|
+
border-radius: 15px;
|
7
|
+
}
|
8
|
+
.clear{
|
9
|
+
clear: both;
|
10
|
+
}
|
11
|
+
.axis path,
|
12
|
+
.axis line {
|
13
|
+
fill: none;
|
14
|
+
stroke: #000;
|
15
|
+
shape-rendering: crispEdges;
|
16
|
+
}
|
17
|
+
|
18
|
+
.bar {
|
19
|
+
fill: orange;
|
20
|
+
}
|
21
|
+
|
22
|
+
.bar:hover {
|
23
|
+
fill: orangered ;
|
24
|
+
}
|
25
|
+
|
26
|
+
.line {
|
27
|
+
fill: white;
|
28
|
+
border-color: orange;
|
29
|
+
}
|
30
|
+
.line:hover {
|
31
|
+
border-color: orangered;
|
32
|
+
}
|
33
|
+
|
34
|
+
.x.axis path {
|
35
|
+
/*display: none;*/
|
36
|
+
}
|
37
|
+
|
38
|
+
.d3-tip {
|
39
|
+
line-height: 1;
|
40
|
+
font-weight: bold;
|
41
|
+
padding: 12px;
|
42
|
+
background: rgba(0, 0, 0, 0.8);
|
43
|
+
color: #fff;
|
44
|
+
border-radius: 2px;
|
45
|
+
}
|
46
|
+
|
47
|
+
/* Creates a small triangle extender for the tooltip */
|
48
|
+
.d3-tip:after {
|
49
|
+
box-sizing: border-box;
|
50
|
+
display: inline;
|
51
|
+
font-size: 10px;
|
52
|
+
width: 100%;
|
53
|
+
line-height: 1;
|
54
|
+
color: rgba(0, 0, 0, 0.8);
|
55
|
+
content: "\25BC";
|
56
|
+
position: absolute;
|
57
|
+
text-align: center;
|
58
|
+
}
|
59
|
+
|
60
|
+
/* Style northward tooltips differently */
|
61
|
+
.d3-tip.n:after {
|
62
|
+
margin: -1px 0 0 0;
|
63
|
+
top: 100%;
|
64
|
+
left: 0;
|
65
|
+
}
|
@@ -2,16 +2,14 @@ require_dependency "audit_rails/application_controller"
|
|
2
2
|
|
3
3
|
module AuditRails
|
4
4
|
class AuditsController < ApplicationController
|
5
|
+
before_filter :apply_filter, except: [:create]
|
5
6
|
|
6
7
|
def index
|
7
|
-
|
8
|
-
|
9
|
-
respond_to do |format|
|
10
|
-
format.html # index.html.erb
|
11
|
-
format.json { render json: @audits }
|
12
|
-
format.xls { send_data @audits.to_xls(:columns => [:user_name, :action, :description, :created_at],
|
13
|
-
:headers => ['User name', 'Action', 'Details', 'When?']), filename: 'audits.xls'}
|
8
|
+
find_all_audits
|
14
9
|
|
10
|
+
if params[:commit] == "Download Filtered Report"
|
11
|
+
send_data(@audits.to_xls(:columns => [:user_name, :action, :description, :created_at],
|
12
|
+
:headers => ['User name', 'Action', 'Details', 'When?']), filename: 'audits.xls') and return
|
15
13
|
end
|
16
14
|
end
|
17
15
|
|
@@ -21,12 +19,27 @@ module AuditRails
|
|
21
19
|
end
|
22
20
|
|
23
21
|
def analytics
|
24
|
-
@range_begin = params[:analytics] ? params[:analytics][:range_begin] : nil
|
25
|
-
@range_end = params[:analytics] ? params[:analytics][:range_end] : nil
|
26
22
|
@analysis_by_user_name = AuditRails::Audit.in_range(@range_begin, @range_end).analysis_by_user_name
|
27
23
|
@analysis_by_page_views = AuditRails::Audit.in_range(@range_begin, @range_end).analysis_by_page_views
|
28
24
|
@total = AuditRails::Audit.in_range(@range_begin, @range_end).count
|
29
25
|
@no_audits = AuditRails::Audit.count == 0
|
26
|
+
|
27
|
+
if params[:commit] == "Download Filtered Report"
|
28
|
+
find_all_audits
|
29
|
+
send_data(@audits.to_xls(:columns => [:user_name, :action, :description, :created_at],
|
30
|
+
:headers => ['User name', 'Action', 'Details', 'When?']), filename: 'audits.xls') and return
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def apply_filter
|
37
|
+
@range_begin = params[:analytics] ? params[:analytics][:range_begin] : nil
|
38
|
+
@range_end = params[:analytics] ? params[:analytics][:range_end] : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_all_audits
|
42
|
+
@audits = AuditRails::Audit.reverse_chronological.in_range(@range_begin, @range_end)
|
30
43
|
end
|
31
44
|
end
|
32
45
|
end
|
@@ -3,13 +3,18 @@ module AuditRails
|
|
3
3
|
def add_to_audit(action_name=nil, controller_name=nil, user_name=nil, description=nil)
|
4
4
|
if action_name == "login"
|
5
5
|
if AuditRails::Audit.no_audit_entry_for_today?(action_name, user_name)
|
6
|
-
AuditRails::Audit.create(action: action_name,
|
6
|
+
AuditRails::Audit.create(action: action_name,
|
7
|
+
controller: controller_name || request.params[:controller],
|
8
|
+
user_name: user_name,
|
9
|
+
description: description,
|
10
|
+
ip_address: request.remote_ip.to_s)
|
7
11
|
end
|
8
12
|
else
|
9
13
|
AuditRails::Audit.create(action: action_name || request.params[:action],
|
10
14
|
controller: controller_name || request.params[:controller],
|
11
15
|
user_name: user_name,
|
12
|
-
description: description
|
16
|
+
description: description,
|
17
|
+
ip_address: request.remote_ip.to_s)
|
13
18
|
end
|
14
19
|
end
|
15
20
|
|
@@ -5,7 +5,7 @@ module AuditRails
|
|
5
5
|
end
|
6
6
|
|
7
7
|
if needs_attr_accessible?
|
8
|
-
attr_accessible :action, :controller, :description, :user_name
|
8
|
+
attr_accessible :action, :controller, :description, :user_name, :ip_address
|
9
9
|
end
|
10
10
|
|
11
11
|
# Supports both string and date format of range given
|
@@ -19,6 +19,7 @@ module AuditRails
|
|
19
19
|
scope :reverse_chronological, ->{order('created_at DESC')}
|
20
20
|
scope :group_by_controller_action, ->{group([:controller, :action])}
|
21
21
|
scope :group_by_user_name, ->{group('user_name')}
|
22
|
+
scope :group_by_ip_address, ->{group('ip_address')}
|
22
23
|
|
23
24
|
def self.no_audit_entry_for_today?(action_name, user_name)
|
24
25
|
audits = where(action: action_name, user_name: user_name,
|
@@ -28,11 +29,19 @@ module AuditRails
|
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.analysis_by_user_name
|
31
|
-
group_by_user_name.count
|
32
|
+
group_by_user_name.count.map{|k,v| {'user' => k, 'count' => v}}.to_json
|
32
33
|
end
|
33
34
|
|
34
35
|
def self.analysis_by_page_views
|
35
|
-
group_by_controller_action.count
|
36
|
+
group_by_controller_action.count.map{|k,v| {'page' => k.join('/'), 'count' => v}}.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.unique_visitor_count
|
40
|
+
group_by_ip_address.count.values.size
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.visitor_count
|
44
|
+
group_by_ip_address.count.values.sum
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
@@ -1,10 +1,5 @@
|
|
1
|
-
<div>
|
2
|
-
<%= form_tag(
|
3
|
-
|
4
|
-
<%= text_field_tag('analytics[range_begin]', @range_begin)%>
|
5
|
-
|
6
|
-
End:
|
7
|
-
<%= text_field_tag('analytics[range_end]', @range_end)%>
|
8
|
-
<%= submit_tag('filter')%>
|
1
|
+
<div class='row'>
|
2
|
+
<%= form_tag(path, method: :get) do %>
|
3
|
+
<%= render partial: 'form_elements' %>
|
9
4
|
<% end %>
|
10
5
|
</div>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<div class='col-sm-2'>
|
2
|
+
<div class="input-group">
|
3
|
+
<%= text_field_tag('analytics[range_begin]', @range_begin, placeholder: 'Begin', class: 'form-control')%>
|
4
|
+
</div>
|
5
|
+
</div>
|
6
|
+
<div class='col-sm-2'>
|
7
|
+
<div class="input-group">
|
8
|
+
<%= text_field_tag('analytics[range_end]', @range_end, placeholder: 'End', class: 'form-control')%>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
<div class='col-sm-1'>
|
12
|
+
<div class="input-group">
|
13
|
+
<%= submit_tag('Filter', class: 'btn btn-primary btn-sm')%>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
<div class='col-sm-2'>
|
17
|
+
<div class="input-group">
|
18
|
+
<%= submit_tag('Download Filtered Report', class: 'btn btn-primary btn-sm') %>
|
19
|
+
</div>
|
20
|
+
</div>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<% audits.each do |audit| %>
|
2
|
+
<div class='audit-row well'>
|
3
|
+
<div class='audit-title'>
|
4
|
+
<%= "#{audit.controller}/" if audit.controller %><%= audit.action %> by <%= audit.user_name %> about <%= distance_of_time_in_words(audit.created_at, Time.now) %> ago
|
5
|
+
</div>
|
6
|
+
<% if audit.description %>
|
7
|
+
<div class='audit-description'>
|
8
|
+
<%= audit.description %>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
@@ -1,24 +1,11 @@
|
|
1
|
+
<div class='clear'></div>
|
1
2
|
<div class='left'>
|
2
|
-
<
|
3
|
+
<ul class="nav nav-pills">
|
4
|
+
<li class="active">
|
5
|
+
<a href="#">Total page views <span class="badge"><%= @total %></span></a>
|
6
|
+
</li>
|
7
|
+
</ul>
|
3
8
|
<br/>
|
4
|
-
<
|
5
|
-
<script>
|
9
|
+
<div id='pageViewsCount' data-visit-count='<%= @analysis_by_page_views %>'></div>
|
6
10
|
|
7
|
-
var lineChartData = {
|
8
|
-
labels : <%= "#{@analysis_by_page_views.keys}".html_safe %>,
|
9
|
-
datasets : [
|
10
|
-
{
|
11
|
-
fillColor : "rgba(151,187,205,0.5)",
|
12
|
-
strokeColor : "rgba(220,220,220,1)",
|
13
|
-
pointColor : "rgba(220,220,220,1)",
|
14
|
-
pointStrokeColor : "#fff",
|
15
|
-
data : <%= @analysis_by_page_views.values %>
|
16
|
-
}
|
17
|
-
]
|
18
|
-
|
19
|
-
}
|
20
|
-
|
21
|
-
var myLine = new Chart(document.getElementById("pageViews").getContext("2d")).Line(lineChartData, {scaleLineWidth : 2, scaleLineColor : "rgba(0,0,0,.1)", scaleSteps : <%= @analysis_by_user_name.values.max/5 + 2%>});
|
22
|
-
|
23
|
-
</script>
|
24
11
|
</div>
|
@@ -1,24 +1,9 @@
|
|
1
1
|
<div class='left'>
|
2
|
-
<
|
2
|
+
<ul class="nav nav-pills">
|
3
|
+
<li class="active">
|
4
|
+
<a href="#">Total user clicks <span class="badge"><%= @total %></span></a>
|
5
|
+
</li>
|
6
|
+
</ul>
|
3
7
|
<br/>
|
4
|
-
<
|
5
|
-
<script>
|
6
|
-
|
7
|
-
var barChartData = {
|
8
|
-
labels : <%= "#{@analysis_by_user_name.keys}".html_safe %>,
|
9
|
-
datasets : [
|
10
|
-
{
|
11
|
-
fillColor : "rgba(151,187,205,0.5)",
|
12
|
-
strokeColor : "rgba(220,220,220,1)",
|
13
|
-
pointColor : "rgba(220,220,220,1)",
|
14
|
-
pointStrokeColor : "#fff",
|
15
|
-
data : <%= @analysis_by_user_name.values %>
|
16
|
-
}
|
17
|
-
]
|
18
|
-
|
19
|
-
}
|
20
|
-
|
21
|
-
var myBar = new Chart(document.getElementById("userViews").getContext("2d")).Bar(barChartData, {scaleLineWidth : 2, scaleLineColor : "rgba(0,0,0,.1)", scaleSteps : <%= @analysis_by_user_name.values.max/5 + 2%>});
|
22
|
-
|
23
|
-
</script>
|
8
|
+
<div id='userViewsCount' data-visit-count='<%= @analysis_by_user_name %>'></div>
|
24
9
|
</div>
|