From 7031dd389ed202e9a9e2eca1c548b1d6b6dc3ae3 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 13 Mar 2018 16:23:49 +0100 Subject: [PATCH 01/19] Scaffold controller and route --- app/assets/stylesheets/statistics.scss | 3 +++ app/controllers/statistics_controller.rb | 11 +++++++++++ app/policies/statistics_policy.rb | 2 ++ app/views/application/_navigation.html.slim | 1 + app/views/statistics/show.html.slim | 1 + config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ config/routes.rb | 2 ++ 8 files changed, 24 insertions(+) create mode 100644 app/assets/stylesheets/statistics.scss create mode 100644 app/controllers/statistics_controller.rb create mode 100644 app/policies/statistics_policy.rb create mode 100644 app/views/statistics/show.html.slim diff --git a/app/assets/stylesheets/statistics.scss b/app/assets/stylesheets/statistics.scss new file mode 100644 index 00000000..3645fc0b --- /dev/null +++ b/app/assets/stylesheets/statistics.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Statistics controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/statistics_controller.rb b/app/controllers/statistics_controller.rb new file mode 100644 index 00000000..66ffea02 --- /dev/null +++ b/app/controllers/statistics_controller.rb @@ -0,0 +1,11 @@ +class StatisticsController < ApplicationController + + def policy_class + StatisticsPolicy + end + + def show + authorize self + end + +end diff --git a/app/policies/statistics_policy.rb b/app/policies/statistics_policy.rb new file mode 100644 index 00000000..36ca2b51 --- /dev/null +++ b/app/policies/statistics_policy.rb @@ -0,0 +1,2 @@ +class StatisticsPolicy < AdminOnlyPolicy +end diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim index a2604c7a..127e170c 100644 --- a/app/views/application/_navigation.html.slim +++ b/app/views/application/_navigation.html.slim @@ -7,6 +7,7 @@ ul.dropdown-menu role='menu' - if current_user.admin? li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path) + li = link_to(t('breadcrumbs.statistics.show'), statistics_path) li.divider - models = [ExecutionEnvironment, Exercise, ExerciseCollection, ProxyExercise, Tag, Consumer, CodeHarborLink, UserExerciseFeedback, ErrorTemplate, ErrorTemplateAttribute, ExternalUser, FileType, FileTemplate, InternalUser].sort_by {|model| model.model_name.human(count: 2) } diff --git a/app/views/statistics/show.html.slim b/app/views/statistics/show.html.slim new file mode 100644 index 00000000..14372a07 --- /dev/null +++ b/app/views/statistics/show.html.slim @@ -0,0 +1 @@ +h1 = t('shared.statistics') diff --git a/config/locales/de.yml b/config/locales/de.yml index ce03d90f..4db92696 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -217,6 +217,8 @@ de: show: Dashboard sessions: destroy_through_lti: Code-Abgabe + statistics: + show: "Statistiken" consumers: show: link: Konsument diff --git a/config/locales/en.yml b/config/locales/en.yml index be92de5b..1a7ef575 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -217,6 +217,8 @@ en: show: Dashboard sessions: destroy_through_lti: Code Submission + statistics: + show: "Statistics" consumers: show: link: Consumer diff --git a/config/routes.rb b/config/routes.rb index 264e7471..21b1b719 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,8 @@ Rails.application.routes.draw do get '/help', to: 'application#help' + get 'statistics/', to: 'statistics#show' + concern :statistics do member do get :statistics From da0859d4838f76ee50d80957ed9c7a2bc7f18fbd Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 13 Mar 2018 17:37:43 +0100 Subject: [PATCH 02/19] Prepare grid --- app/assets/stylesheets/statistics.css.scss | 27 ++++++++++++++++++++++ app/assets/stylesheets/statistics.scss | 3 --- app/views/statistics/show.html.slim | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) delete mode 100644 app/assets/stylesheets/statistics.scss diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index a5ea4430..c1c8d993 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -58,3 +58,30 @@ div.negative-result { box-shadow: 0px 0px 11px 1px rgba(222,0,0,1); } +///////////////////////////////////////////////////////////////////////////////////////////// +// StatisticsController: + +.statistics-wrapper { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-auto-rows: 150px; + grid-gap: 10px; + + > div { + border: 2px solid #0055ba; + border-radius: 5px; + background-color: #008cba; + padding: 1em; + color: #fff; + display: flex; + flex-flow: column-reverse; + text-align: center; + + > .data { + flex-grow: 1; + font-size: 40px; + vertical-align: middle; + line-height: 100px; + } + } +} diff --git a/app/assets/stylesheets/statistics.scss b/app/assets/stylesheets/statistics.scss deleted file mode 100644 index 3645fc0b..00000000 --- a/app/assets/stylesheets/statistics.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the Statistics controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/views/statistics/show.html.slim b/app/views/statistics/show.html.slim index 14372a07..c151cf29 100644 --- a/app/views/statistics/show.html.slim +++ b/app/views/statistics/show.html.slim @@ -1 +1,3 @@ h1 = t('shared.statistics') + +.statistics-wrapper From 68d3fd174e3af3853e0f883529adaf37c9de1624 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 13 Mar 2018 18:38:54 +0100 Subject: [PATCH 03/19] Implement basic statistics --- app/assets/stylesheets/statistics.css.scss | 10 ++- app/controllers/statistics_controller.rb | 5 ++ app/helpers/statistics_helper.rb | 74 ++++++++++++++++++++++ app/views/statistics/show.html.slim | 10 ++- config/locales/de.yml | 10 +++ config/locales/en.yml | 10 +++ 6 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 app/helpers/statistics_helper.rb diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index c1c8d993..bf5e5c05 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -61,6 +61,10 @@ div.negative-result { ///////////////////////////////////////////////////////////////////////////////////////////// // StatisticsController: +#statistics-container { + margin-bottom: 40px; +} + .statistics-wrapper { display: grid; grid-template-columns: repeat(4, 1fr); @@ -81,7 +85,11 @@ div.negative-result { flex-grow: 1; font-size: 40px; vertical-align: middle; - line-height: 100px; + line-height: 50px; + } + + > .title { + height: 42px; } } } diff --git a/app/controllers/statistics_controller.rb b/app/controllers/statistics_controller.rb index 66ffea02..a26d5670 100644 --- a/app/controllers/statistics_controller.rb +++ b/app/controllers/statistics_controller.rb @@ -1,4 +1,5 @@ class StatisticsController < ApplicationController + include StatisticsHelper def policy_class StatisticsPolicy @@ -6,6 +7,10 @@ class StatisticsController < ApplicationController def show authorize self + respond_to do |format| + format.html + format.json { render(json: statistics_data) } + end end end diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb new file mode 100644 index 00000000..124ddf3d --- /dev/null +++ b/app/helpers/statistics_helper.rb @@ -0,0 +1,74 @@ +module StatisticsHelper + + def statistics_data + [ + { + key: 'users', + name: t('statistics.sections.users'), + entries: user_statistics + }, + { + key: 'exercises', + name: t('statistics.sections.exercises'), + entries: exercise_statistics + }, + { + key: 'request_for_comments', + name: t('statistics.sections.request_for_comments'), + entries: rfc_statistics + } + ] + end + + def user_statistics + [ + { + key: 'internal_users', + title: t('activerecord.models.internal_user.other'), + data: InternalUser.count + }, + { + key: 'external_users', + title: t('activerecord.models.external_user.other'), + data: ExternalUser.count + } + ] + end + + def exercise_statistics + [ + { + key: 'exercises', + title: t('activerecord.models.exercise.other'), + data: Exercise.count + }, + { + key: 'average_submissions', + title: t('statistics.entries.exercises.average_number_of_submissions'), + data: Submission.count / Exercise.count + } + ] + end + + def rfc_statistics + [ + { + key: 'rfcs', + title: t('activerecord.models.request_for_comment.other'), + data: RequestForComment.count + }, + { + key: 'percent_solved', + title: t('statistics.entries.request_for_comments.percent_solved'), + data: (100.0 / RequestForComment.count * RequestForComment.where(solved: true).count).round(2), + unit: '%' + }, + { + key: 'comments', + title: t('activerecord.models.comment.other'), + data: Comment.count + }, + ] + end + +end diff --git a/app/views/statistics/show.html.slim b/app/views/statistics/show.html.slim index c151cf29..e34f8933 100644 --- a/app/views/statistics/show.html.slim +++ b/app/views/statistics/show.html.slim @@ -1,3 +1,9 @@ -h1 = t('shared.statistics') -.statistics-wrapper +#statistics-container + - statistics_data.each do | section | + h2 = section[:name] + .statistics-wrapper + - section[:entries].each do | entry | + div + .title = entry[:title] + .data = entry[:data].to_s + (entry[:unit] or '') diff --git a/config/locales/de.yml b/config/locales/de.yml index 4db92696..9e8b180c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -676,3 +676,13 @@ de: subscriptions: successfully_unsubscribed: "Ihr Abonnement für weitere Kommentare auf dieser Kommentaranfrage wurde erfolgreich beendet." subscription_not_existent: "Das Abonnement, von dem Sie sich abmelden wollen, existiert nicht." + statistics: + sections: + users: "Benutzer" + exercises: "Aufgaben" + request_for_comments: "Kommentaranfragen" + entries: + exercises: + average_number_of_submissions: "Durchschnittliche Zahl von Abgaben" + request_for_comments: + percent_solved: "Beantwortete Anfragen" diff --git a/config/locales/en.yml b/config/locales/en.yml index 1a7ef575..3dca3a02 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -676,3 +676,13 @@ en: subscriptions: successfully_unsubscribed: "You successfully unsubscribed from this Request for Comment" subscription_not_existent: "The subscription you want to unsubscribe from does not exist." + statistics: + sections: + users: "Users" + exercises: "Exercises" + request_for_comments: "Requests for Comment" + entries: + exercises: + average_number_of_submissions: "Average Number of Submissions" + request_for_comments: + percent_solved: "Solved Requests" From 1bf2757c442f369c4653d9fb4deb9b860e8803e2 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 08:42:49 +0100 Subject: [PATCH 04/19] Adjust attribute names --- app/helpers/statistics_helper.rb | 14 +++++++------- app/views/statistics/show.html.slim | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index 124ddf3d..0b91b808 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -24,12 +24,12 @@ module StatisticsHelper [ { key: 'internal_users', - title: t('activerecord.models.internal_user.other'), + name: t('activerecord.models.internal_user.other'), data: InternalUser.count }, { key: 'external_users', - title: t('activerecord.models.external_user.other'), + name: t('activerecord.models.external_user.other'), data: ExternalUser.count } ] @@ -39,12 +39,12 @@ module StatisticsHelper [ { key: 'exercises', - title: t('activerecord.models.exercise.other'), + name: t('activerecord.models.exercise.other'), data: Exercise.count }, { key: 'average_submissions', - title: t('statistics.entries.exercises.average_number_of_submissions'), + name: t('statistics.entries.exercises.average_number_of_submissions'), data: Submission.count / Exercise.count } ] @@ -54,18 +54,18 @@ module StatisticsHelper [ { key: 'rfcs', - title: t('activerecord.models.request_for_comment.other'), + name: t('activerecord.models.request_for_comment.other'), data: RequestForComment.count }, { key: 'percent_solved', - title: 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(2), unit: '%' }, { key: 'comments', - title: t('activerecord.models.comment.other'), + name: t('activerecord.models.comment.other'), data: Comment.count }, ] diff --git a/app/views/statistics/show.html.slim b/app/views/statistics/show.html.slim index e34f8933..8846457b 100644 --- a/app/views/statistics/show.html.slim +++ b/app/views/statistics/show.html.slim @@ -2,8 +2,8 @@ #statistics-container - statistics_data.each do | section | h2 = section[:name] - .statistics-wrapper + .statistics-wrapper data-key=section[:key] - section[:entries].each do | entry | - div - .title = entry[:title] + div data-key=entry[:key] + .title = entry[:name] .data = entry[:data].to_s + (entry[:unit] or '') From 6252df3a74d273e82f3133b064b3228ffc031329 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 08:54:33 +0100 Subject: [PATCH 05/19] Allow linking to specific pages --- app/assets/stylesheets/statistics.css.scss | 36 ++++++++++++---------- app/helpers/statistics_helper.rb | 15 ++++++--- app/views/statistics/show.html.slim | 7 +++-- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index bf5e5c05..dd66f0e6 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -71,25 +71,29 @@ div.negative-result { grid-auto-rows: 150px; grid-gap: 10px; - > div { - border: 2px solid #0055ba; - border-radius: 5px; - background-color: #008cba; - padding: 1em; + > a { color: #fff; - display: flex; - flex-flow: column-reverse; - text-align: center; + text-decoration: none; - > .data { - flex-grow: 1; - font-size: 40px; - vertical-align: middle; - line-height: 50px; - } + > div { + border: 2px solid #0055ba; + border-radius: 5px; + background-color: #008cba; + padding: 1em; + display: flex; + flex-flow: column-reverse; + text-align: center; - > .title { - height: 42px; + > .data { + flex-grow: 1; + font-size: 40px; + vertical-align: middle; + line-height: 50px; + } + + > .title { + height: 42px; + } } } } diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index 0b91b808..08e7ee88 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -25,12 +25,14 @@ module StatisticsHelper { key: 'internal_users', name: t('activerecord.models.internal_user.other'), - data: InternalUser.count + data: InternalUser.count, + url: internal_users_path }, { key: 'external_users', name: t('activerecord.models.external_user.other'), - data: ExternalUser.count + data: ExternalUser.count, + url: external_users_path } ] end @@ -40,7 +42,8 @@ module StatisticsHelper { key: 'exercises', name: t('activerecord.models.exercise.other'), - data: Exercise.count + data: Exercise.count, + url: exercises_path }, { key: 'average_submissions', @@ -55,13 +58,15 @@ module StatisticsHelper { key: 'rfcs', name: t('activerecord.models.request_for_comment.other'), - data: RequestForComment.count + data: RequestForComment.count, + url: request_for_comments_path }, { key: 'percent_solved', name: t('statistics.entries.request_for_comments.percent_solved'), data: (100.0 / RequestForComment.count * RequestForComment.where(solved: true).count).round(2), - unit: '%' + unit: '%', + url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=0' }, { key: 'comments', diff --git a/app/views/statistics/show.html.slim b/app/views/statistics/show.html.slim index 8846457b..bf06d2e0 100644 --- a/app/views/statistics/show.html.slim +++ b/app/views/statistics/show.html.slim @@ -4,6 +4,7 @@ h2 = section[:name] .statistics-wrapper data-key=section[:key] - section[:entries].each do | entry | - div data-key=entry[:key] - .title = entry[:name] - .data = entry[:data].to_s + (entry[:unit] or '') + a href=entry[:url] + div data-key=entry[:key] + .title = entry[:name] + .data = entry[:data].to_s + (entry[:unit] or '') From 0fe3ce000b77bc64a410953dee7a54f031478588 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 09:20:05 +0100 Subject: [PATCH 06/19] Add more statistics --- app/helpers/statistics_helper.rb | 26 +++++++++++++++++++++++++- config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index 08e7ee88..9a8e7a29 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -33,6 +33,11 @@ module StatisticsHelper name: t('activerecord.models.external_user.other'), data: ExternalUser.count, url: external_users_path + }, + { + key: 'currently_active', + name: t('statistics.entries.users.currently_active'), + data: ExternalUser.joins(:submissions).where(['submissions.created_at >= ?', DateTime.now - 5.minutes]).count } ] end @@ -49,6 +54,18 @@ module StatisticsHelper key: 'average_submissions', name: t('statistics.entries.exercises.average_number_of_submissions'), data: Submission.count / Exercise.count + }, + { + key: 'execution_environments', + name: t('activerecord.models.execution_environment.other'), + data: ExecutionEnvironment.count, + url: execution_environments_path + }, + { + key: 'exercise_collections', + name: t('activerecord.models.exercise_collection.other'), + data: ExerciseCollection.count, + url: exercise_collections_path } ] end @@ -64,10 +81,17 @@ module StatisticsHelper { key: 'percent_solved', name: t('statistics.entries.request_for_comments.percent_solved'), - data: (100.0 / RequestForComment.count * RequestForComment.where(solved: true).count).round(2), + data: (100.0 / RequestForComment.count * RequestForComment.where(solved: true).count).round(1), unit: '%', url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=0' }, + { + key: 'percent_unsolved', + name: t('statistics.entries.request_for_comments.percent_unsolved'), + data: (100.0 / RequestForComment.count * RequestForComment.where(solved: false).count).round(1), + unit: '%', + url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=1' + }, { key: 'comments', name: t('activerecord.models.comment.other'), diff --git a/config/locales/de.yml b/config/locales/de.yml index 9e8b180c..8799808b 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -686,3 +686,6 @@ de: average_number_of_submissions: "Durchschnittliche Zahl von Abgaben" request_for_comments: percent_solved: "Beantwortete Anfragen" + percent_unsolved: "Unbeantwortete Anfragen" + users: + currently_active: "Aktiv (5 Minuten)" diff --git a/config/locales/en.yml b/config/locales/en.yml index 3dca3a02..138ba105 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -686,3 +686,6 @@ en: average_number_of_submissions: "Average Number of Submissions" request_for_comments: percent_solved: "Solved Requests" + percent_unsolved: "Unsolved Requests" + users: + currently_active: "Active (5 minutes)" From be608e4c12cdd17a11ac0a717a8a5e68c186ba9b Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 11:04:58 +0100 Subject: [PATCH 07/19] Fix query --- app/helpers/statistics_helper.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index 9a8e7a29..320d925a 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -37,7 +37,9 @@ module StatisticsHelper { key: 'currently_active', name: t('statistics.entries.users.currently_active'), - data: ExternalUser.joins(:submissions).where(['submissions.created_at >= ?', DateTime.now - 5.minutes]).count + data: ExternalUser.joins(:submissions) + .where(['submissions.created_at >= ?', DateTime.now - 5.minutes]) + .distinct('external_users.id').count } ] end From 5a2c8335814bd372347369d9537908432cc40b96 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 11:09:12 +0100 Subject: [PATCH 08/19] Improve unit visuals --- app/assets/stylesheets/statistics.css.scss | 4 ++++ app/views/statistics/show.html.slim | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index dd66f0e6..9ce3d61f 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -89,6 +89,10 @@ div.negative-result { font-size: 40px; vertical-align: middle; line-height: 50px; + + > .unit { + font-size: 20px; + } } > .title { diff --git a/app/views/statistics/show.html.slim b/app/views/statistics/show.html.slim index bf06d2e0..9b7edefe 100644 --- a/app/views/statistics/show.html.slim +++ b/app/views/statistics/show.html.slim @@ -7,4 +7,6 @@ a href=entry[:url] div data-key=entry[:key] .title = entry[:name] - .data = entry[:data].to_s + (entry[:unit] or '') + .data + span = entry[:data].to_s + span.unit = entry[:unit] if entry.key? :unit From be2a4d84fd9be0de8c2d6db0f1f2b5b8619cc278 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 11:43:59 +0100 Subject: [PATCH 09/19] Create TimeHelper module --- app/helpers/time_helper.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/helpers/time_helper.rb diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb new file mode 100644 index 00000000..ed05ede5 --- /dev/null +++ b/app/helpers/time_helper.rb @@ -0,0 +1,12 @@ +module TimeHelper + + # convert timestamps ('12:34:56.789') to seconds + def time_to_f(timestamp) + unless timestamp.nil? + timestamp = timestamp.split(':') + return timestamp[0].to_i * 60 * 60 + timestamp[1].to_i * 60 + timestamp[2].to_f + end + nil + end + +end From 119cc9ee7189d0ce46dba1fc94af9bcc708bc8cd Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 11:44:18 +0100 Subject: [PATCH 10/19] Scaffold exercise collection statistics --- app/controllers/exercise_collections_controller.rb | 11 ++++++++++- app/policies/exercise_collection_policy.rb | 4 ++++ app/views/exercise_collections/index.html.slim | 3 ++- app/views/exercise_collections/statistics.html.slim | 6 ++++++ config/routes.rb | 6 +++++- 5 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 app/views/exercise_collections/statistics.html.slim diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 4861a062..6d239d9c 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -1,7 +1,8 @@ class ExerciseCollectionsController < ApplicationController include CommonBehavior + include TimeHelper - before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy] + before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy, :statistics] def index @exercise_collections = ExerciseCollection.all.paginate(:page => params[:page]) @@ -34,6 +35,14 @@ class ExerciseCollectionsController < ApplicationController update_and_respond(object: @exercise_collection, params: exercise_collection_params) end + def statistics + @working_times = {} + @exercise_collection.exercises.each do |exercise| + @working_times[exercise.id] = time_to_f exercise.average_working_time + end + @average = @working_times.values.reduce(:+) / @working_times.size + end + private def set_exercise_collection diff --git a/app/policies/exercise_collection_policy.rb b/app/policies/exercise_collection_policy.rb index ff150290..3d6b725e 100644 --- a/app/policies/exercise_collection_policy.rb +++ b/app/policies/exercise_collection_policy.rb @@ -1,3 +1,7 @@ class ExerciseCollectionPolicy < AdminOnlyPolicy + def statistics? + admin? + end + end diff --git a/app/views/exercise_collections/index.html.slim b/app/views/exercise_collections/index.html.slim index 75a9d011..e0e8ebbc 100644 --- a/app/views/exercise_collections/index.html.slim +++ b/app/views/exercise_collections/index.html.slim @@ -8,7 +8,7 @@ h1 = ExerciseCollection.model_name.human(count: 2) th = t('activerecord.attributes.exercise_collections.name') th = t('activerecord.attributes.exercise_collections.updated_at') th = t('activerecord.attributes.exercise_collections.exercises') - th colspan=3 = t('shared.actions') + th colspan=4 = t('shared.actions') tbody - @exercise_collections.each do |collection| tr @@ -18,6 +18,7 @@ h1 = ExerciseCollection.model_name.human(count: 2) td = collection.exercises.size td = link_to(t('shared.show'), collection) td = link_to(t('shared.edit'), edit_exercise_collection_path(collection)) + td = link_to(t('shared.statistics'), statistics_exercise_collection_path(collection)) td = link_to(t('shared.destroy'), collection, data: {confirm: t('shared.confirm_destroy')}, method: :delete) = render('shared/pagination', collection: @exercise_collections) diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim new file mode 100644 index 00000000..9ca7cc78 --- /dev/null +++ b/app/views/exercise_collections/statistics.html.slim @@ -0,0 +1,6 @@ +h1 = @exercise_collection + += row(label: 'exercise_collections.name', value: @exercise_collection.name) += row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) += row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count) += row(label: 'exercises.statistics.average_worktime', value: @average.round(3).to_s + 's') diff --git a/config/routes.rb b/config/routes.rb index 21b1b719..399bee40 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -84,7 +84,11 @@ Rails.application.routes.draw do end end - resources :exercise_collections + resources :exercise_collections do + member do + get :statistics + end + end resources :proxy_exercises do member do From 0564c881059d88221264d480ffcc21314b976f61 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 13:16:00 +0100 Subject: [PATCH 11/19] Add soft solved rfcs --- app/helpers/statistics_helper.rb | 9 ++++++++- config/locales/de.yml | 1 + config/locales/en.yml | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index 320d925a..ed2d82b7 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -87,10 +87,17 @@ module StatisticsHelper unit: '%', url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=0' }, + { + key: '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), + unit: '%', + url: request_for_comments_path + }, { key: 'percent_unsolved', name: t('statistics.entries.request_for_comments.percent_unsolved'), - data: (100.0 / RequestForComment.count * RequestForComment.where(solved: false).count).round(1), + data: (100.0 / RequestForComment.count * RequestForComment.unsolved.count).round(1), unit: '%', url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=1' }, diff --git a/config/locales/de.yml b/config/locales/de.yml index 8799808b..cb042f7d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -687,5 +687,6 @@ de: request_for_comments: percent_solved: "Beantwortete Anfragen" percent_unsolved: "Unbeantwortete Anfragen" + percent_soft_solved: "Ungelöst mit voller Punktzahl" users: currently_active: "Aktiv (5 Minuten)" diff --git a/config/locales/en.yml b/config/locales/en.yml index 138ba105..4e3564bd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -687,5 +687,6 @@ en: request_for_comments: percent_solved: "Solved Requests" percent_unsolved: "Unsolved Requests" + percent_soft_solved: "Unsolved with full score" users: currently_active: "Active (5 minutes)" From a7451a505734cd4d45e389df50f752f89886deba Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 14 Mar 2018 14:53:02 +0100 Subject: [PATCH 12/19] Add RfCs with comments --- app/helpers/statistics_helper.rb | 7 +++++++ config/locales/de.yml | 1 + config/locales/en.yml | 1 + 3 files changed, 9 insertions(+) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index ed2d82b7..058cf4c1 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -106,6 +106,13 @@ module StatisticsHelper name: t('activerecord.models.comment.other'), data: Comment.count }, + { + key: 'rfcs_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 + 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 + } ] end diff --git a/config/locales/de.yml b/config/locales/de.yml index 281d349d..9b3aeb37 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -748,5 +748,6 @@ de: percent_solved: "Beantwortete Anfragen" percent_unsolved: "Unbeantwortete Anfragen" percent_soft_solved: "Ungelöst mit voller Punktzahl" + with_comments: "Anfragen mit Kommentaren" users: currently_active: "Aktiv (5 Minuten)" diff --git a/config/locales/en.yml b/config/locales/en.yml index c5e05183..93b095be 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -748,5 +748,6 @@ en: percent_solved: "Solved Requests" percent_unsolved: "Unsolved Requests" percent_soft_solved: "Unsolved with full score" + with_comments: "RfCs with Comments" users: currently_active: "Active (5 minutes)" From 6c5cd8d1ee1b4473714fc8e4ab258bb3b3e67cee Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 14:38:39 +0100 Subject: [PATCH 13/19] Use TimeHelper in rake task --- lib/tasks/detect_exercise_anomalies.rake | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake index e22550b2..3436b87a 100644 --- a/lib/tasks/detect_exercise_anomalies.rake +++ b/lib/tasks/detect_exercise_anomalies.rake @@ -22,6 +22,8 @@ namespace :detect_exercise_anomalies do AVERAGE_WORKING_TIME_CACHE = {} task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args| + include TimeHelper + number_of_exercises = args[:number_of_exercises] number_of_solutions = args[:number_of_solutions] @@ -71,14 +73,6 @@ namespace :detect_exercise_anomalies do end end - def time_to_f(timestamp) - unless timestamp.nil? - timestamp = timestamp.split(':') - return timestamp[0].to_i * 60 * 60 + timestamp[1].to_i * 60 + timestamp[2].to_f - end - nil - end - def get_average_working_time(exercise) unless AVERAGE_WORKING_TIME_CACHE.key?(exercise.id) seconds = time_to_f exercise.average_working_time From 667d1cb38b82e1037bb06f1ffbe60f9d54485f33 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 14:55:14 +0100 Subject: [PATCH 14/19] Add submission volume metric --- app/helpers/statistics_helper.rb | 8 +++++++- config/locales/de.yml | 1 + config/locales/en.yml | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/helpers/statistics_helper.rb b/app/helpers/statistics_helper.rb index 058cf4c1..bd597cc1 100644 --- a/app/helpers/statistics_helper.rb +++ b/app/helpers/statistics_helper.rb @@ -55,7 +55,13 @@ module StatisticsHelper { key: 'average_submissions', name: t('statistics.entries.exercises.average_number_of_submissions'), - data: Submission.count / Exercise.count + data: (Submission.count.to_f / Exercise.count).round(2) + }, + { + 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' }, { key: 'execution_environments', diff --git a/config/locales/de.yml b/config/locales/de.yml index 9b3aeb37..4f5b9dcb 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -744,6 +744,7 @@ de: entries: exercises: average_number_of_submissions: "Durchschnittliche Zahl von Abgaben" + submissions_per_minute: "Aktuelle Abgabenhäufigkeit (1h)" request_for_comments: percent_solved: "Beantwortete Anfragen" percent_unsolved: "Unbeantwortete Anfragen" diff --git a/config/locales/en.yml b/config/locales/en.yml index 93b095be..f65b90f4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -744,6 +744,7 @@ en: entries: exercises: average_number_of_submissions: "Average Number of Submissions" + submissions_per_minute: "Current Submission Volume (1h)" request_for_comments: percent_solved: "Solved Requests" percent_unsolved: "Unsolved Requests" From f1f1594e5b289cef41c36ec8aa1097df396b4a8c Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 15:01:50 +0100 Subject: [PATCH 15/19] Move method to model --- app/controllers/exercise_collections_controller.rb | 6 ------ app/models/exercise_collection.rb | 9 +++++++++ app/views/exercise_collections/statistics.html.slim | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb index 3a6112ef..de425dcd 100644 --- a/app/controllers/exercise_collections_controller.rb +++ b/app/controllers/exercise_collections_controller.rb @@ -1,6 +1,5 @@ class ExerciseCollectionsController < ApplicationController include CommonBehavior - include TimeHelper before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy, :statistics] @@ -37,11 +36,6 @@ class ExerciseCollectionsController < ApplicationController end def statistics - @working_times = {} - @exercise_collection.exercises.each do |exercise| - @working_times[exercise.id] = time_to_f exercise.average_working_time - end - @average = @working_times.values.reduce(:+) / @working_times.size end private diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index f9f09269..249e7269 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -1,8 +1,17 @@ class ExerciseCollection < ActiveRecord::Base + include TimeHelper has_and_belongs_to_many :exercises belongs_to :user, polymorphic: true + def average_working_time + working_times = {} + exercises.each do |exercise| + working_times[exercise.id] = time_to_f exercise.average_working_time + end + working_times.values.reduce(:+) / working_times.size + end + def to_s "#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})" end diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 9ca7cc78..686c79c1 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -3,4 +3,4 @@ h1 = @exercise_collection = row(label: 'exercise_collections.name', value: @exercise_collection.name) = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) = row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count) -= row(label: 'exercises.statistics.average_worktime', value: @average.round(3).to_s + 's') += row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's') From 2fc46fe9abe4eef3c1c9a02c53f7ef7b1720cfd7 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 15:33:12 +0100 Subject: [PATCH 16/19] Fix d3 at ~4.0 --- Gemfile | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 6a6486a6..076ed2aa 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ gem 'tubesock' gem 'faye-websocket' gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, version 1.2.5 still has an error in eventmachine.rb:202: [BUG] Segmentation fault, which is not yet fixed and causes the whole ruby process to crash gem 'nokogiri' -gem 'd3-rails' +gem 'd3-rails', '~>4.0' gem 'rest-client' gem 'rubyzip' gem 'whenever', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 7edb56f0..456aeb24 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -388,7 +388,7 @@ DEPENDENCIES coffee-rails concurrent-ruby concurrent-ruby-ext - d3-rails + d3-rails (~> 4.0) database_cleaner docker-api eventmachine (= 1.0.9.1) From 36d0c2839dcfb6c5b61ae9e72fdfa460eaa47d6b Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 15:33:37 +0100 Subject: [PATCH 17/19] Fix working time graphs that were broken by d3 update --- app/assets/javascripts/exercise_graphs.js | 56 +++++-------------- app/assets/javascripts/working_time_graphs.js | 37 ++++-------- 2 files changed, 23 insertions(+), 70 deletions(-) diff --git a/app/assets/javascripts/exercise_graphs.js b/app/assets/javascripts/exercise_graphs.js index 5f521b39..b095e1a5 100644 --- a/app/assets/javascripts/exercise_graphs.js +++ b/app/assets/javascripts/exercise_graphs.js @@ -1,9 +1,7 @@ $(function() { - // http://localhost:3333/exercises/38/statistics good for testing - // originally at--> localhost:3333/exercises/69/statistics + // /exercises/38/statistics good for testing if ($.isController('exercises') && $('.graph-functions-2').isPresent()) { - // GET THE DATA var submissions = $('#data').data('submissions'); var submissions_length = submissions.length; @@ -14,10 +12,7 @@ $(function() { submissionsAutosaves = []; var maximumValue = 0; - var wtimes = $('#wtimes').data('working_times'); //.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until) - - // console.log(submissions); - // console.log(wtimes); + var wtimes = $('#wtimes').data('working_times'); for (var i = 0;i

") - - // var minutes_count = new Array(10); - // var minutes_array_len = minutes_array.length; - // for (var i=0; i< minutes_count; i++){ - // - // for (var j = 0; j < minutes_array_len; j++){ - // if () - // } - // } - function getWidth() { if (self.innerHeight) { return self.innerWidth; @@ -81,22 +69,17 @@ $(function() { //var formatDate = d3.time.format("%M"); - var x = d3.scale.linear() + var x = d3.scaleLinear() .range([0, width]); - var y = d3.scale.linear() + var y = d3.scaleLinear() .range([height, 0]); // - (height/20 - var xAxis = d3.svg.axis() - .scale(x) - .orient("bottom") - .ticks(20); - var yAxis = d3.svg.axis() - .scale(y) - .orient("left") + var xAxis = d3.axisBottom(x).ticks(20); + var yAxis = d3.axisLeft(y) .ticks(20) - .innerTickSize(-width) - .outerTickSize(0); + .tickSizeInner(-width) + .tickSizeOuter(0); - var line = d3.svg.line() + var line = d3.line() .x(function (d, i) { return x(i); }) @@ -225,7 +208,7 @@ $(function() { var x = d3.scale.ordinal() .rangeRoundBands([0, width], .1); - var y = d3.scale.linear() + var y = d3.scaleLinear() .range([0,height-(margin.top + margin.bottom)]); @@ -236,7 +219,7 @@ $(function() { var yAxis = d3.svg.axis() - .scale(d3.scale.linear().domain([0,max_of_array]).range([height,0]))//y + .scale(d3.scaleLinear().domain([0,max_of_array]).range([height,0]))//y .orient("left") .ticks(10) .innerTickSize(-width); @@ -299,7 +282,7 @@ $(function() { .text("Working Time (Minutes)") .style('font-size', 14); - y = d3.scale.linear() + y = d3.scaleLinear() .domain([(0),max_of_array]) .range([0,height]); From d55b39eb487859ed377c19e4f76b3cc9cef5c310 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 18:17:34 +0100 Subject: [PATCH 18/19] Visualize exercise working times in exercise collection statstics --- .../javascripts/exercise_collections.js.erb | 103 ++++++++++++++++++ .../stylesheets/exercise_collections.scss | 22 ++++ app/models/exercise_collection.rb | 8 +- .../exercise_collections/statistics.html.slim | 3 + 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/exercise_collections.js.erb create mode 100644 app/assets/stylesheets/exercise_collections.scss diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb new file mode 100644 index 00000000..7e048b55 --- /dev/null +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -0,0 +1,103 @@ +$(function() { + if ($.isController('exercise_collections')) { + var data = $('#data').data('working-times'); + var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time')); + + var margin = { top: 30, right: 40, bottom: 30, left: 50 }, + width = 720 - margin.left - margin.right, + height = 500 - margin.top - margin.bottom; + + var x = d3.scaleBand().range([0, width]); + var y = d3.scaleLinear().range([height, 0]); + + var xAxis = d3.axisBottom(x); + var yAxisLeft = d3.axisLeft(y); + + var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip"); + + var averageWorkingTime = d3.line() + .x(function (d) { return x(d.index) + x.bandwidth()/2; }) + .y(function () { return y(averageWorkingTimeValue); }); + + var minWorkingTime = d3.line() + .x(function (d) { return x(d.index) + x.bandwidth()/2; }) + .y(function () { return y(0.1*averageWorkingTimeValue); }); + + var maxWorkingTime = d3.line() + .x(function (d) { return x(d.index) + x.bandwidth()/2; }) + .y(function () { return y(2*averageWorkingTimeValue); }); + + var svg = d3.select('#graph') + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", + "translate(" + margin.left + "," + margin.top + ")"); + + // Get the data + data = Object.keys(data).map(function (key, index) { + return { + index: index, + exercise_id: parseInt(key), + working_time: parseFloat(data[key]) + }; + }); + + // Scale the range of the data + x.domain(data.map(function (d) { return d.index; })); + y.domain([0, d3.max(data, function (d) { return d.working_time; })]); + + // Add the X Axis + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + // Add the Y Axis + svg.append("g") + .attr("class", "y axis") + .style("fill", "steelblue") + .call(yAxisLeft); + + // Draw the bars + svg.selectAll("bar") + .data(data) + .enter() + .append("rect") + .style("fill", "#008cba") + .style("cursor", "pointer") + .on("mousemove", function (d){ + tooltip + .style("left", d3.event.pageX - 50 + "px") + .style("top", d3.event.pageY + 50 + "px") + .style("display", "inline-block") + .html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "
" + + "<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s"); + }) + .on("mouseout", function (){ tooltip.style("display", "none");}) + .on("click", function (d) { + window.location.href = "/exercises/" + d.exercise_id + "/statistics"; + }) + .attr("x", function (d) { return x(d.index); }) + .attr("width", x.bandwidth()) + .attr("y", function (d) { return y(d.working_time); }) + .attr("height", function (d) { return height - y(d.working_time); }); + + // Add the average working time path + svg.append("path") + .datum(data) + .attr("class", "line average-working-time") + .attr("d", averageWorkingTime); + + // Add the anomaly paths (min/max average exercise working time) + svg.append("path") + .datum(data) + .attr("class", "line minimum-working-time") + .attr("d", minWorkingTime); + svg.append("path") + .datum(data) + .attr("class", "line maximum-working-time") + .attr("d", maxWorkingTime); + } +}); diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss new file mode 100644 index 00000000..6a2d630e --- /dev/null +++ b/app/assets/stylesheets/exercise_collections.scss @@ -0,0 +1,22 @@ +path.line.minimum-working-time { + stroke: #8efa00; +} + +path.line.average-working-time { + stroke: #ffca00; +} + +path.line.maximum-working-time { + stroke: #ff2600; +} + +.exercise-id-tooltip { + position: absolute; + display: none; + min-width: 80px; + height: auto; + background: none repeat scroll 0 0 #ffffff; + border: 1px solid #008cba; + padding: 14px; + text-align: center; +} diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb index 249e7269..661bed81 100644 --- a/app/models/exercise_collection.rb +++ b/app/models/exercise_collection.rb @@ -4,12 +4,16 @@ class ExerciseCollection < ActiveRecord::Base has_and_belongs_to_many :exercises belongs_to :user, polymorphic: true - def average_working_time + def exercise_working_times working_times = {} exercises.each do |exercise| working_times[exercise.id] = time_to_f exercise.average_working_time end - working_times.values.reduce(:+) / working_times.size + working_times + end + + def average_working_time + exercise_working_times.values.reduce(:+) / exercises.size end def to_s diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 686c79c1..4a8a04ac 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -4,3 +4,6 @@ h1 = @exercise_collection = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) = row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count) = row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's') + +#graph + #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time) From 0e613d941d0bdfd85988088b36e7efe654549ce7 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Mon, 19 Mar 2018 18:45:50 +0100 Subject: [PATCH 19/19] Add legend --- .../javascripts/exercise_collections.js.erb | 3 +- .../stylesheets/exercise_collections.scss | 53 +++++++++++++++++-- .../exercise_collections/statistics.html.slim | 8 +++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb index 7e048b55..3530b636 100644 --- a/app/assets/javascripts/exercise_collections.js.erb +++ b/app/assets/javascripts/exercise_collections.js.erb @@ -65,8 +65,7 @@ $(function() { .data(data) .enter() .append("rect") - .style("fill", "#008cba") - .style("cursor", "pointer") + .attr("class", "value-bar") .on("mousemove", function (d){ tooltip .style("left", d3.event.pageX - 50 + "px") diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss index 6a2d630e..11b6b3a1 100644 --- a/app/assets/stylesheets/exercise_collections.scss +++ b/app/assets/stylesheets/exercise_collections.scss @@ -1,13 +1,60 @@ +$time-color: #008cba; +$min-color: #8efa00; +$avg-color: #ffca00; +$max-color: #ff2600; + path.line.minimum-working-time { - stroke: #8efa00; + stroke: $min-color; } path.line.average-working-time { - stroke: #ffca00; + stroke: $avg-color; } path.line.maximum-working-time { - stroke: #ff2600; + stroke: $max-color; +} + +rect.value-bar { + fill: $time-color; + cursor: pointer; +} + +#legend { + display: flex; + margin-top: 20px; + + .legend-entry { + flex-grow: 1; + display: flex; + + .box { + width: 20px; + height: 20px; + border: solid 1px #000; + } + + .box.time { + background-color: $time-color; + } + + .box.min { + background-color: $min-color; + } + + .box.avg { + background-color: $avg-color; + } + + .box.max { + background-color: $max-color; + } + + .box-label { + margin-left: 5px; + margin-right: 15px; + } + } } .exercise-id-tooltip { diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 4a8a04ac..486a0dbd 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -7,3 +7,11 @@ h1 = @exercise_collection #graph #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time) + #legend + - {time: t('exercises.statistics.average_worktime'), + min: 'min. anomaly threshold', + avg: 'average time', + max: 'max. anomaly threshold'}.each_pair do |klass, label| + .legend-entry + div(class="box #{klass}") + .box-label = label