@ -23,10 +23,8 @@ $(function() {
|
|||||||
groups = new vis.DataSet(buildChartGroups());
|
groups = new vis.DataSet(buildChartGroups());
|
||||||
graph = new vis.Graph2d(document.getElementById('graph'), dataset, groups, {
|
graph = new vis.Graph2d(document.getElementById('graph'), dataset, groups, {
|
||||||
dataAxis: {
|
dataAxis: {
|
||||||
customRange: {
|
|
||||||
left: {
|
left: {
|
||||||
min: 0
|
range: {min: 0}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
showMinorLabels: false
|
showMinorLabels: false
|
||||||
},
|
},
|
||||||
|
87
app/assets/javascripts/statistics_activity_history.js
Normal file
87
app/assets/javascripts/statistics_activity_history.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
function manageActivityHistory(prefix) {
|
||||||
|
var containerId = prefix + '-activity-history';
|
||||||
|
|
||||||
|
if ($('.graph#' + containerId).isPresent()) {
|
||||||
|
|
||||||
|
var chartData;
|
||||||
|
var dataset;
|
||||||
|
var graph;
|
||||||
|
var groups;
|
||||||
|
|
||||||
|
var buildChartGroups = function () {
|
||||||
|
return _.map(chartData, function (element) {
|
||||||
|
return {
|
||||||
|
content: element.name,
|
||||||
|
id: element.key,
|
||||||
|
visible: true,
|
||||||
|
options: {
|
||||||
|
interpolation: false,
|
||||||
|
yAxisOrientation: element.axis ? element.axis : 'left'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initializeChart = function () {
|
||||||
|
dataset = new vis.DataSet();
|
||||||
|
groups = new vis.DataSet(buildChartGroups());
|
||||||
|
graph = new vis.Graph2d(document.getElementById(containerId), dataset, groups, {
|
||||||
|
dataAxis: {
|
||||||
|
left: {
|
||||||
|
range: {min: 0}
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
range: {min: 0}
|
||||||
|
},
|
||||||
|
showMinorLabels: true,
|
||||||
|
alignZeros: true
|
||||||
|
},
|
||||||
|
drawPoints: {
|
||||||
|
style: 'circle'
|
||||||
|
},
|
||||||
|
legend: true,
|
||||||
|
start: $('#from-date')[0].value || 0,
|
||||||
|
end: $('#to-date')[0].value || 0
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var refreshData = function (callback) {
|
||||||
|
var params = new URLSearchParams(window.location.search.slice(1));
|
||||||
|
var jqxhr = $.ajax(prefix + '-activity-history.json', {
|
||||||
|
dataType: 'json',
|
||||||
|
data: {from: params.get('from'), to: params.get('to'), interval: params.get('interval')},
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
jqxhr.done(function (response) {
|
||||||
|
(callback || _.noop)(response);
|
||||||
|
updateChartData(response);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateChartData = function (response) {
|
||||||
|
_.each(response, function (group) {
|
||||||
|
_.each(group.data, function (data) {
|
||||||
|
dataset.add({
|
||||||
|
group: group.key,
|
||||||
|
x: data.key,
|
||||||
|
y: data.value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshData(function (data) {
|
||||||
|
chartData = data;
|
||||||
|
$('#' + containerId).parent().find('.spinner').hide();
|
||||||
|
initializeChart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($.isController('statistics')) {
|
||||||
|
manageActivityHistory('rfc');
|
||||||
|
manageActivityHistory('user');
|
||||||
|
}
|
||||||
|
});
|
107
app/assets/javascripts/statistics_graphs.js
Normal file
107
app/assets/javascripts/statistics_graphs.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
if ($.isController('statistics') && $('.graph#user-activity').isPresent()) {
|
||||||
|
|
||||||
|
function manageGraph(containerId, url, refreshAfter) {
|
||||||
|
var CHART_START = window.vis ? vis.moment().add(-1, 'minute') : undefined;
|
||||||
|
var DEFAULT_REFRESH_INTERVAL = refreshAfter * 1000 || 10000;
|
||||||
|
|
||||||
|
var refreshInterval;
|
||||||
|
|
||||||
|
var initialData;
|
||||||
|
var dataset;
|
||||||
|
var graph;
|
||||||
|
var groups;
|
||||||
|
|
||||||
|
var buildChartGroups = function() {
|
||||||
|
return _.map(initialData, function(element) {
|
||||||
|
return {
|
||||||
|
content: element.name + (element.unit ? ' [' + element.unit + ']' : ''),
|
||||||
|
id: element.key,
|
||||||
|
visible: false,
|
||||||
|
options: {
|
||||||
|
yAxisOrientation: element.axis ? element.axis : 'left'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initializeChart = function() {
|
||||||
|
dataset = new vis.DataSet();
|
||||||
|
groups = new vis.DataSet(buildChartGroups());
|
||||||
|
graph = new vis.Graph2d(document.getElementById(containerId), dataset, groups, {
|
||||||
|
dataAxis: {
|
||||||
|
left: {
|
||||||
|
range: {min: 0}
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
range: {min: 0}
|
||||||
|
},
|
||||||
|
showMinorLabels: true,
|
||||||
|
alignZeros: true
|
||||||
|
},
|
||||||
|
drawPoints: {
|
||||||
|
style: 'circle'
|
||||||
|
},
|
||||||
|
end: vis.moment(),
|
||||||
|
legend: true,
|
||||||
|
start: CHART_START
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var refreshChart = function() {
|
||||||
|
var now = vis.moment();
|
||||||
|
var window = graph.getWindow();
|
||||||
|
var interval = window.end - window.start;
|
||||||
|
graph.setWindow(now - interval, now);
|
||||||
|
};
|
||||||
|
|
||||||
|
var refreshData = function(callback) {
|
||||||
|
if (! ($.isController('statistics') && $('#' + containerId).isPresent())) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
} else {
|
||||||
|
var jqxhr = $.ajax(url, {
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
jqxhr.done(function(response) {
|
||||||
|
(callback || _.noop)(response);
|
||||||
|
setGroupVisibility(response);
|
||||||
|
updateChartData(response);
|
||||||
|
requestAnimationFrame(refreshChart);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var setGroupVisibility = function(response) {
|
||||||
|
_.each(response, function(data) {
|
||||||
|
groups.update({
|
||||||
|
id: data.key,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateChartData = function(response) {
|
||||||
|
_.each(response, function(data) {
|
||||||
|
dataset.add({
|
||||||
|
group: data.key,
|
||||||
|
x: vis.moment(),
|
||||||
|
y: data.data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshData(function (data) {
|
||||||
|
initialData = data;
|
||||||
|
$('#' + containerId).parent().find('.spinner').hide();
|
||||||
|
initializeChart();
|
||||||
|
|
||||||
|
var refresh_interval = location.search.match(/interval=(\d+)/) ? parseInt(RegExp.$1) : DEFAULT_REFRESH_INTERVAL;
|
||||||
|
refreshInterval = setInterval(refreshData, refresh_interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
manageGraph('user-activity', 'graphs/user-activity', 10);
|
||||||
|
manageGraph('rfc-activity', 'graphs/rfc-activity', 30);
|
||||||
|
}
|
||||||
|
});
|
@ -105,3 +105,49 @@ tr.highlight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: #333;
|
||||||
|
|
||||||
|
margin: 100px auto;
|
||||||
|
-webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;
|
||||||
|
animation: sk-rotateplane 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes sk-rotateplane {
|
||||||
|
0% { -webkit-transform: perspective(120px) }
|
||||||
|
50% { -webkit-transform: perspective(120px) rotateY(180deg) }
|
||||||
|
100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sk-rotateplane {
|
||||||
|
0% {
|
||||||
|
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
|
||||||
|
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
|
||||||
|
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
|
||||||
|
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,16 +1,59 @@
|
|||||||
class StatisticsController < ApplicationController
|
class StatisticsController < ApplicationController
|
||||||
include StatisticsHelper
|
include StatisticsHelper
|
||||||
|
|
||||||
|
before_action :authorize!, only: [:show, :graphs, :user_activity, :user_activity_history, :rfc_activity,
|
||||||
|
:rfc_activity_history]
|
||||||
|
|
||||||
def policy_class
|
def policy_class
|
||||||
StatisticsPolicy
|
StatisticsPolicy
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize self
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html
|
format.html
|
||||||
format.json { render(json: statistics_data) }
|
format.json { render(json: statistics_data) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def graphs
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_activity
|
||||||
|
respond_to do |format|
|
||||||
|
format.json { render(json: user_activity_live_data) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_activity_history
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render('activity_history', locals: {resource: :user}) }
|
||||||
|
format.json { render_ranged_data :ranged_user_data}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rfc_activity
|
||||||
|
respond_to do |format|
|
||||||
|
format.json { render(json: rfc_activity_data) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rfc_activity_history
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render('activity_history', locals: {resource: :rfc}) }
|
||||||
|
format.json { render_ranged_data :ranged_rfc_data }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_ranged_data(data_source)
|
||||||
|
interval = params[:interval].to_s.empty? ? 'year' : params[:interval]
|
||||||
|
from = DateTime.strptime(params[:from], '%Y-%m-%d') rescue DateTime.new(0)
|
||||||
|
to = DateTime.strptime(params[:to], '%Y-%m-%d') rescue DateTime.now
|
||||||
|
render(json: self.send(data_source, interval, from, to))
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize!
|
||||||
|
authorize self
|
||||||
|
end
|
||||||
|
private :authorize!
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -39,7 +39,8 @@ module StatisticsHelper
|
|||||||
name: t('statistics.entries.users.currently_active'),
|
name: t('statistics.entries.users.currently_active'),
|
||||||
data: ExternalUser.joins(:submissions)
|
data: ExternalUser.joins(:submissions)
|
||||||
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
|
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
|
||||||
.distinct('external_users.id').count
|
.distinct('external_users.id').count,
|
||||||
|
url: 'statistics/graphs'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@ -61,7 +62,8 @@ module StatisticsHelper
|
|||||||
key: 'submissions_per_minute',
|
key: 'submissions_per_minute',
|
||||||
name: t('statistics.entries.exercises.submissions_per_minute'),
|
name: t('statistics.entries.exercises.submissions_per_minute'),
|
||||||
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
|
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
|
||||||
unit: '/min'
|
unit: '/min',
|
||||||
|
url: statistics_graphs_path
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'execution_environments',
|
key: 'execution_environments',
|
||||||
@ -79,45 +81,129 @@ module StatisticsHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rfc_statistics
|
def rfc_statistics
|
||||||
|
rfc_activity_data + [
|
||||||
|
{
|
||||||
|
key: 'comments',
|
||||||
|
name: t('activerecord.models.comment.other'),
|
||||||
|
data: Comment.count
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_activity_live_data
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'active_in_last_hour',
|
||||||
|
name: t('statistics.entries.users.currently_active'),
|
||||||
|
data: ExternalUser.joins(:submissions)
|
||||||
|
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
|
||||||
|
.distinct('external_users.id').count,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'submissions_per_minute',
|
||||||
|
name: t('statistics.entries.exercises.submissions_per_minute'),
|
||||||
|
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
|
||||||
|
unit: '/min',
|
||||||
|
axis: 'right'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def rfc_activity_data(from=DateTime.new(0), to=DateTime.now)
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'rfcs',
|
key: 'rfcs',
|
||||||
name: t('activerecord.models.request_for_comment.other'),
|
name: t('activerecord.models.request_for_comment.other'),
|
||||||
data: RequestForComment.count,
|
data: RequestForComment.in_range(from, to).count,
|
||||||
url: request_for_comments_path
|
url: request_for_comments_path
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'percent_solved',
|
key: 'percent_solved',
|
||||||
name: t('statistics.entries.request_for_comments.percent_solved'),
|
name: t('statistics.entries.request_for_comments.percent_solved'),
|
||||||
data: (100.0 / RequestForComment.count * RequestForComment.where(solved: true).count).round(1),
|
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).where(solved: true).count).round(1),
|
||||||
unit: '%',
|
unit: '%',
|
||||||
url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=0'
|
axis: 'right',
|
||||||
|
url: statistics_graphs_path
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'percent_soft_solved',
|
key: 'percent_soft_solved',
|
||||||
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
||||||
data: (100.0 / RequestForComment.count * RequestForComment.unsolved.where(full_score_reached: true).count).round(1),
|
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).unsolved.where(full_score_reached: true).count).round(1),
|
||||||
unit: '%',
|
unit: '%',
|
||||||
url: request_for_comments_path
|
axis: 'right',
|
||||||
|
url: statistics_graphs_path
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'percent_unsolved',
|
key: 'percent_unsolved',
|
||||||
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
||||||
data: (100.0 / RequestForComment.count * RequestForComment.unsolved.count).round(1),
|
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).unsolved.count).round(1),
|
||||||
unit: '%',
|
unit: '%',
|
||||||
url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=1'
|
axis: 'right',
|
||||||
},
|
url: statistics_graphs_path
|
||||||
{
|
|
||||||
key: 'comments',
|
|
||||||
name: t('activerecord.models.comment.other'),
|
|
||||||
data: Comment.count
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rfcs_with_comments',
|
key: 'rfcs_with_comments',
|
||||||
name: t('statistics.entries.request_for_comments.with_comments'),
|
name: t('statistics.entries.request_for_comments.with_comments'),
|
||||||
data: RequestForComment.joins('join "submissions" s on s.id = request_for_comments.submission_id
|
data: RequestForComment.in_range(from, to).joins('join "submissions" s on s.id = request_for_comments.submission_id
|
||||||
join "files" f on f.context_id = s.id and f.context_type = \'Submission\'
|
join "files" f on f.context_id = s.id and f.context_type = \'Submission\'
|
||||||
join "comments" c on c.file_id = f.id').group('request_for_comments.id').count.size
|
join "comments" c on c.file_id = f.id').group('request_for_comments.id').count.size,
|
||||||
|
url: statistics_graphs_path
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def ranged_rfc_data(interval='year', from=DateTime.new(0), to=DateTime.now)
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'rfcs',
|
||||||
|
name: t('activerecord.models.request_for_comment.other'),
|
||||||
|
data: RequestForComment.in_range(from, to)
|
||||||
|
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||||
|
.group('key').order('key')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rfcs_solved',
|
||||||
|
name: t('statistics.entries.request_for_comments.percent_solved'),
|
||||||
|
data: RequestForComment.in_range(from, to)
|
||||||
|
.where(solved: true)
|
||||||
|
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||||
|
.group('key').order('key')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rfcs_soft_solved',
|
||||||
|
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
||||||
|
data: RequestForComment.in_range(from, to).unsolved
|
||||||
|
.where(full_score_reached: true)
|
||||||
|
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||||
|
.group('key').order('key')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rfcs_unsolved',
|
||||||
|
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
||||||
|
data: RequestForComment.in_range(from, to).unsolved
|
||||||
|
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||||
|
.group('key').order('key')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def ranged_user_data(interval='year', from=DateTime.new(0), to=DateTime.now)
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: 'active',
|
||||||
|
name: t('statistics.entries.users.active'),
|
||||||
|
data: ExternalUser.joins(:submissions)
|
||||||
|
.where(submissions: {created_at: from..to})
|
||||||
|
.select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"")
|
||||||
|
.group('key').order('key')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'submissions',
|
||||||
|
name: t('statistics.entries.exercises.submissions'),
|
||||||
|
data: Submission.where(created_at: from..to)
|
||||||
|
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||||
|
.group('key').order('key'),
|
||||||
|
axis: 'right'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
@ -8,6 +8,7 @@ class RequestForComment < ActiveRecord::Base
|
|||||||
has_many :subscriptions
|
has_many :subscriptions
|
||||||
|
|
||||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||||
|
scope :in_range, -> (from, to) { where(created_at: from..to) }
|
||||||
|
|
||||||
def self.last_per_user(n = 5)
|
def self.last_per_user(n = 5)
|
||||||
from("(#{row_number_user_sql}) as request_for_comments")
|
from("(#{row_number_user_sql}) as request_for_comments")
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
class StatisticsPolicy < AdminOnlyPolicy
|
class StatisticsPolicy < AdminOnlyPolicy
|
||||||
|
|
||||||
|
[:graphs?, :user_activity?, :user_activity_history?, :rfc_activity?, :rfc_activity_history?].each do |action|
|
||||||
|
define_method(action) { admin? }
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
22
app/views/statistics/activity_history.html.slim
Normal file
22
app/views/statistics/activity_history.html.slim
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
- content_for :head do
|
||||||
|
= javascript_include_tag(asset_path('vis.min.js', type: :javascript))
|
||||||
|
= stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet))
|
||||||
|
|
||||||
|
.group
|
||||||
|
.title
|
||||||
|
h1 = t("statistics.graphs.#{resource}_activity")
|
||||||
|
.spinner
|
||||||
|
.graph id="#{resource}-activity-history"
|
||||||
|
form
|
||||||
|
.form-group
|
||||||
|
label for="from-date" = t('.from')
|
||||||
|
input type="date" class="form-control" id="from-date" name="from" value=(params[:from] || DateTime.new(2016).to_date)
|
||||||
|
.form-group
|
||||||
|
label for="to-date" = t('.to')
|
||||||
|
input type="date" class="form-control" id="to-date" name="to" value=(params[:to] || DateTime.now.to_date)
|
||||||
|
.form-group
|
||||||
|
label for="interval" = t('.interval')
|
||||||
|
select class="form-control" id="interval" name="interval"
|
||||||
|
= [:year, :quarter, :month, :day, :hour, :minute, :second].each do | key |
|
||||||
|
option selected=(key.to_s == params[:interval]) = key
|
||||||
|
button type="submit" class="btn btn-primary" = t('.update')
|
17
app/views/statistics/graphs.html.slim
Normal file
17
app/views/statistics/graphs.html.slim
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
- content_for :head do
|
||||||
|
= javascript_include_tag(asset_path('vis.min.js', type: :javascript))
|
||||||
|
= stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet))
|
||||||
|
|
||||||
|
.group
|
||||||
|
.title
|
||||||
|
h1 = t('.user_activity')
|
||||||
|
a href=statistics_graphs_user_activity_history_path = t('.history')
|
||||||
|
.spinner
|
||||||
|
.graph#user-activity
|
||||||
|
|
||||||
|
.group
|
||||||
|
.title
|
||||||
|
h1 = t('.rfc_activity')
|
||||||
|
a href=statistics_graphs_rfc_activity_history_path = t('.history')
|
||||||
|
.spinner
|
||||||
|
.graph#rfc-activity
|
@ -221,6 +221,9 @@ de:
|
|||||||
destroy_through_lti: Code-Abgabe
|
destroy_through_lti: Code-Abgabe
|
||||||
statistics:
|
statistics:
|
||||||
show: "Statistiken"
|
show: "Statistiken"
|
||||||
|
graphs: "Visualisierungen"
|
||||||
|
user_activity_history: Nutzeraktivitätshistorie
|
||||||
|
rfc_activity_history: Kommentaranfragenhistorie
|
||||||
consumers:
|
consumers:
|
||||||
show:
|
show:
|
||||||
link: Konsument
|
link: Konsument
|
||||||
@ -749,6 +752,7 @@ de:
|
|||||||
exercises:
|
exercises:
|
||||||
average_number_of_submissions: "Durchschnittliche Zahl von Abgaben"
|
average_number_of_submissions: "Durchschnittliche Zahl von Abgaben"
|
||||||
submissions_per_minute: "Aktuelle Abgabenhäufigkeit (1h)"
|
submissions_per_minute: "Aktuelle Abgabenhäufigkeit (1h)"
|
||||||
|
submissions: "Abgaben"
|
||||||
request_for_comments:
|
request_for_comments:
|
||||||
percent_solved: "Beantwortete Anfragen"
|
percent_solved: "Beantwortete Anfragen"
|
||||||
percent_unsolved: "Unbeantwortete Anfragen"
|
percent_unsolved: "Unbeantwortete Anfragen"
|
||||||
@ -756,6 +760,17 @@ de:
|
|||||||
with_comments: "Anfragen mit Kommentaren"
|
with_comments: "Anfragen mit Kommentaren"
|
||||||
users:
|
users:
|
||||||
currently_active: "Aktiv (5 Minuten)"
|
currently_active: "Aktiv (5 Minuten)"
|
||||||
|
currently_active60: "Aktiv (60 Minuten)"
|
||||||
|
active: "Aktive Nutzer"
|
||||||
|
graphs:
|
||||||
|
user_activity: "Nutzeraktivität"
|
||||||
|
rfc_activity: "Kommentaranfragenaktivität"
|
||||||
|
history: "Historie"
|
||||||
|
activity_history:
|
||||||
|
from: "Von"
|
||||||
|
to: "Bis"
|
||||||
|
interval: "Intervall"
|
||||||
|
update: "Aktualisieren"
|
||||||
navigation:
|
navigation:
|
||||||
sections:
|
sections:
|
||||||
errors: "Fehler"
|
errors: "Fehler"
|
||||||
|
@ -221,6 +221,9 @@ en:
|
|||||||
destroy_through_lti: Code Submission
|
destroy_through_lti: Code Submission
|
||||||
statistics:
|
statistics:
|
||||||
show: "Statistics"
|
show: "Statistics"
|
||||||
|
graphs: "Graphs"
|
||||||
|
user_activity_history: User Activity History
|
||||||
|
rfc_activity_history: RfC Activity History
|
||||||
consumers:
|
consumers:
|
||||||
show:
|
show:
|
||||||
link: Consumer
|
link: Consumer
|
||||||
@ -749,6 +752,7 @@ en:
|
|||||||
exercises:
|
exercises:
|
||||||
average_number_of_submissions: "Average Number of Submissions"
|
average_number_of_submissions: "Average Number of Submissions"
|
||||||
submissions_per_minute: "Current Submission Volume (1h)"
|
submissions_per_minute: "Current Submission Volume (1h)"
|
||||||
|
submissions: "Submissions"
|
||||||
request_for_comments:
|
request_for_comments:
|
||||||
percent_solved: "Solved Requests"
|
percent_solved: "Solved Requests"
|
||||||
percent_unsolved: "Unsolved Requests"
|
percent_unsolved: "Unsolved Requests"
|
||||||
@ -756,6 +760,17 @@ en:
|
|||||||
with_comments: "RfCs with Comments"
|
with_comments: "RfCs with Comments"
|
||||||
users:
|
users:
|
||||||
currently_active: "Active (5 minutes)"
|
currently_active: "Active (5 minutes)"
|
||||||
|
currently_active60: "Active (60 minutes)"
|
||||||
|
active: "Active Users"
|
||||||
|
graphs:
|
||||||
|
user_activity: "User Activity"
|
||||||
|
rfc_activity: "RfC Activity"
|
||||||
|
history: "History"
|
||||||
|
activity_history:
|
||||||
|
from: "From"
|
||||||
|
to: "To"
|
||||||
|
interval: "Interval"
|
||||||
|
update: "Update"
|
||||||
navigation:
|
navigation:
|
||||||
sections:
|
sections:
|
||||||
errors: "Errors"
|
errors: "Errors"
|
||||||
|
@ -43,6 +43,11 @@ Rails.application.routes.draw do
|
|||||||
get '/help', to: 'application#help'
|
get '/help', to: 'application#help'
|
||||||
|
|
||||||
get 'statistics/', to: 'statistics#show'
|
get 'statistics/', to: 'statistics#show'
|
||||||
|
get 'statistics/graphs', to: 'statistics#graphs'
|
||||||
|
get 'statistics/graphs/user-activity', to: 'statistics#user_activity'
|
||||||
|
get 'statistics/graphs/user-activity-history', to: 'statistics#user_activity_history'
|
||||||
|
get 'statistics/graphs/rfc-activity', to: 'statistics#rfc_activity'
|
||||||
|
get 'statistics/graphs/rfc-activity-history', to: 'statistics#rfc_activity_history'
|
||||||
|
|
||||||
concern :statistics do
|
concern :statistics do
|
||||||
member do
|
member do
|
||||||
|
41
public/javascripts/vis.min.js
vendored
41
public/javascripts/vis.min.js
vendored
File diff suppressed because one or more lines are too long
2
public/stylesheets/vis.min.css
vendored
2
public/stylesheets/vis.min.css
vendored
File diff suppressed because one or more lines are too long
34
spec/controllers/statistics_controller_spec.rb
Normal file
34
spec/controllers/statistics_controller_spec.rb
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe StatisticsController do
|
||||||
|
let(:user) { FactoryBot.create(:admin) }
|
||||||
|
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||||
|
|
||||||
|
[:show, :graphs].each do |route|
|
||||||
|
describe "GET ##{route}" do
|
||||||
|
before(:each) { get route }
|
||||||
|
|
||||||
|
expect_status(200)
|
||||||
|
expect_template(route)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[:user_activity_history, :rfc_activity_history].each do |route|
|
||||||
|
describe "GET ##{route}" do
|
||||||
|
before(:each) { get route }
|
||||||
|
|
||||||
|
expect_status(200)
|
||||||
|
expect_template(:activity_history)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[:show, :user_activity, :user_activity_history, :rfc_activity, :rfc_activity_history].each do |route|
|
||||||
|
describe "GET ##{route}.json" do
|
||||||
|
before(:each) { get route, format: :json }
|
||||||
|
|
||||||
|
expect_status(200)
|
||||||
|
expect_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Reference in New Issue
Block a user