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 {
|
.markdown {
|
||||||
height: 200px;
|
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
|
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
|
end
|
||||||
|
@ -8,6 +8,7 @@ module User
|
|||||||
has_many :exercises, as: :user
|
has_many :exercises, as: :user
|
||||||
has_many :file_types, as: :user
|
has_many :file_types, as: :user
|
||||||
has_many :submissions, 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_proxy_exercise_exercises, as: :user
|
||||||
has_many :user_exercise_interventions, as: :user
|
has_many :user_exercise_interventions, as: :user
|
||||||
has_many :interventions, through: :user_exercise_interventions
|
has_many :interventions, through: :user_exercise_interventions
|
||||||
|
@ -214,8 +214,8 @@ class ProxyExercise < ActiveRecord::Base
|
|||||||
ex.tags.each do |t|
|
ex.tags.each do |t|
|
||||||
tags_counter[t] += 1
|
tags_counter[t] += 1
|
||||||
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
|
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
|
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}, 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 #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
|
||||||
Rails.logger.debug("tag_ratio #{tag_ratio}")
|
Rails.logger.debug("tag_ratio #{tag_ratio}")
|
||||||
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
|
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
|
||||||
@ -226,11 +226,10 @@ class ProxyExercise < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
||||||
end
|
end
|
||||||
private :get_user_knowledge_and_max_knowledge
|
|
||||||
|
|
||||||
def tag_diminishing_return_function(count_tag, total_count_tag)
|
def tag_diminishing_return_function(count_tag, total_count_tag)
|
||||||
total_count_tag += 1 # bonus exercise comes on top
|
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
|
end
|
||||||
|
|
||||||
def select_easiest_exercise(exercises)
|
def select_easiest_exercise(exercises)
|
||||||
|
@ -2,4 +2,8 @@ class ExternalUserPolicy < AdminOnlyPolicy
|
|||||||
def statistics?
|
def statistics?
|
||||||
admin?
|
admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tag_statistics?
|
||||||
|
admin?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,5 +4,12 @@ h1 = @user.name
|
|||||||
//= row(label: 'external_user.email', value: @user.email)
|
//= row(label: 'external_user.email', value: @user.email)
|
||||||
= row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer))
|
= row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer))
|
||||||
|
|
||||||
br
|
h4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user))
|
||||||
= link_to(t('shared.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
|
score: Bewertung
|
||||||
runs: Abgaben
|
runs: Abgaben
|
||||||
worktime: Arbeitszeit
|
worktime: Arbeitszeit
|
||||||
|
show:
|
||||||
|
loading_tag_statistics: "Lade Lernbereichstatistiken"
|
||||||
|
tag_statistics: "Lernbereichstatistiken"
|
||||||
|
empty_tag_statistics: "Keine Statistiken verfügbar"
|
||||||
|
exercise_statistics: "Aufgabenstatistiken"
|
||||||
files:
|
files:
|
||||||
roles:
|
roles:
|
||||||
main_file: Hauptdatei
|
main_file: Hauptdatei
|
||||||
|
@ -380,6 +380,11 @@ en:
|
|||||||
score: Score
|
score: Score
|
||||||
runs: Submissions
|
runs: Submissions
|
||||||
worktime: Working Time
|
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:
|
files:
|
||||||
roles:
|
roles:
|
||||||
main_file: Main File
|
main_file: Main File
|
||||||
|
@ -128,6 +128,9 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :external_users, only: [:index, :show], concerns: :statistics do
|
resources :external_users, only: [:index, :show], concerns: :statistics do
|
||||||
resources :exercises, concerns: :statistics
|
resources :exercises, concerns: :statistics
|
||||||
|
member do
|
||||||
|
get :tag_statistics
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :code_ocean do
|
namespace :code_ocean do
|
||||||
|
@ -61,16 +61,25 @@ namespace :detect_exercise_anomalies do
|
|||||||
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
|
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_anomalies(collection)
|
def collect_working_times(collection)
|
||||||
working_times = {}
|
working_times = {}
|
||||||
collection.exercises.each do |exercise|
|
collection.exercises.each do |exercise|
|
||||||
puts "\t\t> #{exercise.title}"
|
puts "\t\t> #{exercise.title}"
|
||||||
working_times[exercise.id] = get_average_working_time(exercise)
|
working_times[exercise.id] = get_average_working_time(exercise)
|
||||||
end
|
end
|
||||||
average = working_times.values.reduce(:+) / working_times.size
|
working_times
|
||||||
working_times.select do |exercise_id, working_time|
|
end
|
||||||
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
|
|
||||||
|
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
|
||||||
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_average_working_time(exercise)
|
def get_average_working_time(exercise)
|
||||||
|
Reference in New Issue
Block a user