Merge branch 'fix/exercise-anomaly-detection-nil-values' into feature/ordered_exercise_collection
This commit is contained in:
32
app/assets/javascripts/external_users.js
Normal file
32
app/assets/javascripts/external_users.js
Normal file
@ -0,0 +1,32 @@
|
||||
$(function() {
|
||||
var grid = $('#tag-grid');
|
||||
|
||||
if ($.isController('external_users') && grid.isPresent()) {
|
||||
var spinner = $('#loading');
|
||||
var noElements = $('#no-elements');
|
||||
|
||||
var buildTagContainer = function(tag) {
|
||||
return '\
|
||||
<div class="tag">\
|
||||
<div class="name">' + tag.key + '</div>\
|
||||
<div class="progress">\
|
||||
<div class="progress-bar" role="progressbar" style="width:' + tag.value + '%">' + tag.value + '%</div>\
|
||||
</div>\
|
||||
</div>';
|
||||
};
|
||||
|
||||
var jqxhr = $.ajax(window.location.href + '/tag_statistics', {
|
||||
dataType: 'json',
|
||||
method: 'GET'
|
||||
});
|
||||
jqxhr.done(function(response) {
|
||||
spinner.hide();
|
||||
if (response.length === 0) {
|
||||
noElements.show();
|
||||
} else {
|
||||
var elements = response.map(buildTagContainer);
|
||||
grid.append(elements);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -59,3 +59,34 @@ span.caret {
|
||||
.markdown {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
16
app/assets/stylesheets/external_users.css.scss
Normal file
16
app/assets/stylesheets/external_users.css.scss
Normal file
@ -0,0 +1,16 @@
|
||||
#no-elements {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tag-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 25% 25% 25% 25%;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 15px;
|
||||
|
||||
.progress {
|
||||
.progress-bar {
|
||||
min-width: 2em;
|
||||
}
|
||||
}
|
||||
}
|
@ -120,34 +120,3 @@ tr.highlight {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -63,4 +63,20 @@ class ExternalUsersController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
def tag_statistics
|
||||
@user = ExternalUser.find(params[:id])
|
||||
authorize!
|
||||
|
||||
statistics = []
|
||||
tags = ProxyExercise.new().get_user_knowledge_and_max_knowledge(@user, @user.participations.uniq.compact)
|
||||
tags[:user_topic_knowledge].each_pair do |key, value|
|
||||
statistics.append({:key => key.name.to_s, :value => (100.0 / tags[:max_topic_knowledge][key] * value).round})
|
||||
end
|
||||
statistics.sort_by! {|item| -item[:value]}
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render(json: statistics) }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ module User
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :user
|
||||
has_many :participations, through: :submissions, source: :exercise, as: :user
|
||||
has_many :user_proxy_exercise_exercises, as: :user
|
||||
has_many :user_exercise_interventions, as: :user
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
|
@ -214,8 +214,8 @@ class ProxyExercise < ActiveRecord::Base
|
||||
ex.tags.each do |t|
|
||||
tags_counter[t] += 1
|
||||
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
|
||||
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
|
||||
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}")
|
||||
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum + et.factor }.to_f
|
||||
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum + et.factor }}")
|
||||
Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
|
||||
Rails.logger.debug("tag_ratio #{tag_ratio}")
|
||||
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
|
||||
@ -226,11 +226,10 @@ class ProxyExercise < ActiveRecord::Base
|
||||
end
|
||||
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
||||
end
|
||||
private :get_user_knowledge_and_max_knowledge
|
||||
|
||||
def tag_diminishing_return_function(count_tag, total_count_tag)
|
||||
total_count_tag += 1 # bonus exercise comes on top
|
||||
return 1/(1+(Math::E**(-3/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))
|
||||
1 / (1 + (Math::E**(-3 / (0.5 * total_count_tag) * (count_tag - 0.5 * total_count_tag))))
|
||||
end
|
||||
|
||||
def select_easiest_exercise(exercises)
|
||||
|
@ -2,4 +2,8 @@ class ExternalUserPolicy < AdminOnlyPolicy
|
||||
def statistics?
|
||||
admin?
|
||||
end
|
||||
|
||||
def tag_statistics?
|
||||
admin?
|
||||
end
|
||||
end
|
||||
|
@ -4,5 +4,12 @@ h1 = @user.name
|
||||
//= row(label: 'external_user.email', value: @user.email)
|
||||
= row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer))
|
||||
|
||||
br
|
||||
= link_to(t('shared.statistics'), statistics_external_user_path(@user))
|
||||
h4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user))
|
||||
|
||||
h4 = t('.tag_statistics')
|
||||
#loading
|
||||
.spinner
|
||||
= t('.loading_tag_statistics')
|
||||
#no-elements
|
||||
= t('.empty_tag_statistics')
|
||||
#tag-grid
|
||||
|
@ -380,6 +380,11 @@ de:
|
||||
score: Bewertung
|
||||
runs: Abgaben
|
||||
worktime: Arbeitszeit
|
||||
show:
|
||||
loading_tag_statistics: "Lade Lernbereichstatistiken"
|
||||
tag_statistics: "Lernbereichstatistiken"
|
||||
empty_tag_statistics: "Keine Statistiken verfügbar"
|
||||
exercise_statistics: "Aufgabenstatistiken"
|
||||
files:
|
||||
roles:
|
||||
main_file: Hauptdatei
|
||||
|
@ -380,6 +380,11 @@ en:
|
||||
score: Score
|
||||
runs: Submissions
|
||||
worktime: Working Time
|
||||
show:
|
||||
loading_tag_statistics: "Loading tag statistics..."
|
||||
tag_statistics: "Tag Statistics"
|
||||
empty_tag_statistics: "No statistics available"
|
||||
exercise_statistics: "Exercise Statistics"
|
||||
files:
|
||||
roles:
|
||||
main_file: Main File
|
||||
|
@ -128,6 +128,9 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :external_users, only: [:index, :show], concerns: :statistics do
|
||||
resources :exercises, concerns: :statistics
|
||||
member do
|
||||
get :tag_statistics
|
||||
end
|
||||
end
|
||||
|
||||
namespace :code_ocean do
|
||||
|
@ -61,16 +61,25 @@ namespace :detect_exercise_anomalies do
|
||||
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
|
||||
end
|
||||
|
||||
def find_anomalies(collection)
|
||||
def collect_working_times(collection)
|
||||
working_times = {}
|
||||
collection.exercises.each do |exercise|
|
||||
puts "\t\t> #{exercise.title}"
|
||||
working_times[exercise.id] = get_average_working_time(exercise)
|
||||
end
|
||||
average = working_times.values.reduce(:+) / working_times.size
|
||||
working_times.select do |exercise_id, working_time|
|
||||
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
|
||||
working_times
|
||||
end
|
||||
|
||||
def find_anomalies(collection)
|
||||
working_times = collect_working_times(collection)
|
||||
values = working_times.values.reject {|value| value.nil?}
|
||||
if working_times.size > 0
|
||||
average = values.reduce(:+) / working_times.size
|
||||
return working_times.select do |_, working_time|
|
||||
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
|
||||
end
|
||||
end
|
||||
{}
|
||||
end
|
||||
|
||||
def get_average_working_time(exercise)
|
||||
|
Reference in New Issue
Block a user